Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle properly stringifying multiline macro expressions #15305

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
11 changes: 6 additions & 5 deletions spec/compiler/crystal/tools/expand_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ describe "expand" do
{% end %}
CRYSTAL

assert_expand_simple code, "1\n2\n3\n"
assert_expand_simple code, "1\n\n2\n\n3\n"
end

it "expands macro control {% for %} with cursor inside it" do
Expand All @@ -167,7 +167,7 @@ describe "expand" do
{% end %}
CRYSTAL

assert_expand_simple code, "1\n2\n3\n"
assert_expand_simple code, "1\n\n2\n\n3\n"
end

it "expands macro control {% for %} with cursor at end of it" do
Expand All @@ -177,7 +177,7 @@ describe "expand" do
‸{% end %}
CRYSTAL

assert_expand_simple code, "1\n2\n3\n"
assert_expand_simple code, "1\n\n2\n\n3\n"
end

it "expands macro control {% for %} with indent" do
Expand All @@ -195,7 +195,7 @@ describe "expand" do
{% end %}
CRYSTAL

assert_expand_simple code, original: original, expanded: "1\n2\n3\n"
assert_expand_simple code, original: original, expanded: "1\n\n2\n\n3\n"
end

it "expands simple macro" do
Expand Down Expand Up @@ -258,7 +258,7 @@ describe "expand" do
‸foo
CRYSTAL

assert_expand_simple code, original: "foo", expanded: %("if true"\n"1"\n"2"\n"3"\n)
assert_expand_simple code, original: "foo", expanded: %("if true"\n\n\n"1"\n\n"2"\n\n"3"\n)
end

it "expands macros with 2 level" do
Expand Down Expand Up @@ -615,6 +615,7 @@ describe "expand" do
def hello_str
"hello"
end

# symbol of hello
def hello_sym
:hello
Expand Down
135 changes: 132 additions & 3 deletions spec/compiler/parser/to_s_spec.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require "../../support/syntax"

private def expect_to_s(original, expected = original, emit_doc = false, file = __FILE__, line = __LINE__)
it "does to_s of #{original.inspect}", file, line do
private def expect_to_s(original, expected = original, emit_doc = false, file = __FILE__, line = __LINE__, focus = false)
it "does to_s of #{original.inspect}", file, line, focus: focus do
str = IO::Memory.new expected.bytesize

source = original
Expand Down Expand Up @@ -246,8 +246,137 @@ describe "ASTNode#to_s" do
expect_to_s "1.+(&block)"
expect_to_s "1.//(2, a: 3)"
expect_to_s "1.//(2, &block)"
expect_to_s %({% verbatim do %}\n 1{{ 2 }}\n 3{{ 4 }}\n{% end %})
expect_to_s <<-'CR'
{% verbatim do %}
1{{ 2 }}
3{{ 4 }}
{% end %}
CR

expect_to_s %({% for foo in bar %}\n {{ if true\n foo\n bar\nend }}\n{% end %})
expect_to_s "{% a = 1 %}"
expect_to_s "{{ a = 1 }}"
expect_to_s "{%\n 1\n 2\n 3\n%}"
expect_to_s "{%\n 1\n%}"
expect_to_s "{%\n 2 + 2\n%}"
expect_to_s "{%\n a = 1 %}", "{%\n a = 1\n%}"
expect_to_s "{% a = 1\n%}", "{% a = 1 %}"

expect_to_s <<-'CR', <<-'CR'
macro finished
{% verbatim do %}
{%
10

# Foo

20
%}
{% end %}
end
CR
macro finished
{% verbatim do %}
{%
10



20
%}
{% end %}
end
CR

expect_to_s <<-'CR', <<-'CR'
macro finished
{% verbatim do %}
{%
10

# Foo
20
%}
{% end %}
end
CR
macro finished
{% verbatim do %}
{%
10


20
%}
{% end %}
end
CR

expect_to_s <<-'CR', <<-'CR'
macro finished
{% verbatim do %}
{%
10

# Foo

20
30

# Bar

40
%}
{%
50
60
%}
{% end %}
end
CR
macro finished
{% verbatim do %}
{%
10



20
30



40
%}
{%
50
60
%}
{% end %}
end
CR

expect_to_s <<-'CR'
macro finished
{% verbatim do %}
{%
10
20
%}
{% end %}
end
CR

expect_to_s <<-'CR'
macro finished
{% verbatim do %}
{%
10
%}
{% end %}
end
CR

expect_to_s %(asm("nop" ::::))
expect_to_s %(asm("nop" : "a"(1), "b"(2) : "c"(3), "d"(4) : "e", "f" : "volatile", "alignstack", "intel"))
expect_to_s %(asm("nop" :: "c"(3), "d"(4) ::))
Expand Down
6 changes: 6 additions & 0 deletions spec/compiler/semantic/restrictions_augmenter_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ describe "Semantic: restrictions augmenter" do
class Baz
end
end

@x : Bar::Baz

def initialize(value : ::Foo::Bar::Baz)
@x = value
end
Expand Down Expand Up @@ -110,7 +112,9 @@ describe "Semantic: restrictions augmenter" do
class Baz
end
end

@x : Bar::Baz

def initialize(value : Bar::Baz)
@x = value
end
Expand Down Expand Up @@ -400,8 +404,10 @@ describe "Semantic: restrictions augmenter" do
macro foo
{{ yield }}
end

class Foo
end

class Bar
@x : Foo
def initialize(value : ::Foo)
Expand Down
70 changes: 59 additions & 11 deletions src/compiler/crystal/syntax/to_s.cr
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ module Crystal
true
end

private def write_extra_newlines(first_node_location : Location?, second_node_location : Location?, &) : Nil
# If any location information is missing, don't add any extra newlines.
if !first_node_location || !second_node_location
yield
return
end

# Only write the "extra" newlines. I.e. If there are more than one. The first newline is handled directly via the Expressions visitor.
((second_node_location.line_number - 1) - first_node_location.line_number).times do
newline
end

yield
end

def visit(node : Nop)
false
end
Expand Down Expand Up @@ -221,11 +236,17 @@ module Crystal
if @inside_macro > 0
node.expressions.each &.accept self
else
last_node = nil

node.expressions.each_with_index do |exp, i|
unless exp.nop?
append_indent unless node.keyword.paren? && i == 0
exp.accept self
newline unless node.keyword.paren? && i == node.expressions.size - 1
self.write_extra_newlines((last_node || exp).end_location, exp.location) do
append_indent unless node.keyword.paren? && i == 0
exp.accept self
newline unless node.keyword.paren? && i == node.expressions.size - 1
end

last_node = exp
end
end
end
Expand Down Expand Up @@ -717,8 +738,10 @@ module Crystal
end
newline

inside_macro do
accept node.body
with_indent do
inside_macro do
accept node.body
end
end

# newline
Expand All @@ -728,13 +751,34 @@ module Crystal
end

def visit(node : MacroExpression)
@str << (node.output? ? "{{" : "{% ")
@str << ' ' if node.output?
# The node is considered multiline if its starting location is on a different line than its expression.
is_multiline = (start_loc = node.location) && (end_loc = node.exp.location) && end_loc.line_number > start_loc.line_number

@str << (node.output? ? "{{ " : is_multiline ? "{%" : "{% ")

if is_multiline
newline
@indent += 1
end

outside_macro do
# If the MacroExpression consists of a single node we need to manually handle appending indent and trailing newline if *is_multiline*
# Otherwise, the Expressions logic handles that for us
if is_multiline && !node.exp.is_a?(Expressions)
append_indent
end

node.exp.accept self
end
@str << ' ' if node.output?
@str << (node.output? ? "}}" : " %}")

# If the opening tag has a newline after it, force trailing tag to have one as well
if is_multiline
@indent -= 1
newline if !node.exp.is_a? Expressions
append_indent
end

@str << (node.output? ? " }}" : is_multiline ? "%}" : " %}")
false
end

Expand Down Expand Up @@ -790,9 +834,13 @@ module Crystal

def visit(node : MacroVerbatim)
@str << "{% verbatim do %}"
inside_macro do
node.exp.accept self

with_indent do
inside_macro do
node.exp.accept self
end
end

@str << "{% end %}"
false
end
Expand Down
Loading