Skip to content

Commit 453e84d

Browse files
committed
Treat whitespace in MacroExpressions as significant
1 parent 5545bca commit 453e84d

File tree

5 files changed

+127
-13
lines changed

5 files changed

+127
-13
lines changed

spec/compiler/parser/parser_spec.cr

+81-2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ private def node_source(string, node)
4040
source_between(string, node.location, node.end_location)
4141
end
4242

43+
private def assert_location(node : ASTNode, line_number : Int32, column_number : Int32)
44+
location = node.location.should_not be_nil
45+
location.line_number.should eq line_number
46+
location.column_number.should eq column_number
47+
end
48+
4349
private def assert_end_location(source, line_number = 1, column_number = source.size, file = __FILE__, line = __LINE__)
4450
it "gets corrects end location for #{source.inspect}", file, line do
4551
string = "#{source}; 1"
@@ -1118,7 +1124,7 @@ module Crystal
11181124
it_parses "puts {{**1}}", Call.new(nil, "puts", MacroExpression.new(DoubleSplat.new(1.int32)))
11191125
it_parses "{{a = 1 if 2}}", MacroExpression.new(If.new(2.int32, Assign.new("a".var, 1.int32)))
11201126
it_parses "{% a = 1 %}", MacroExpression.new(Assign.new("a".var, 1.int32), output: false)
1121-
it_parses "{%\na = 1\n%}", MacroExpression.new(Assign.new("a".var, 1.int32), output: false)
1127+
it_parses "{%\na = 1\n%}", MacroExpression.new(Assign.new("a".var, 1.int32), output: false, multiline: true)
11221128
it_parses "{% a = 1 if 2 %}", MacroExpression.new(If.new(2.int32, Assign.new("a".var, 1.int32)), output: false)
11231129
it_parses "{% if 1; 2; end %}", MacroExpression.new(If.new(1.int32, 2.int32), output: false)
11241130
it_parses "{%\nif 1; 2; end\n%}", MacroExpression.new(If.new(1.int32, 2.int32), output: false)
@@ -1128,7 +1134,7 @@ module Crystal
11281134
it_parses "{% unless 1; 2; else 3; end %}", MacroExpression.new(Unless.new(1.int32, 2.int32, 3.int32), output: false)
11291135
it_parses "{% unless 1\n x\nend %}", MacroExpression.new(Unless.new(1.int32, "x".var), output: false)
11301136
it_parses "{% x unless 1 %}", MacroExpression.new(Unless.new(1.int32, "x".var), output: false)
1131-
it_parses "{%\n1\n2\n3\n%}", MacroExpression.new(Expressions.new([1.int32, 2.int32, 3.int32] of ASTNode), output: false)
1137+
it_parses "{%\n1\n2\n3\n%}", MacroExpression.new(Expressions.new([1.int32, 2.int32, 3.int32] of ASTNode), output: false, multiline: true)
11321138

11331139
assert_syntax_error "{% unless 1; 2; elsif 3; 4; end %}"
11341140
assert_syntax_error "{% unless 1 %} 2 {% elsif 3 %} 3 {% end %}"
@@ -2772,6 +2778,79 @@ module Crystal
27722778
else_node_location.line_number.should eq 7
27732779
end
27742780

2781+
it "sets the correct location for MacroExpressions in a MacroVerbatim in a finished hook with significant whitespace" do
2782+
parser = Parser.new(<<-CR)
2783+
macro finished
2784+
{% verbatim do %}
2785+
{%
2786+
10
2787+
2788+
# Foo
2789+
2790+
20
2791+
30
2792+
2793+
# Bar
2794+
2795+
40
2796+
%}
2797+
2798+
{%
2799+
50
2800+
60
2801+
%}
2802+
{% end %}
2803+
end
2804+
CR
2805+
2806+
node = parser.parse.should be_a Macro
2807+
2808+
assert_location node, 1, 1
2809+
2810+
macro_body = node.body.should be_a Expressions
2811+
verbatim_node = macro_body[1].should be_a MacroVerbatim
2812+
2813+
expressions = verbatim_node.exp.as(Expressions).expressions
2814+
expressions.size.should eq 5
2815+
2816+
expressions[0].should eq MacroLiteral.new("\n ")
2817+
expression = expressions[1].should be_a MacroExpression
2818+
2819+
macro_expression = expression.exp.as(Expressions).expressions
2820+
macro_expression.size.should eq 10
2821+
macro_expression.select(MacroLiteral).size.should eq 6
2822+
2823+
num = macro_expression[0].should be_a NumberLiteral
2824+
num.value.should eq "10"
2825+
assert_location num, 4, 7
2826+
2827+
num = macro_expression[4].should be_a NumberLiteral
2828+
num.value.should eq "20"
2829+
assert_location num, 8, 7
2830+
2831+
num = macro_expression[5].should be_a NumberLiteral
2832+
num.value.should eq "30"
2833+
assert_location num, 9, 7
2834+
2835+
num = macro_expression[9].should be_a NumberLiteral
2836+
num.value.should eq "40"
2837+
assert_location num, 13, 7
2838+
2839+
expression = expressions[3].should be_a MacroExpression
2840+
2841+
macro_expression = expression.exp.as(Expressions).expressions
2842+
macro_expression.size.should eq 2
2843+
macro_expression.select(MacroLiteral).size.should eq 0
2844+
2845+
num = macro_expression[0].should be_a NumberLiteral
2846+
num.value.should eq "50"
2847+
assert_location num, 17, 7
2848+
2849+
num = macro_expression[1].should be_a NumberLiteral
2850+
num.value.should eq "60"
2851+
assert_location num, 18, 7
2852+
end
2853+
27752854
it "sets correct location of Begin within another node" do
27762855
parser = Parser.new(<<-CR)
27772856
macro finished

src/compiler/crystal/syntax/ast.cr

+4-3
Original file line numberDiff line numberDiff line change
@@ -2192,19 +2192,20 @@ module Crystal
21922192
class MacroExpression < ASTNode
21932193
property exp : ASTNode
21942194
property? output : Bool
2195+
property? multiline : Bool
21952196

2196-
def initialize(@exp : ASTNode, @output = true)
2197+
def initialize(@exp : ASTNode, @output = true, @multiline = false)
21972198
end
21982199

21992200
def accept_children(visitor)
22002201
@exp.accept visitor
22012202
end
22022203

22032204
def clone_without_location
2204-
MacroExpression.new(@exp.clone, @output)
2205+
MacroExpression.new(@exp.clone, @output, @multiline)
22052206
end
22062207

2207-
def_equals_and_hash exp, output?
2208+
def_equals_and_hash exp, output?, multiline?
22082209
end
22092210

22102211
# Free text that is part of a macro

src/compiler/crystal/syntax/lexer.cr

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module Crystal
77
class Lexer
88
property? doc_enabled : Bool
99
property? comments_enabled : Bool
10+
property? comments_as_newlines : Bool
1011
property? count_whitespace : Bool
1112
property? wants_raw : Bool
1213
property? slash_is_regex : Bool
@@ -69,6 +70,7 @@ module Crystal
6970
@doc_enabled = false
7071
@comment_is_doc = true
7172
@comments_enabled = false
73+
@comments_as_newlines = false
7274
@count_whitespace = false
7375
@slash_is_regex = true
7476
@wants_raw = false
@@ -125,6 +127,12 @@ module Crystal
125127
return consume_comment(start)
126128
else
127129
skip_comment
130+
131+
if @comments_as_newlines
132+
@token.type = :newline
133+
@token.value = "\n"
134+
return @token
135+
end
128136
end
129137
end
130138
end

src/compiler/crystal/syntax/parser.cr

+33-8
Original file line numberDiff line numberDiff line change
@@ -110,26 +110,43 @@ module Crystal
110110
end
111111

112112
exp = parse_multi_assign
113+
exps = [] of ASTNode
114+
newlines = [] of ASTNode
115+
exps.push exp
113116

114117
slash_is_regex!
115-
skip_statement_end
118+
collect_significant_newlines.tap do |newlines|
119+
exps.concat newlines if newlines.size > 1
120+
end
116121

117122
if end_token?
118123
return exp
119124
end
120125

121-
exps = [] of ASTNode
122-
exps.push exp
123-
124126
loop do
125127
exps << parse_multi_assign
126-
skip_statement_end
128+
collect_significant_newlines.tap do |newlines|
129+
exps.concat newlines if newlines.size > 1
130+
end
131+
127132
break if end_token?
128133
end
129134

130135
Expressions.from(exps)
131136
end
132137

138+
# Replicates what `#skip_statement_end` does, but collects the newlines if within a macro expression
139+
private def collect_significant_newlines : Array(ASTNode)
140+
newlines = [] of ASTNode
141+
142+
while (@token.type.space? || @token.type.newline? || @token.type.op_semicolon?)
143+
newlines << MacroLiteral.new("") if @token.type.newline? && @in_macro_expression
144+
next_token
145+
end
146+
147+
newlines
148+
end
149+
133150
def parse_multi_assign
134151
location = @token.location
135152

@@ -3353,7 +3370,13 @@ module Crystal
33533370

33543371
def parse_macro_control(start_location, macro_state = Token::MacroState.default)
33553372
location = @token.location
3356-
next_token_skip_space_or_newline
3373+
next_token_skip_space
3374+
multiline = false
3375+
3376+
if @token.type.newline?
3377+
multiline = true
3378+
next_token_skip_space_or_newline
3379+
end
33573380

33583381
case @token.value
33593382
when Keyword::FOR
@@ -3431,16 +3454,18 @@ module Crystal
34313454
next_token_skip_space
34323455
check :OP_PERCENT_RCURLY
34333456

3434-
return MacroVerbatim.new(body).at_end(token_end_location)
3457+
return MacroVerbatim.new(body).at(location).at_end(token_end_location)
34353458
else
34363459
# will be parsed as a normal expression
34373460
end
34383461

34393462
@in_macro_expression = true
3463+
@comments_as_newlines = true
34403464
exps = parse_expressions
34413465
@in_macro_expression = false
3466+
@comments_as_newlines = false
34423467

3443-
MacroExpression.new(exps, output: false).at(location).at_end(token_end_location)
3468+
MacroExpression.new(exps, output: false, multiline: multiline).at(location).at_end(token_end_location)
34443469
end
34453470

34463471
def parse_macro_if(start_location, macro_state, check_end = true, is_unless = false)

src/compiler/crystal/syntax/to_s.cr

+1
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,7 @@ module Crystal
730730
def visit(node : MacroExpression)
731731
@str << (node.output? ? "{{" : "{% ")
732732
@str << ' ' if node.output?
733+
newline if node.multiline?
733734
outside_macro do
734735
node.exp.accept self
735736
end

0 commit comments

Comments
 (0)