diff --git a/spec/compiler/crystal/tools/expand_spec.cr b/spec/compiler/crystal/tools/expand_spec.cr index e8f9b770f3ec..4ebdfff6b836 100644 --- a/spec/compiler/crystal/tools/expand_spec.cr +++ b/spec/compiler/crystal/tools/expand_spec.cr @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -615,6 +615,7 @@ describe "expand" do def hello_str "hello" end + # symbol of hello def hello_sym :hello diff --git a/spec/compiler/parser/parser_spec.cr b/spec/compiler/parser/parser_spec.cr index ab8b1e9edfca..5d0e466e0872 100644 --- a/spec/compiler/parser/parser_spec.cr +++ b/spec/compiler/parser/parser_spec.cr @@ -2841,6 +2841,146 @@ module Crystal location.line_number.should eq 8 end + it "sets correct location for output macro expression in for loop" do + parser = Parser.new(<<-CR) + {% for foo in bar %} + {{ if true + foo + bar + end }} + {% end %} + CR + + node = parser.parse.should be_a MacroFor + node = node.body.should be_a Expressions + + node = node.expressions[1].should be_a MacroExpression + + location = node.location.should_not be_nil + location.line_number.should eq 2 + location = node.end_location.should_not be_nil + location.line_number.should eq 5 + + node = node.exp.should be_a If + + location = node.location.should_not be_nil + location.line_number.should eq 2 + location = node.end_location.should_not be_nil + location.line_number.should eq 5 + end + + it "sets correct location for single node within another node" do + parser = Parser.new(<<-CR) + macro finished + {% verbatim do %} + {% + + a = 1 %} + {% end %} + end + CR + + node = parser.parse.should be_a Macro + node = node.body.should be_a Expressions + node = node.expressions[1].should be_a MacroVerbatim + node = node.exp.should be_a Expressions + node = node.expressions[1].should be_a MacroExpression + + location = node.location.should_not be_nil + location.line_number.should eq 3 + location = node.end_location.should_not be_nil + location.line_number.should eq 5 + + assign = node.exp.should be_a Assign + + location = assign.location.should_not be_nil + location.line_number.should eq 5 + location = assign.end_location.should_not be_nil + location.line_number.should eq 5 + + target = assign.target.should be_a Var + + location = target.location.should_not be_nil + location.line_number.should eq 5 + location = target.end_location.should_not be_nil + location.line_number.should eq 5 + + value = assign.value.should be_a NumberLiteral + + location = value.location.should_not be_nil + location.line_number.should eq 5 + location = value.end_location.should_not be_nil + location.line_number.should eq 5 + end + + it "sets correct location for multiple nodes within another node" do + parser = Parser.new(<<-CR) + macro finished + {% verbatim do %} + {% + + + a = 1 + b = 2 %} + {% end %} + end + CR + + node = parser.parse.should be_a Macro + node = node.body.should be_a Expressions + node = node.expressions[1].should be_a MacroVerbatim + node = node.exp.should be_a Expressions + node = node.expressions[1].should be_a MacroExpression + + location = node.location.should_not be_nil + location.line_number.should eq 3 + location = node.end_location.should_not be_nil + location.line_number.should eq 7 + + node = node.exp.should be_a Expressions + assign = node.expressions[0].should be_a Assign + + location = assign.location.should_not be_nil + location.line_number.should eq 6 + location = assign.end_location.should_not be_nil + location.line_number.should eq 6 + + target = assign.target.should be_a Var + + location = target.location.should_not be_nil + location.line_number.should eq 6 + location = target.end_location.should_not be_nil + location.line_number.should eq 6 + + value = assign.value.should be_a NumberLiteral + + location = value.location.should_not be_nil + location.line_number.should eq 6 + location = value.end_location.should_not be_nil + location.line_number.should eq 6 + + assign = node.expressions[1].should be_a Assign + + location = assign.location.should_not be_nil + location.line_number.should eq 7 + location = assign.end_location.should_not be_nil + location.line_number.should eq 7 + + target = assign.target.should be_a Var + + location = target.location.should_not be_nil + location.line_number.should eq 7 + location = target.end_location.should_not be_nil + location.line_number.should eq 7 + + value = assign.value.should be_a NumberLiteral + + location = value.location.should_not be_nil + location.line_number.should eq 7 + location = value.end_location.should_not be_nil + location.line_number.should eq 7 + end + it "sets correct location of trailing ensure" do parser = Parser.new("foo ensure bar") node = parser.parse.as(ExceptionHandler) diff --git a/spec/compiler/parser/to_s_spec.cr b/spec/compiler/parser/to_s_spec.cr index 86464e197267..2f35c0b6a064 100644 --- a/spec/compiler/parser/to_s_spec.cr +++ b/spec/compiler/parser/to_s_spec.cr @@ -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 @@ -246,8 +246,194 @@ 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 %({% for foo in bar %}\n {{ if true\n foo\n bar\nend }}\n{% end %}) + expect_to_s <<-'CR' + {% verbatim do %} + 1{{ 2 }} + 3{{ 4 }} + {% end %} + CR + + expect_to_s <<-'CR', <<-'CR' + {% for foo in bar %} + {{ if true + foo + bar + end }} + {% end %} + CR + {% for foo in bar %} + {{ if true + foo + bar + end }} + {% end %} + CR + + 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 %}" + expect_to_s "{% a = 1\n%}" + + 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 <<-'CR' + macro finished + {% verbatim do %} + {% + + a = 1 %} + {% end %} + end + CR + + expect_to_s <<-'CR' + macro finished + {% verbatim do %} + {% + + + a = 1 + b = 2 %} + {% end %} + end + CR + + expect_to_s <<-'CR', <<-'CR' + macro finished + {% verbatim do %} + {% a = 1 + b = 2 + + %} + {% end %} + end + CR + macro finished + {% verbatim do %} + {% a = 1 + b = 2 + + %} + {% 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) ::)) diff --git a/spec/compiler/semantic/restrictions_augmenter_spec.cr b/spec/compiler/semantic/restrictions_augmenter_spec.cr index 2b7250658693..d77b61327f82 100644 --- a/spec/compiler/semantic/restrictions_augmenter_spec.cr +++ b/spec/compiler/semantic/restrictions_augmenter_spec.cr @@ -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 @@ -110,7 +112,9 @@ describe "Semantic: restrictions augmenter" do class Baz end end + @x : Bar::Baz + def initialize(value : Bar::Baz) @x = value end @@ -400,8 +404,10 @@ describe "Semantic: restrictions augmenter" do macro foo {{ yield }} end + class Foo end + class Bar @x : Foo def initialize(value : ::Foo) diff --git a/src/compiler/crystal/syntax/parser.cr b/src/compiler/crystal/syntax/parser.cr index 60a3ec6414a7..7cae1e1f4f91 100644 --- a/src/compiler/crystal/syntax/parser.cr +++ b/src/compiler/crystal/syntax/parser.cr @@ -3224,7 +3224,8 @@ module Crystal when .macro_literal? pieces << MacroLiteral.new(@token.value.to_s).at(@token.location).at_end(token_end_location) when .macro_expression_start? - pieces << MacroExpression.new(parse_macro_expression).at(@token.location).at_end(token_end_location) + start_loc = @token.location + pieces << MacroExpression.new(parse_macro_expression).at(start_loc).at_end(token_end_location) check_macro_expression_end skip_whitespace = check_macro_skip_whitespace when .macro_control_start? diff --git a/src/compiler/crystal/syntax/to_s.cr b/src/compiler/crystal/syntax/to_s.cr index 4ce9ca7efc43..e792cb73a2cf 100644 --- a/src/compiler/crystal/syntax/to_s.cr +++ b/src/compiler/crystal/syntax/to_s.cr @@ -17,6 +17,7 @@ module Crystal @str : IO @macro_expansion_pragmas : Hash(Int32, Array(Lexer::LocPragma))? @current_arg_type : DefArgType = :none + @write_trailing_newline : Bool = true # Inside a comma-separated list of parameters or args, this becomes true and # the outermost pair of parentheses are removed from type restrictions that @@ -55,6 +56,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 @@ -221,11 +237,22 @@ 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 + + if !@write_trailing_newline && i == node.expressions.size - 1 + # no-op + else + newline unless node.keyword.paren? && i == node.expressions.size - 1 + end + end + + last_node = exp end end end @@ -717,8 +744,10 @@ module Crystal end newline - inside_macro do - accept node.body + with_indent do + inside_macro do + accept node.body + end end # newline @@ -728,13 +757,47 @@ module Crystal end def visit(node : MacroExpression) - @str << (node.output? ? "{{" : "{% ") - @str << ' ' if node.output? + # A node starts multiline when its starting location (`{{` or `{%`) is on a different line than the start of its expression + start_multiline = (start_loc = node.location) && (end_loc = node.exp.location) && end_loc.line_number > start_loc.line_number + + # and similarly ends multiline if its expression end location is on a different line than its end location (`}}` or `%}`) + end_multiline = (body_end_loc = node.exp.end_location) && (end_loc = node.end_location) && end_loc.line_number > body_end_loc.line_number + + @str << (node.output? ? "{{ " : start_multiline ? "{%" : "{% ") + + if start_multiline + newline + @indent += 1 + end + outside_macro do - node.exp.accept self + self.write_extra_newlines(node.location, node.exp.location) do + # If the MacroExpression consists of a single node we need to manually handle appending indent and trailing newline if *start_multiline* + # Otherwise, the Expressions logic handles that for us + if start_multiline && !node.exp.is_a?(Expressions) + append_indent + end + + # Only skip writing trailing newlines when the macro expressions' expression is not an Expressions. + # This allow Expressions that may be nested deeper in the AST to include trailing newlines. + @write_trailing_newline = !node.exp.is_a?(Expressions) + node.exp.accept self + @write_trailing_newline = true + end end - @str << ' ' if node.output? - @str << (node.output? ? "}}" : " %}") + + self.write_extra_newlines(node.exp.end_location, node.end_location) { } + + # After writing the expression body, de-indent if things were originally multiline. + # This ensures the ending control has the proper indent relative to the start. + @indent -= 1 if start_multiline + + if end_multiline + newline + append_indent + end + + @str << (node.output? ? " }}" : end_multiline ? "%}" : " %}") false end @@ -790,9 +853,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