Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions spec/compiler/formatter/formatter_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,9 @@ describe Crystal::Formatter do
assert_format "%w{one( two( three)}", "%w{one( two( three)}"
assert_format "%i{one( two( three)}", "%i{one( two( three)}"

assert_format "%w(\n\n)\n# ```\n# 1\n# ```\n", "%w()\n# ```\n# 1\n# ```"
assert_format "%w(a\\ b)"

assert_format "/foo/"
assert_format "/foo/imx"
assert_format "/foo \#{ bar }/", "/foo \#{bar}/"
Expand Down Expand Up @@ -1815,6 +1818,7 @@ describe Crystal::Formatter do
assert_format "1 #=> 2", "1 # => 2"
assert_format "1 #=>2", "1 # => 2"
assert_format "foo(\n [\n 1,\n 2,\n ],\n [\n 3,\n 4,\n ]\n)"
assert_format "begin\n %w(\n one two\n three four\n )\nend"
assert_format "%w(\n one two\n three four\n)"
assert_format "a = %w(\n one two\n three four\n)"
assert_format "foo &.bar do\n 1 + 2\nend"
Expand Down
30 changes: 19 additions & 11 deletions spec/compiler/lexer/lexer_string_array_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@ private def it_should_be_valid_string_array_lexer(lexer)
token = lexer.next_token
token.type.should eq(t :STRING_ARRAY_START)

token = lexer.next_string_array_token
token = lexer.next_string_token(token.delimiter_state)
token.type.should eq(t :STRING)
token.value.should eq("one")

token = lexer.next_string_array_token
token = lexer.next_string_token(token.delimiter_state)
token.type.should eq(t :SPACE)
token.value.should eq(" ")

token = lexer.next_string_token(token.delimiter_state)
token.type.should eq(t :STRING)
token.value.should eq("two")

token = lexer.next_string_array_token
token = lexer.next_string_token(token.delimiter_state)
token.type.should eq(t :STRING_ARRAY_END)
end

describe "Lexer string array" do
describe "Lexer %w string array" do
it "lexes simple string array" do
lexer = Lexer.new("%w(one two)")

Expand All @@ -33,25 +37,29 @@ describe "Lexer string array" do
token = lexer.next_token
token.type.should eq(t :STRING_ARRAY_START)

token = lexer.next_string_array_token
token = lexer.next_string_token(token.delimiter_state)
token.type.should eq(t :STRING)
token.value.should eq("one")

token = lexer.next_string_array_token
token = lexer.next_string_token(token.delimiter_state)
token.type.should eq(t :SPACE)
token.value.should eq(" \n ")

token = lexer.next_string_token(token.delimiter_state)
token.type.should eq(t :STRING)
token.value.should eq("two")

token = lexer.next_string_array_token
token = lexer.next_string_token(token.delimiter_state)
token.type.should eq(t :STRING_ARRAY_END)
end

it "lexes string array with new line gives correct column for next token" do
lexer = Lexer.new("%w(one \n two).")

lexer.next_token
lexer.next_string_array_token
lexer.next_string_array_token
lexer.next_string_array_token
token = lexer.next_token
lexer.next_string_token(token.delimiter_state)
lexer.next_string_token(token.delimiter_state)
lexer.next_string_token(token.delimiter_state)

token = lexer.next_token
token.line_number.should eq(2)
Expand Down
12 changes: 6 additions & 6 deletions spec/compiler/parser/parser_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3032,8 +3032,8 @@ module Crystal
"/" => regex("a\\\\ b"),
"%x[" => command("a\\ b"),
"`" => command("a\\ b"),
"%w[" => string_array("a\\ b".string),
"%i[" => symbol_array("a\\ b".symbol),
"%w[" => string_array("a\\".string, "b".string),
"%i[" => symbol_array("a\\".symbol, "b".symbol),
":\"" => "a\\ b".symbol,
}
it_parses_literal "\\\\a", {
Expand All @@ -3045,8 +3045,8 @@ module Crystal
"/" => regex("\\\\a"),
"%x[" => command("\\a"),
"`" => command("\\a"),
"%w[" => string_array("\\\\a".string),
"%i[" => symbol_array("\\\\a".symbol),
"%w[" => string_array("\\a".string),
"%i[" => symbol_array("\\a".symbol),
":\"" => "\\a".symbol,
}
it_parses_literal "\\", {
Expand All @@ -3071,8 +3071,8 @@ module Crystal
"/" => regex("\\\\"),
"%x[" => command("\\"),
"`" => command("\\"),
"%w[" => "Unterminated string array literal", # FIXME: #12277
"%i[" => "Unterminated symbol array literal", # FIXME: #12277
"%w[" => string_array("\\".string),
"%i[" => symbol_array("\\".symbol),
":\"" => "\\".symbol,
}
it_parses_literal "\\\\\\", {
Expand Down
115 changes: 41 additions & 74 deletions src/compiler/crystal/syntax/lexer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ module Crystal
start_char = next_char
next_char :SYMBOL_ARRAY_START
@token.raw = "%i#{start_char}" if @wants_raw
@token.delimiter_state = Token::DelimiterState.new(:symbol_array, start_char, closing_char(start_char))
@token.delimiter_state = Token::DelimiterState.new(:symbol_array, start_char, closing_char(start_char), allow_escapes: false)
else
@token.type = :OP_PERCENT
end
Expand Down Expand Up @@ -357,7 +357,7 @@ module Crystal
start_char = next_char
next_char :STRING_ARRAY_START
@token.raw = "%w#{start_char}" if @wants_raw
@token.delimiter_state = Token::DelimiterState.new(:string_array, start_char, closing_char(start_char))
@token.delimiter_state = Token::DelimiterState.new(:string_array, start_char, closing_char(start_char), allow_escapes: false)
else
@token.type = :OP_PERCENT
end
Expand Down Expand Up @@ -1444,13 +1444,13 @@ module Crystal
end
end

case current_char
case char = current_char
when '\0'
raise_unterminated_quoted delimiter_state
when string_end
next_char
if string_open_count == 0
@token.type = :DELIMITER_END
@token.type = delimiter_state.kind.array? ? Token::Kind::STRING_ARRAY_END : Token::Kind::DELIMITER_END
else
@token.type = :STRING
@token.value = string_end.to_s
Expand Down Expand Up @@ -1545,6 +1545,16 @@ module Crystal
next_char
end
end
elsif delimiter_state.kind.array?
case char = next_char
when '\\', .ascii_whitespace?, string_end, string_nest
string_token_escape_value char.to_s
else
next_char
@token.type = :STRING
@token.value = string_range(start)
@token.invalid_escape = true
end
else
@token.type = :STRING
@token.value = current_char.to_s
Expand All @@ -1570,17 +1580,34 @@ module Crystal
@token.line_number = @line_number
@token.column_number = @column_number

if delimiter_state.kind.heredoc?
case delimiter_state.kind
when .heredoc?
unless check_heredoc_end delimiter_state
next_string_token_noescape delimiter_state
@token.value = string_range(start)
end
when .array?
@token.type = :SPACE
else
@token.type = :STRING
@token.value = is_slash_r ? "\r\n" : "\n"
end
else
next_string_token_noescape delimiter_state
if delimiter_state.kind.array?
if char.ascii_whitespace?
while char.ascii_whitespace?
handle_slash_r_slash_n_or_slash_n
incr_line_number if char == '\n'
char = next_char
end

@token.type = :SPACE
else
next_string_array_token_noescape delimiter_state
end
else
next_string_token_noescape delimiter_state
end
@token.value = string_range(start)
end

Expand Down Expand Up @@ -1645,7 +1672,9 @@ module Crystal
when .regex? then "Unterminated regular expression"
when .heredoc?
"Unterminated heredoc: can't find \"#{delimiter_state.end}\" anywhere before the end of file"
when .string? then "Unterminated string literal"
when .string? then "Unterminated string literal"
when .string_array? then "Unterminated string array literal"
when .symbol_array? then "Unterminated symbol array literal"
else
::raise "unreachable"
end
Expand Down Expand Up @@ -1800,7 +1829,7 @@ module Crystal
next_char
elsif char == 'w' && peek_next_char.in?('(', '<', '[', '{', '|')
next_char
delimiter_state = Token::DelimiterState.percent_literal(:string_array, current_char, closing_char)
delimiter_state = Token::DelimiterState.percent_literal(:string_array, current_char, closing_char, allow_escapes: false)
next_char
elsif char == 'x' && peek_next_char.in?('(', '<', '[', '{', '|')
next_char
Expand Down Expand Up @@ -2307,77 +2336,15 @@ module Crystal
set_token_raw_from_start(start)
end

def next_string_array_token
while true
if current_char == '\n'
next_char
incr_line_number 1
elsif current_char.ascii_whitespace?
next_char
else
break
end
end

reset_token

if current_char == @token.delimiter_state.end
@token.raw = current_char.to_s if @wants_raw
next_char :STRING_ARRAY_END
return @token
end

start = current_pos
sub_start = start
value = String::Builder.new

escaped = false
while true
case current_char
when Char::ZERO
break # raise is handled by parser
when @token.delimiter_state.end
unless escaped
# For symmetric delimiters (like ||), don't use nesting logic
if @token.delimiter_state.nest == @token.delimiter_state.end || @token.delimiter_state.open_count == 0
break
else
@token.delimiter_state = @token.delimiter_state.with_open_count_delta(-1)
end
end
when @token.delimiter_state.nest
unless @token.delimiter_state.nest == @token.delimiter_state.end || escaped
@token.delimiter_state = @token.delimiter_state.with_open_count_delta(+1)
end
when .ascii_whitespace?
break unless escaped
else
if escaped
value << '\\'
end
end

escaped = current_char == '\\'
if escaped
value.write @reader.string.to_slice[sub_start, current_pos - sub_start]
sub_start = current_pos + 1
end
def next_string_array_token_noescape(delimiter_state)
string_end = delimiter_state.end
string_nest = delimiter_state.nest

while !current_char.in?(string_end, string_nest, '\0', '\\', '#') && !current_char.ascii_whitespace?
next_char
end

if start == current_pos
@token.type = :EOF
return @token
end

value.write @reader.string.to_slice[sub_start, current_pos - sub_start]

@token.type = :STRING
@token.value = value.to_s
set_token_raw_from_start(start)

@token
end

def consume_loc_pragma
Expand Down
52 changes: 41 additions & 11 deletions src/compiler/crystal/syntax/parser.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2406,32 +2406,62 @@ module Crystal
end

def parse_string_array
parse_string_or_symbol_array StringLiteral, "String"
strings, end_location = parse_string_or_symbol_array_strings do |pieces|
combine_pieces(pieces, @token.delimiter_state)
end

ArrayLiteral.new(strings, Path.global("String")).at_end(end_location)
end

def parse_symbol_array
parse_string_or_symbol_array SymbolLiteral, "Symbol"
strings, end_location = parse_string_or_symbol_array_strings do |pieces|
string = combine_stringliteral_pieces(pieces, @token.delimiter_state)
SymbolLiteral.new(string)
end

ArrayLiteral.new(strings, Path.global("Symbol")).at_end(end_location)
end

def parse_string_or_symbol_array(klass, elements_type)
def parse_string_or_symbol_array_strings(&)
strings = [] of ASTNode

while !@token.type.string_array_end?
if element = parse_percent_array_element { |pieces| yield pieces }
strings << element
end
end

check :STRING_ARRAY_END

end_location = @token.location

next_token

{strings, end_location}
end

def parse_percent_array_element(&)
pieces = [] of Piece
start_location = nil
end_location = nil
delimiter_state = @token.delimiter_state

while true
next_string_array_token
end_location = token_end_location
next_string_token(delimiter_state)
start_location ||= @token.location
delimiter_state = @token.delimiter_state
case @token.type
when .string?
strings << klass.new(@token.value.to_s).at(@token.location).at_end(token_end_location)
when .string_array_end?
end_location = token_end_location
next_token
break
pieces << Piece.new(@token.value.to_s, @token.line_number)
else
raise "Unterminated #{elements_type.downcase} array literal"
break
end
end

ArrayLiteral.new(strings, Path.global(elements_type)).at_end(end_location)
return if pieces.empty?

(yield pieces).at(start_location).at_end(end_location)
end

def parse_empty_array_literal
Expand Down
Loading
Loading