Skip to content

Commit 8eb8554

Browse files
asteriteRX14
authored andcommittedJan 17, 2018
Correct implementation of heredoc (#5578)
Now you can specify multiple heredocs in a single line, just like in Ruby.
1 parent 295ddc3 commit 8eb8554

File tree

9 files changed

+224
-96
lines changed

9 files changed

+224
-96
lines changed
 

‎spec/compiler/formatter/formatter_spec.cr

+3-3
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,9 @@ describe Crystal::Formatter do
881881
assert_format "<<-HTML\n hello \n world \n HTML"
882882
assert_format " <<-HTML\n hello \n world \n HTML", "<<-HTML\n hello \n world \n HTML"
883883

884+
assert_format "x, y = <<-FOO, <<-BAR\n hello\n FOO\n world\n BAR"
885+
assert_format "x, y, z = <<-FOO, <<-BAR, <<-BAZ\n hello\n FOO\n world\n BAR\n qux\nBAZ"
886+
884887
assert_format "#!shebang\n1 + 2"
885888

886889
assert_format " {{\n1 + 2 }}", "{{\n 1 + 2\n}}"
@@ -1029,9 +1032,6 @@ describe Crystal::Formatter do
10291032

10301033
assert_format "lib Foo\n {% if 1 %}\n 2\n {% end %}\nend\n\nmacro bar\n 1\nend"
10311034

1032-
assert_format %(puts(<<-FOO\n1\nFOO, 2))
1033-
assert_format %(puts <<-FOO\n1\nFOO, 2)
1034-
10351035
assert_format "x : Int32 |\nString", "x : Int32 |\n String"
10361036

10371037
assert_format %(foo("bar" \\\n"baz")), %(foo("bar" \\\n "baz"))

‎spec/compiler/lexer/lexer_string_spec.cr

+8-34
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ describe "Lexer string" do
161161
tester = LexerObjects::Strings.new(lexer)
162162

163163
tester.string_should_start_correctly
164+
tester.next_token_should_be(:NEWLINE)
164165
tester.next_string_token_should_be("Hello, mom! I am HERE.")
165166
tester.next_string_token_should_be("\nHER dress is beautiful.")
166167
tester.next_string_token_should_be("\nHE is OK.")
@@ -173,6 +174,7 @@ describe "Lexer string" do
173174
tester = LexerObjects::Strings.new(lexer)
174175

175176
tester.string_should_start_correctly
177+
tester.next_token_should_be(:NEWLINE)
176178
tester.next_string_token_should_be("foo")
177179
tester.next_string_token_should_be("\n")
178180
tester.string_should_end_correctly
@@ -183,6 +185,7 @@ describe "Lexer string" do
183185
tester = LexerObjects::Strings.new(lexer)
184186

185187
tester.string_should_start_correctly
188+
tester.next_token_should_be(:NEWLINE)
186189
tester.next_string_token_should_be("foo")
187190
tester.next_string_token_should_be("\r\n")
188191
tester.string_should_end_correctly
@@ -193,6 +196,7 @@ describe "Lexer string" do
193196
tester = LexerObjects::Strings.new(lexer)
194197

195198
tester.string_should_start_correctly
199+
tester.next_token_should_be(:NEWLINE)
196200
tester.next_string_token_should_be("foo")
197201
tester.string_should_end_correctly
198202
end
@@ -203,6 +207,7 @@ describe "Lexer string" do
203207
tester = LexerObjects::Strings.new(lexer)
204208

205209
tester.string_should_start_correctly
210+
tester.next_token_should_be(:NEWLINE)
206211
tester.next_string_token_should_be("Hello, mom! I am HERE.")
207212
tester.token_should_be_at(line: 2)
208213
tester.next_string_token_should_be("\nHER dress is beautiful.")
@@ -221,6 +226,7 @@ describe "Lexer string" do
221226
tester = LexerObjects::Strings.new(lexer)
222227

223228
tester.string_should_start_correctly
229+
tester.next_token_should_be(:NEWLINE)
224230
tester.next_string_token_should_be("abc")
225231
tester.string_should_have_an_interpolation_of("foo")
226232
tester.string_should_end_correctly
@@ -239,46 +245,14 @@ describe "Lexer string" do
239245
end
240246
end
241247

242-
it "raises on invalid heredoc identifier (<<-HERE A)" do
243-
lexer = Lexer.new("<<-HERE A\ntest\nHERE\n")
244-
245-
expect_raises Crystal::SyntaxException, /invalid character '.+' for heredoc identifier/ do
246-
lexer.next_token
247-
end
248-
end
249-
250-
it "raises on invalid heredoc identifier (<<-HERE\\n)" do
251-
lexer = Lexer.new("<<-HERE\\ntest\nHERE\n")
252-
253-
expect_raises Crystal::SyntaxException, /invalid character '.+' for heredoc identifier/ do
254-
lexer.next_token
255-
end
256-
end
257-
258-
it "raises when identifier doesn't start with a leter" do
259-
lexer = Lexer.new("<<-123\\ntest\n123\n")
248+
it "raises when identifier doesn't start with a leter or number" do
249+
lexer = Lexer.new("<<-!!!\\ntest\n!!!\n")
260250

261251
expect_raises Crystal::SyntaxException, /heredoc identifier starts with invalid character/ do
262252
lexer.next_token
263253
end
264254
end
265255

266-
it "raises when identifier contains a character not for identifier" do
267-
lexer = Lexer.new("<<-aaa.bbb?\\ntest\naaa.bbb?\n")
268-
269-
expect_raises Crystal::SyntaxException, /invalid character '.+' for heredoc identifier/ do
270-
lexer.next_token
271-
end
272-
end
273-
274-
it "raises when identifier contains spaces" do
275-
lexer = Lexer.new("<<-aaa bbb\\ntest\naaabbb\n")
276-
277-
expect_raises Crystal::SyntaxException, /invalid character '.+' for heredoc identifier/ do
278-
lexer.next_token
279-
end
280-
end
281-
282256
it "raises on unexpected EOF while lexing heredoc" do
283257
lexer = Lexer.new("<<-aaa")
284258

‎spec/compiler/normalize/string_interpolation_spec.cr

+4
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ describe "Normalize: string interpolation" do
1010
assert_expand "\"foo\#{bar}#{s}\"",
1111
"((((::String::Builder.new(218)) << \"foo\") << bar) << \"#{s}\").to_s"
1212
end
13+
14+
it "normalizes heredoc" do
15+
assert_normalize "<<-FOO\nhello\nFOO", %("hello")
16+
end
1317
end

‎spec/compiler/parser/parser_spec.cr

+24-15
Original file line numberDiff line numberDiff line change
@@ -1180,16 +1180,17 @@ describe "Parser" do
11801180
it_parses %("hello " \\\n "world"), StringLiteral.new("hello world")
11811181
it_parses %("hello "\\\n"world"), StringLiteral.new("hello world")
11821182
it_parses %("hello \#{1}" \\\n "\#{2} world"), StringInterpolation.new(["hello ".string, 1.int32, 2.int32, " world".string] of ASTNode)
1183-
it_parses "<<-HERE\nHello, mom! I am HERE.\nHER dress is beautiful.\nHE is OK.\n HERESY\nHERE", "Hello, mom! I am HERE.\nHER dress is beautiful.\nHE is OK.\n HERESY".string
1184-
it_parses "<<-HERE\n One\n Zero\n HERE", " One\nZero".string
1185-
it_parses "<<-HERE\n One \\n Two\n Zero\n HERE", " One \n Two\nZero".string
1186-
it_parses "<<-HERE\n One\n\n Zero\n HERE", " One\n\nZero".string
1187-
it_parses "<<-HERE\n One\n \n Zero\n HERE", " One\n\nZero".string
1183+
it_parses "<<-HERE\nHello, mom! I am HERE.\nHER dress is beautiful.\nHE is OK.\n HERESY\nHERE",
1184+
"Hello, mom! I am HERE.\nHER dress is beautiful.\nHE is OK.\n HERESY".string_interpolation
1185+
it_parses "<<-HERE\n One\n Zero\n HERE", " One\nZero".string_interpolation
1186+
it_parses "<<-HERE\n One \\n Two\n Zero\n HERE", " One \n Two\nZero".string_interpolation
1187+
it_parses "<<-HERE\n One\n\n Zero\n HERE", " One\n\nZero".string_interpolation
1188+
it_parses "<<-HERE\n One\n \n Zero\n HERE", " One\n\nZero".string_interpolation
11881189
it_parses "<<-HERE\n \#{1}One\n \#{2}Zero\n HERE", StringInterpolation.new([" ".string, 1.int32, "One\n".string, 2.int32, "Zero".string] of ASTNode)
11891190
it_parses "<<-HERE\n foo\#{1}bar\n baz\n HERE", StringInterpolation.new(["foo".string, 1.int32, "bar\n baz".string] of ASTNode)
1190-
it_parses "<<-HERE\r\n One\r\n Zero\r\n HERE", " One\r\nZero".string
1191-
it_parses "<<-HERE\r\n One\r\n Zero\r\n HERE\r\n", " One\r\nZero".string
1192-
it_parses "<<-SOME\n Sa\n Se\n SOME", "Sa\nSe".string
1191+
it_parses "<<-HERE\r\n One\r\n Zero\r\n HERE", " One\r\nZero".string_interpolation
1192+
it_parses "<<-HERE\r\n One\r\n Zero\r\n HERE\r\n", " One\r\nZero".string_interpolation
1193+
it_parses "<<-SOME\n Sa\n Se\n SOME", "Sa\nSe".string_interpolation
11931194
it_parses "<<-HERE\n \#{1} \#{2}\n HERE", StringInterpolation.new([1.int32, " ".string, 2.int32] of ASTNode)
11941195
it_parses "<<-HERE\n \#{1} \\n \#{2}\n HERE", StringInterpolation.new([1.int32, " \n ".string, 2.int32] of ASTNode)
11951196
assert_syntax_error "<<-HERE\n One\nwrong\n Zero\n HERE", "heredoc line must have an indent greater or equal than 2", 3, 1
@@ -1200,16 +1201,24 @@ describe "Parser" do
12001201
assert_syntax_error "<<-HERE\n One\n \#{1}\n HERE", "heredoc line must have an indent greater or equal than 2", 2, 1
12011202
assert_syntax_error %("foo" "bar")
12021203

1203-
it_parses "<<-'HERE'\n hello \\n world\n \#{1}\n HERE", StringLiteral.new("hello \\n world\n\#{1}")
1204+
it_parses "<<-'HERE'\n hello \\n world\n \#{1}\n HERE", "hello \\n world\n\#{1}".string_interpolation
12041205
assert_syntax_error "<<-'HERE\n", "expecting closing single quote"
12051206

1206-
it_parses "<<-FOO\n1\nFOO.bar", Call.new("1".string, "bar")
1207-
it_parses "<<-FOO\n1\nFOO + 2", Call.new("1".string, "+", 2.int32)
1207+
it_parses "<<-'HERE COMES HEREDOC'\n hello \\n world\n \#{1}\n HERE COMES HEREDOC", "hello \\n world\n\#{1}".string_interpolation
12081208

1209-
it_parses "<<-FOO\n\t1\n\tFOO", StringLiteral.new("1")
1210-
it_parses "<<-FOO\n \t1\n \tFOO", StringLiteral.new("1")
1211-
it_parses "<<-FOO\n \t 1\n \t FOO", StringLiteral.new("1")
1212-
it_parses "<<-FOO\n\t 1\n\t FOO", StringLiteral.new("1")
1209+
assert_syntax_error "<<-FOO\n1\nFOO.bar", "Unterminated heredoc: can't find \"FOO\" anywhere before the end of file"
1210+
assert_syntax_error "<<-FOO\n1\nFOO + 2", "Unterminated heredoc: can't find \"FOO\" anywhere before the end of file"
1211+
1212+
it_parses "<<-FOO\n\t1\n\tFOO", "1".string_interpolation
1213+
it_parses "<<-FOO\n \t1\n \tFOO", "1".string_interpolation
1214+
it_parses "<<-FOO\n \t 1\n \t FOO", "1".string_interpolation
1215+
it_parses "<<-FOO\n\t 1\n\t FOO", "1".string_interpolation
1216+
1217+
it_parses "x, y = <<-FOO, <<-BAR\nhello\nFOO\nworld\nBAR",
1218+
MultiAssign.new(["x".var, "y".var] of ASTNode, ["hello".string_interpolation, "world".string_interpolation] of ASTNode)
1219+
1220+
it_parses "x, y, z = <<-FOO, <<-BAR, <<-BAZ\nhello\nFOO\nworld\nBAR\n!\nBAZ",
1221+
MultiAssign.new(["x".var, "y".var, "z".var] of ASTNode, ["hello".string_interpolation, "world".string_interpolation, "!".string_interpolation] of ASTNode)
12131222

12141223
it_parses "enum Foo; A\nB, C\nD = 1; end", EnumDef.new("Foo".path, [Arg.new("A"), Arg.new("B"), Arg.new("C"), Arg.new("D", 1.int32)] of ASTNode)
12151224
it_parses "enum Foo; A = 1, B; end", EnumDef.new("Foo".path, [Arg.new("A", 1.int32), Arg.new("B")] of ASTNode)

‎spec/support/syntax.cr

+4
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ class String
9090
StringLiteral.new self
9191
end
9292

93+
def string_interpolation
94+
StringInterpolation.new([self.string] of ASTNode)
95+
end
96+
9397
def float32
9498
NumberLiteral.new self, :f32
9599
end

‎src/compiler/crystal/semantic/normalizer.cr

+11
Original file line numberDiff line numberDiff line change
@@ -375,5 +375,16 @@ module Crystal
375375
Assign.new(target.clone, call).at(node)
376376
end
377377
end
378+
379+
def transform(node : StringInterpolation)
380+
# If the interpolation has just one string literal inside it,
381+
# return that instead of an interpolation
382+
if node.expressions.size == 1
383+
first = node.expressions.first
384+
return first if first.is_a?(StringLiteral)
385+
end
386+
387+
super
388+
end
378389
end
379390
end

‎src/compiler/crystal/syntax/lexer.cr

+41-17
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ module Crystal
1818
@token_end_location : Location?
1919
@string_pool : StringPool
2020

21+
# This is an interface for storing data associated to a heredoc
22+
module HeredocItem
23+
end
24+
25+
# Heredocs pushed when found. Should be processed when encountering a newline
26+
getter heredocs = [] of {Token::DelimiterState, HeredocItem}
27+
2128
def initialize(string, string_pool : StringPool? = nil)
2229
@reader = Char::Reader.new(string)
2330
@token = Token.new
@@ -158,30 +165,36 @@ module Crystal
158165
found_closing_single_quote = false
159166

160167
char = next_char
168+
start_here = current_pos
169+
161170
if char == '\''
162171
has_single_quote = true
163172
char = next_char
173+
start_here = current_pos
164174
end
165175

166-
unless ident_start?(char)
176+
unless ident_part?(char)
167177
raise "heredoc identifier starts with invalid character"
168178
end
169179

170-
here << char
180+
end_here = 0
181+
171182
while true
172183
char = next_char
173184
case
174185
when char == '\r'
175186
if peek_next_char == '\n'
176-
next
187+
end_here = current_pos
188+
next_char
189+
break
177190
else
178191
raise "expecting '\\n' after '\\r'"
179192
end
180193
when char == '\n'
181-
incr_line_number 0
194+
end_here = current_pos
182195
break
183196
when ident_part?(char)
184-
here << char
197+
# ok
185198
when char == '\0'
186199
raise "Unexpected EOF on heredoc identifier"
187200
else
@@ -191,8 +204,11 @@ module Crystal
191204
if peek != '\r' && peek != '\n'
192205
raise "expecting '\\n' or '\\r' after closing single quote"
193206
end
207+
elsif has_single_quote
208+
# wait until another quote
194209
else
195-
raise "invalid character #{char.inspect} for heredoc identifier"
210+
end_here = current_pos
211+
break
196212
end
197213
end
198214
end
@@ -201,8 +217,11 @@ module Crystal
201217
raise "expecting closing single quote"
202218
end
203219

204-
here = here.to_s
205-
delimited_pair :heredoc, here, here, start, allow_escapes: !has_single_quote
220+
end_here -= 1 if has_single_quote
221+
222+
here = string_range(start_here, end_here)
223+
224+
delimited_pair :heredoc, here, here, start, allow_escapes: !has_single_quote, advance: false
206225
else
207226
@token.type = :"<<"
208227
end
@@ -1176,6 +1195,10 @@ module Crystal
11761195
end
11771196

11781197
def consume_newlines
1198+
# If there are heredocs we don't freely consume newlines because
1199+
# these will be part of the heredoc string
1200+
return unless @heredocs.empty?
1201+
11791202
if @count_whitespace
11801203
return
11811204
end
@@ -1721,6 +1744,7 @@ module Crystal
17211744

17221745
def next_string_token(delimiter_state)
17231746
@token.line_number = @line_number
1747+
@token.delimiter_state = delimiter_state
17241748

17251749
start = current_pos
17261750
string_end = delimiter_state.end
@@ -1737,13 +1761,13 @@ module Crystal
17371761
else
17381762
@token.type = :STRING
17391763
@token.value = string_end.to_s
1740-
@token.delimiter_state = @token.delimiter_state.with_open_count_delta(-1)
1764+
@token.delimiter_state = delimiter_state.with_open_count_delta(-1)
17411765
end
17421766
when string_nest
17431767
next_char
17441768
@token.type = :STRING
17451769
@token.value = string_nest.to_s
1746-
@token.delimiter_state = @token.delimiter_state.with_open_count_delta(+1)
1770+
@token.delimiter_state = delimiter_state.with_open_count_delta(+1)
17471771
when '\\'
17481772
if delimiter_state.allow_escapes
17491773
if delimiter_state.kind == :regex
@@ -1877,10 +1901,9 @@ module Crystal
18771901

18781902
if reached_end &&
18791903
(current_char == '\n' || current_char == '\0' ||
1880-
(current_char == '\r' && peek_next_char == '\n' && next_char) ||
1881-
!ident_part?(current_char))
1904+
(current_char == '\r' && peek_next_char == '\n' && next_char))
18821905
@token.type = :DELIMITER_END
1883-
@token.delimiter_state = @token.delimiter_state.with_heredoc_indent(indent)
1906+
@token.delimiter_state = delimiter_state.with_heredoc_indent(indent)
18841907
else
18851908
@reader.pos = old_pos
18861909
@column_number = old_column
@@ -1923,8 +1946,9 @@ module Crystal
19231946
msg = case delimiter_state.kind
19241947
when :command then "Unterminated command literal"
19251948
when :regex then "Unterminated regular expression"
1926-
when :heredoc then "Unterminated heredoc"
1927-
when :string then "Unterminated string literal"
1949+
when :heredoc
1950+
"Unterminated heredoc: can't find \"#{delimiter_state.end}\" anywhere before the end of file"
1951+
when :string then "Unterminated string literal"
19281952
else
19291953
::raise "unreachable"
19301954
end
@@ -2409,8 +2433,8 @@ module Crystal
24092433
@token.value = value
24102434
end
24112435

2412-
def delimited_pair(kind, string_nest, string_end, start, allow_escapes = true)
2413-
next_char
2436+
def delimited_pair(kind, string_nest, string_end, start, allow_escapes = true, advance = true)
2437+
next_char if advance
24142438
@token.type = :DELIMITER_START
24152439
@token.delimiter_state = Token::DelimiterState.new(kind, string_nest, string_end, allow_escapes)
24162440
set_token_raw_from_start(start)

‎src/compiler/crystal/syntax/parser.cr

+74-14
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ module Crystal
3636
@wants_doc = false
3737
@doc_enabled = false
3838
@no_type_declaration = 0
39+
@consuming_heredocs = false
3940

4041
# This flags tells the parser where it has to consider a "do"
4142
# as belonging to the current parsed call. For example when writing
@@ -1790,6 +1791,13 @@ module Crystal
17901791

17911792
check :DELIMITER_START
17921793

1794+
if delimiter_state.kind == :heredoc
1795+
node = StringInterpolation.new([] of ASTNode).at(location)
1796+
@heredocs << {delimiter_state, node}
1797+
next_token
1798+
return node
1799+
end
1800+
17931801
next_string_token(delimiter_state)
17941802
delimiter_state = @token.delimiter_state
17951803

@@ -1814,22 +1822,10 @@ module Crystal
18141822
end
18151823

18161824
if has_interpolation
1817-
if needs_heredoc_indent_removed?(delimiter_state)
1818-
pieces = remove_heredoc_indent(pieces, delimiter_state.heredoc_indent)
1819-
else
1820-
pieces = pieces.map do |piece|
1821-
value = piece.value
1822-
value.is_a?(String) ? StringLiteral.new(value) : value
1823-
end
1824-
end
1825+
pieces = combine_interpolation_pieces(pieces, delimiter_state)
18251826
result = StringInterpolation.new(pieces).at(location)
18261827
else
1827-
if needs_heredoc_indent_removed?(delimiter_state)
1828-
pieces = remove_heredoc_indent(pieces, delimiter_state.heredoc_indent)
1829-
string = pieces.join { |piece| piece.as(StringLiteral).value }
1830-
else
1831-
string = pieces.map(&.value).join
1832-
end
1828+
string = combine_pieces(pieces, delimiter_state)
18331829
result = StringLiteral.new string
18341830
end
18351831

@@ -1849,6 +1845,26 @@ module Crystal
18491845
result
18501846
end
18511847

1848+
private def combine_interpolation_pieces(pieces, delimiter_state)
1849+
if needs_heredoc_indent_removed?(delimiter_state)
1850+
remove_heredoc_indent(pieces, delimiter_state.heredoc_indent)
1851+
else
1852+
pieces.map do |piece|
1853+
value = piece.value
1854+
value.is_a?(String) ? StringLiteral.new(value) : value
1855+
end
1856+
end
1857+
end
1858+
1859+
private def combine_pieces(pieces, delimiter_state)
1860+
if needs_heredoc_indent_removed?(delimiter_state)
1861+
pieces = remove_heredoc_indent(pieces, delimiter_state.heredoc_indent)
1862+
pieces.join { |piece| piece.as(StringLiteral).value }
1863+
else
1864+
pieces.map(&.value).join
1865+
end
1866+
end
1867+
18521868
def consume_delimiter(pieces, delimiter_state, has_interpolation)
18531869
options = Regex::Options::None
18541870
token_end_location = nil
@@ -1927,6 +1943,35 @@ module Crystal
19271943
options
19281944
end
19291945

1946+
def consume_heredocs
1947+
@consuming_heredocs = true
1948+
@heredocs.reverse!
1949+
while heredoc = @heredocs.pop?
1950+
consume_heredoc(heredoc[0], heredoc[1].as(StringInterpolation))
1951+
end
1952+
@consuming_heredocs = false
1953+
end
1954+
1955+
def consume_heredoc(delimiter_state, node)
1956+
next_string_token(delimiter_state)
1957+
delimiter_state = @token.delimiter_state
1958+
1959+
pieces = [] of Piece
1960+
has_interpolation = false
1961+
1962+
delimiter_state, has_interpolation, options, token_end_location = consume_delimiter pieces, delimiter_state, has_interpolation
1963+
1964+
if has_interpolation
1965+
pieces = combine_interpolation_pieces(pieces, delimiter_state)
1966+
node.expressions.concat(pieces)
1967+
else
1968+
string = combine_pieces(pieces, delimiter_state)
1969+
node.expressions.push(StringLiteral.new(string).at(node.location).at_end(token_end_location))
1970+
end
1971+
1972+
node.end_location = token_end_location
1973+
end
1974+
19301975
def needs_heredoc_indent_removed?(delimiter_state)
19311976
delimiter_state.kind == :heredoc && delimiter_state.heredoc_indent > 0
19321977
end
@@ -1939,6 +1984,7 @@ module Crystal
19391984
pieces.each_with_index do |piece, i|
19401985
value = piece.value
19411986
line_number = piece.line_number
1987+
19421988
this_piece_is_in_new_line = line_number != previous_line_number
19431989
next_piece_is_in_new_line = i == pieces.size - 1 || pieces[i + 1].line_number != line_number
19441990
if value.is_a?(String)
@@ -5537,5 +5583,19 @@ module Crystal
55375583
@visibility = old_visibility
55385584
value
55395585
end
5586+
5587+
def next_token
5588+
token = super
5589+
5590+
if token.type == :NEWLINE && !@consuming_heredocs && !@heredocs.empty?
5591+
consume_heredocs
5592+
end
5593+
5594+
token
5595+
end
5596+
end
5597+
5598+
class StringInterpolation
5599+
include Lexer::HeredocItem
55405600
end
55415601
end

‎src/compiler/crystal/tools/formatter.cr

+55-13
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ module Crystal
4545
end_line : Int32,
4646
difference : Int32
4747

48+
record HeredocInfo,
49+
node : StringInterpolation,
50+
token : Token,
51+
line : Int32,
52+
column : Int32,
53+
indent : Int32,
54+
string_continuation : Int32 do
55+
include Lexer::HeredocItem
56+
end
57+
4858
@lexer : Lexer
4959
@comment_columns : Array(Int32?)
5060
@indent : Int32
@@ -86,8 +96,7 @@ module Crystal
8696
@indent = 0
8797
@line = 0
8898
@column = 0
89-
@token = @lexer.token
90-
@token = next_token
99+
@token = @lexer.next_token
91100

92101
@output = IO::Memory.new(source.bytesize)
93102
@line_output = IO::Memory.new
@@ -500,19 +509,30 @@ module Crystal
500509
end
501510

502511
def visit(node : StringInterpolation)
512+
if @token.delimiter_state.kind == :heredoc
513+
# For heredoc, only write the start: on a newline will print it
514+
@lexer.heredocs << {@token.delimiter_state, HeredocInfo.new(node, @token.dup, @line, @column, @indent, @string_continuation)}
515+
write @token.raw
516+
next_token
517+
return false
518+
end
519+
503520
check :DELIMITER_START
504521

505-
column = @column
506-
old_indent = @indent
507-
old_string_continuation = @string_continuation
508-
is_regex = @token.delimiter_state.kind == :regex
509-
indent_difference = @token.column_number - (@column + 1)
522+
visit_string_interpolation(node, @token, @line, @column, @indent, @string_continuation)
523+
end
510524

511-
write @token.raw
525+
def visit_string_interpolation(node, token, line, column, old_indent, old_string_continuation, wrote_token = false)
526+
@token = token
527+
528+
is_regex = token.delimiter_state.kind == :regex
529+
indent_difference = token.column_number - (column + 1)
530+
531+
write token.raw unless wrote_token
512532
next_string_token
513533

514-
delimiter_state = @token.delimiter_state
515-
is_heredoc = @token.delimiter_state.kind == :heredoc
534+
delimiter_state = token.delimiter_state
535+
is_heredoc = token.delimiter_state.kind == :heredoc
516536
@last_is_heredoc = is_heredoc
517537

518538
heredoc_line = @line
@@ -615,6 +635,26 @@ module Crystal
615635
end
616636
end
617637

638+
private def consume_heredocs
639+
@consuming_heredocs = true
640+
@lexer.heredocs.reverse!
641+
while heredoc = @lexer.heredocs.pop?
642+
consume_heredoc(heredoc[0], heredoc[1].as(HeredocInfo))
643+
write_line unless @lexer.heredocs.empty?
644+
end
645+
@consuming_heredocs = false
646+
end
647+
648+
private def consume_heredoc(delimiter_state, info)
649+
visit_string_interpolation(
650+
info.node,
651+
info.token,
652+
info.line,
653+
info.column,
654+
info.indent, info.string_continuation,
655+
wrote_token: true)
656+
end
657+
618658
def visit(node : RegexLiteral)
619659
accept node.value
620660

@@ -4012,14 +4052,16 @@ module Crystal
40124052
io << @output
40134053
end
40144054

4015-
def maybe_reset_passed_backslash_newline
4016-
end
4017-
40184055
def next_token
40194056
current_line_number = @lexer.line_number
40204057
@token = @lexer.next_token
40214058
if @token.type == :DELIMITER_START
40224059
increment_lines(@lexer.line_number - current_line_number)
4060+
elsif @token.type == :NEWLINE
4061+
if !@lexer.heredocs.empty? && !@consuming_heredocs
4062+
write_line
4063+
consume_heredocs
4064+
end
40234065
end
40244066
@token
40254067
end

0 commit comments

Comments
 (0)
Please sign in to comment.