diff --git a/spec/compiler/parser/parser_spec.cr b/spec/compiler/parser/parser_spec.cr index 973394a91775..7a94c239641f 100644 --- a/spec/compiler/parser/parser_spec.cr +++ b/spec/compiler/parser/parser_spec.cr @@ -1193,6 +1193,10 @@ describe "Parser" do it_parses "<<-SOME\n Sa\n Se\n SOME", "Sa\nSe".string_interpolation it_parses "<<-HERE\n \#{1} \#{2}\n HERE", StringInterpolation.new([1.int32, " ".string, 2.int32] of ASTNode) it_parses "<<-HERE\n \#{1} \\n \#{2}\n HERE", StringInterpolation.new([1.int32, " \n ".string, 2.int32] of ASTNode) + it_parses "<<-HERE\nHERE", "".string_interpolation + it_parses "<<-HERE1; <<-HERE2\nHERE1\nHERE2", ["".string_interpolation, "".string_interpolation] of ASTNode + it_parses "<<-HERE1; <<-HERE2\nhere1\nHERE1\nHERE2", ["here1".string_interpolation, "".string_interpolation] of ASTNode + it_parses "<<-HERE1; <<-HERE2\nHERE1\nhere2\nHERE2", ["".string_interpolation, "here2".string_interpolation] of ASTNode assert_syntax_error "<<-HERE\n One\nwrong\n Zero\n HERE", "heredoc line must have an indent greater or equal than 2", 3, 1 assert_syntax_error "<<-HERE\n One\n wrong\n Zero\n HERE", "heredoc line must have an indent greater or equal than 2", 3, 1 assert_syntax_error "<<-HERE\n One\n \#{1}\n Zero\n HERE", "heredoc line must have an indent greater or equal than 2", 3, 1 diff --git a/src/compiler/crystal/syntax/lexer.cr b/src/compiler/crystal/syntax/lexer.cr index 8fe78ad31960..2c95bc55b08e 100644 --- a/src/compiler/crystal/syntax/lexer.cr +++ b/src/compiler/crystal/syntax/lexer.cr @@ -1751,6 +1751,14 @@ module Crystal string_nest = delimiter_state.nest string_open_count = delimiter_state.open_count + # For empty heredocs: + if @token.type == :NEWLINE && delimiter_state.kind == :heredoc + if check_heredoc_end delimiter_state + set_token_raw_from_start start + return @token + end + end + case current_char when '\0' raise_unterminated_quoted delimiter_state @@ -1877,63 +1885,16 @@ module Crystal @token.column_number = @column_number if delimiter_state.kind == :heredoc - string_end = string_end.to_s - old_pos = current_pos - old_column = @column_number - - while current_char == ' ' || current_char == '\t' - next_char - end - - indent = @column_number - 1 - - if string_end.starts_with?(current_char) - reached_end = false - - string_end.each_char do |c| - unless c == current_char - reached_end = false - break - end - next_char - reached_end = true - end - - if reached_end && - (current_char == '\n' || current_char == '\0' || - (current_char == '\r' && peek_next_char == '\n' && next_char)) - @token.type = :DELIMITER_END - @token.delimiter_state = delimiter_state.with_heredoc_indent(indent) - else - @reader.pos = old_pos - @column_number = old_column - @token.column_number = @column_number - next_string_token delimiter_state - @token.value = (is_slash_r ? "\r\n" : '\n') + @token.value.to_s - end - else - @reader.pos = old_pos - @column_number = old_column - @token.column_number = @column_number - @token.type = :STRING - @token.value = is_slash_r ? "\r\n" : "\n" + unless check_heredoc_end delimiter_state + next_string_token_noescape delimiter_state + @token.value = string_range(start) end else @token.type = :STRING @token.value = is_slash_r ? "\r\n" : "\n" end else - while current_char != string_end && - current_char != string_nest && - current_char != '\0' && - current_char != '\\' && - current_char != '#' && - current_char != '\r' && - current_char != '\n' - next_char - end - - @token.type = :STRING + next_string_token_noescape delimiter_state @token.value = string_range(start) end @@ -1942,6 +1903,62 @@ module Crystal @token end + def next_string_token_noescape(delimiter_state) + string_end = delimiter_state.end + string_nest = delimiter_state.nest + + while current_char != string_end && + current_char != string_nest && + current_char != '\0' && + current_char != '\\' && + current_char != '#' && + current_char != '\r' && + current_char != '\n' + next_char + end + + @token.type = :STRING + end + + def check_heredoc_end(delimiter_state) + string_end = delimiter_state.end.to_s + old_pos = current_pos + old_column = @column_number + + while current_char == ' ' || current_char == '\t' + next_char + end + + indent = @column_number - 1 + + if string_end.starts_with?(current_char) + reached_end = false + + string_end.each_char do |c| + unless c == current_char + reached_end = false + break + end + next_char + reached_end = true + end + + if reached_end && + (current_char == '\n' || current_char == '\0' || + (current_char == '\r' && peek_next_char == '\n' && next_char)) + @token.type = :DELIMITER_END + @token.delimiter_state = delimiter_state.with_heredoc_indent(indent) + return true + end + end + + @reader.pos = old_pos + @column_number = old_column + @token.column_number = @column_number + + false + end + def raise_unterminated_quoted(delimiter_state) msg = case delimiter_state.kind when :command then "Unterminated command literal"