diff --git a/lib/prism/lex_compat.rb b/lib/prism/lex_compat.rb index 4d92842bda..1b3f967fc7 100644 --- a/lib/prism/lex_compat.rb +++ b/lib/prism/lex_compat.rb @@ -846,6 +846,8 @@ def post_process_tokens(tokens, source, data_loc, bom, eof_token) cache = Translation::Ripper::LineAndColumnCache.new(source) + brace_depth = 0 + tokens.each do |token| # Skip missing heredoc ends. next if token[1] == :on_heredoc_end && token[2] == "" @@ -854,6 +856,24 @@ def post_process_tokens(tokens, source, data_loc, bom, eof_token) line, column = token[0] start_offset = source.byte_offset(line, column) + # Replicate a Ripper quirk where exactly the first unmatched closing + # brace is emitted as :on_embexpr_end, and any subsequent ones are :on_rbrace. + # + # Track brace nesting depth to handle unmatched closing braces. + # :on_lbrace (hashes/blocks), :on_embexpr_beg (interpolation), and :on_tlambeg (lambdas) + # all start a curly brace context balanced by a closing }. + case token[1] + when :on_lbrace, :on_embexpr_beg, :on_tlambeg + brace_depth += 1 + when :on_rbrace + if brace_depth == 0 + token = [token[0], :on_embexpr_end, token[2], prev_token_state] + end + brace_depth -= 1 + when :on_embexpr_end + brace_depth -= 1 + end + # Ripper reports columns on line 1 without counting the BOM, so we # adjust to get the real offset start_offset += 3 if line == 1 && bom diff --git a/test/prism/ruby/ripper_test.rb b/test/prism/ruby/ripper_test.rb index c5aa63aab2..10dee8e380 100644 --- a/test/prism/ruby/ripper_test.rb +++ b/test/prism/ruby/ripper_test.rb @@ -219,6 +219,14 @@ def test_lex_invalid_syntax assert_equal(Ripper.lex('if;)'), Translation::Ripper.lex('if;)')) end + def test_unmatched_braces + assert_ripper_lex("} }") + assert_ripper_lex("{ b: 1 } }") + assert_ripper_lex("[].each { } }") + assert_ripper_lex('"#{1}" }') + assert_ripper_lex("-> { } }") + end + def test_tokenize source = "foo;1;BAZ" assert_equal(Ripper.tokenize(source), Translation::Ripper.tokenize(source))