From 913ffcd1ddb854d2fdbf73a6ddf1930b8a472322 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Tue, 20 Jan 2026 21:20:06 +0100 Subject: [PATCH 1/6] [ruby/prism] Check using Prism nodes if a command call has any arguments in Ripper translator * We don't know what `on_*` events might return so we cannot assume it's an Array. * See https://github.com/ruby/prism/issues/3838#issuecomment-3774702396 https://github.com/ruby/prism/commit/bed4271ce2 --- lib/prism/translation/ripper.rb | 18 +++++++++++++----- test/prism/ruby/ripper_test.rb | 12 ++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/prism/translation/ripper.rb b/lib/prism/translation/ripper.rb index afe30bb5db4131..41dd6994382bc6 100644 --- a/lib/prism/translation/ripper.rb +++ b/lib/prism/translation/ripper.rb @@ -1112,7 +1112,7 @@ def visit_call_node(node) else arguments, block = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc || node.location)) call = - if node.opening_loc.nil? && arguments&.any? + if node.opening_loc.nil? && get_arguments_and_block(node.arguments, node.block).first.any? bounds(node.location) on_command(message, arguments) elsif !node.opening_loc.nil? @@ -1179,17 +1179,25 @@ def visit_call_node(node) end end - # Visit the arguments and block of a call node and return the arguments - # and block as they should be used. - private def visit_call_node_arguments(arguments_node, block_node, trailing_comma) + # Extract the arguments and block Ripper-style, which means if the block + # is like `&b` then it's moved to arguments. + private def get_arguments_and_block(arguments_node, block_node) arguments = arguments_node&.arguments || [] block = block_node if block.is_a?(BlockArgumentNode) - arguments << block + arguments += [block] block = nil end + [arguments, block] + end + + # Visit the arguments and block of a call node and return the arguments + # and block as they should be used. + private def visit_call_node_arguments(arguments_node, block_node, trailing_comma) + arguments, block = get_arguments_and_block(arguments_node, block_node) + [ if arguments.length == 1 && arguments.first.is_a?(ForwardingArgumentsNode) visit(arguments.first) diff --git a/test/prism/ruby/ripper_test.rb b/test/prism/ruby/ripper_test.rb index 174c90cbcabcc7..8848c6a8c05eaf 100644 --- a/test/prism/ruby/ripper_test.rb +++ b/test/prism/ruby/ripper_test.rb @@ -110,6 +110,14 @@ class PrismEvents < Translation::Ripper include Events end + class ObjectEvents < Translation::Ripper + Prism::Translation::Ripper::PARSER_EVENTS.each do |event| + define_method(:"on_#{event}") do |*args| + Object.new + end + end + end + def test_events source = "1 rescue 2" ripper = RipperEvents.new(source) @@ -152,6 +160,10 @@ def test_internals def assert_ripper_sexp_raw(source) assert_equal Ripper.sexp_raw(source), Prism::Translation::Ripper.sexp_raw(source) + + # Similar to test/ripper/assert_parse_files.rb in CRuby + object_events = ObjectEvents.new(source) + assert_nothing_raised { object_events.parse } end def assert_ripper_lex(source) From 8aedb89681c42a8d6169ff1950135f841cd578f1 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:19:42 +0100 Subject: [PATCH 2/6] [ruby/prism] Also handle `BasicObject` as a return value We should touch these as little as possible and just pass them along https://github.com/ruby/prism/commit/52c4fa785e --- lib/prism/translation/ripper.rb | 51 +++++++++++++++++---------------- test/prism/ruby/ripper_test.rb | 18 +++++++----- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/lib/prism/translation/ripper.rb b/lib/prism/translation/ripper.rb index 41dd6994382bc6..735217d2e03608 100644 --- a/lib/prism/translation/ripper.rb +++ b/lib/prism/translation/ripper.rb @@ -832,7 +832,7 @@ def visit_array_pattern_node(node) # foo(bar) # ^^^ def visit_arguments_node(node) - arguments, _ = visit_call_node_arguments(node, nil, false) + arguments, _, _ = visit_call_node_arguments(node, nil, false) arguments end @@ -1042,16 +1042,16 @@ def visit_call_node(node) case node.name when :[] receiver = visit(node.receiver) - arguments, block = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc)) + arguments, block, has_ripper_block = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc)) bounds(node.location) call = on_aref(receiver, arguments) - if block.nil? - call - else + if has_ripper_block bounds(node.location) on_method_add_block(call, block) + else + call end when :[]= receiver = visit(node.receiver) @@ -1110,7 +1110,7 @@ def visit_call_node(node) if node.variable_call? on_vcall(message) else - arguments, block = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc || node.location)) + arguments, block, has_ripper_block = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc || node.location)) call = if node.opening_loc.nil? && get_arguments_and_block(node.arguments, node.block).first.any? bounds(node.location) @@ -1123,11 +1123,11 @@ def visit_call_node(node) on_method_add_arg(on_fcall(message), on_args_new) end - if block.nil? - call - else + if has_ripper_block bounds(node.block.location) on_method_add_block(call, block) + else + call end end end @@ -1151,7 +1151,7 @@ def visit_call_node(node) bounds(node.location) on_assign(on_field(receiver, call_operator, message), value) else - arguments, block = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc || node.location)) + arguments, block, has_ripper_block = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc || node.location)) call = if node.opening_loc.nil? bounds(node.location) @@ -1169,11 +1169,11 @@ def visit_call_node(node) on_method_add_arg(on_call(receiver, call_operator, message), arguments) end - if block.nil? - call - else + if has_ripper_block bounds(node.block.location) on_method_add_block(call, block) + else + call end end end @@ -1211,7 +1211,8 @@ def visit_call_node(node) on_args_add_block(args, false) end end, - visit(block) + visit(block), + block != nil, ] end @@ -1648,10 +1649,10 @@ def visit_def_node(node) end bounds(node.location) - if receiver.nil? - on_def(name, parameters, bodystmt) - else + if receiver on_defs(receiver, operator, name, parameters, bodystmt) + else + on_def(name, parameters, bodystmt) end end @@ -2049,7 +2050,7 @@ def visit_in_node(node) # ^^^^^^^^^^^^^^^ def visit_index_operator_write_node(node) receiver = visit(node.receiver) - arguments, _ = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc)) + arguments, _, _ = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc)) bounds(node.location) target = on_aref_field(receiver, arguments) @@ -2066,7 +2067,7 @@ def visit_index_operator_write_node(node) # ^^^^^^^^^^^^^^^^ def visit_index_and_write_node(node) receiver = visit(node.receiver) - arguments, _ = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc)) + arguments, _, _ = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc)) bounds(node.location) target = on_aref_field(receiver, arguments) @@ -2083,7 +2084,7 @@ def visit_index_and_write_node(node) # ^^^^^^^^^^^^^^^^ def visit_index_or_write_node(node) receiver = visit(node.receiver) - arguments, _ = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc)) + arguments, _, _ = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc)) bounds(node.location) target = on_aref_field(receiver, arguments) @@ -2100,7 +2101,7 @@ def visit_index_or_write_node(node) # ^^^^^^^^ def visit_index_target_node(node) receiver = visit(node.receiver) - arguments, _ = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc)) + arguments, _, _ = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc)) bounds(node.location) on_aref_field(receiver, arguments) @@ -3130,7 +3131,7 @@ def visit_string_node(node) # super(foo) # ^^^^^^^^^^ def visit_super_node(node) - arguments, block = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.rparen_loc || node.location)) + arguments, block, has_ripper_block = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.rparen_loc || node.location)) if !node.lparen_loc.nil? bounds(node.lparen_loc) @@ -3140,11 +3141,11 @@ def visit_super_node(node) bounds(node.location) call = on_super(arguments) - if block.nil? - call - else + if has_ripper_block bounds(node.block.location) on_method_add_block(call, block) + else + call end end diff --git a/test/prism/ruby/ripper_test.rb b/test/prism/ruby/ripper_test.rb index 8848c6a8c05eaf..6e9dcee4c9c26a 100644 --- a/test/prism/ruby/ripper_test.rb +++ b/test/prism/ruby/ripper_test.rb @@ -111,10 +111,18 @@ class PrismEvents < Translation::Ripper end class ObjectEvents < Translation::Ripper + OBJECT = BasicObject.new Prism::Translation::Ripper::PARSER_EVENTS.each do |event| - define_method(:"on_#{event}") do |*args| - Object.new - end + define_method(:"on_#{event}") { |*args| OBJECT } + end + end + + Fixture.each_for_current_ruby(except: incorrect) do |fixture| + define_method("#{fixture.test_name}_events") do + source = fixture.read + # Similar to test/ripper/assert_parse_files.rb in CRuby + object_events = ObjectEvents.new(source) + assert_nothing_raised { object_events.parse } end end @@ -160,10 +168,6 @@ def test_internals def assert_ripper_sexp_raw(source) assert_equal Ripper.sexp_raw(source), Prism::Translation::Ripper.sexp_raw(source) - - # Similar to test/ripper/assert_parse_files.rb in CRuby - object_events = ObjectEvents.new(source) - assert_nothing_raised { object_events.parse } end def assert_ripper_lex(source) From 54fa9c81e13039ec92f5b2132a65e64092d81ad7 Mon Sep 17 00:00:00 2001 From: Schneems Date: Tue, 20 Jan 2026 15:32:33 -0600 Subject: [PATCH 3/6] [ruby/syntax_suggest] Refactor multi-prism version logic The reason this logic for different methods branches in the class instead of internally was to be eagerly aggressive about runtime performance. This code is currently only used once for the document where it's invoked ~N times (where N is number of lines): ```ruby module SyntaxSuggest class CleanDocument # ... def join_trailing_slash! trailing_groups = @document.select(&:trailing_slash?).map do |code_line| take_while_including(code_line.index..) { |x| x.trailing_slash? } end join_groups(trailing_groups) self end ``` Since this is not currently a hot-spot I think merging the branches and using a case statement is a reasonable tradeoff and avoids the need to do specific version testing. An alternative idea was presented in #241 of behavior-based testing for branch logic (which I would prefer), however, calling the code triggered requiring a `DelegateClass` when the `syntax_suggest/api` is being required. https://github.com/ruby/syntax_suggest/commit/ab122c455f --- lib/syntax_suggest/code_line.rb | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/syntax_suggest/code_line.rb b/lib/syntax_suggest/code_line.rb index 892c273c4123cb..76ca892ac384d0 100644 --- a/lib/syntax_suggest/code_line.rb +++ b/lib/syntax_suggest/code_line.rb @@ -180,21 +180,17 @@ def ignore_newline_not_beg? # EOM # expect(lines.first.trailing_slash?).to eq(true) # - if SyntaxSuggest.use_prism_parser? && Prism::VERSION <= "1.8.0" - # Older versions of prism didn't correctly emit on_sp - def trailing_slash? - last = @lex.last - return false unless last - - last.type == :on_tstring_end || (last.type == :on_sp && last.token == TRAILING_SLASH) - end - else - def trailing_slash? - last = @lex.last - return false unless last - return false unless last.type == :on_sp + def trailing_slash? + last = @lex.last + # Older versions of prism diverged slightly from Ripper in compatibility mode + case last&.type + when :on_sp last.token == TRAILING_SLASH + when :on_tstring_end + true + else + false end end From 7f687b0abc155cd941d416677184f98f5f7d1804 Mon Sep 17 00:00:00 2001 From: Schneems Date: Wed, 21 Jan 2026 09:58:28 -0600 Subject: [PATCH 4/6] [ruby/syntax_suggest] v2.0.3 https://github.com/ruby/syntax_suggest/commit/a81b92fcf2 --- lib/syntax_suggest/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/syntax_suggest/version.rb b/lib/syntax_suggest/version.rb index 1aa908f4e5970d..db50a1a89af1b8 100644 --- a/lib/syntax_suggest/version.rb +++ b/lib/syntax_suggest/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SyntaxSuggest - VERSION = "2.0.2" + VERSION = "2.0.3" end From e7f6a30d794fb241deee8987a9fb5cba6c432b48 Mon Sep 17 00:00:00 2001 From: git Date: Wed, 21 Jan 2026 16:02:13 +0000 Subject: [PATCH 5/6] Update default gems list at 7f687b0abc155cd941d416677184f9 [ci skip] --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index 2b021e7cddff83..6f8ac408e52e4b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -49,6 +49,7 @@ releases. * prism 1.8.0 * stringio 3.2.1.dev * strscan 3.1.7.dev +* syntax_suggest 2.0.3 ### The following bundled gems are updated. From f3a5b0cdb5ecd47d9240ded667a90436c489fea7 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:50:28 +0100 Subject: [PATCH 6/6] Apply backport label to 4.X branches (#15925) --- .github/labeler.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/labeler.yml b/.github/labeler.yml index e81aed8e986284..f39fcec386e7b8 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -4,3 +4,4 @@ Documentation: Backport: - base-branch: 'ruby_3_\d' +- base-branch: 'ruby_4_\d'