From 6409715212d22699bd2751a363b050a5d8b94b83 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Tue, 2 Dec 2025 17:34:36 -0800 Subject: Fix allocationless anonymous splat keyword argument check Previously, if an argument splat and keywords are provided by the caller, it did not check whether the method/proc accepted keywords before avoiding the allocation. This is incorrect, because if the method/proc does not accept keywords, the keywords passed by the caller are added as a positional argument, so there must be an allocation to avoid mutating the positional splat argument. Add a check that if the caller passes keywords, the method/proc must accept keywords in order to optimize. If the caller passes a keyword splat, either the method/proc must accept keywords, or the keyword splat must be empty in order to optimize. If keywords are explicitly disallowed via `**nil`, the optimization should be skipped, because the array is mutated before the ArgumentError exception is raised. In addition to a test for the correct behavior, add an allocation test for a method that accepts an anonymous splat without keywords. Fixes [Bug #21757] --- test/ruby/test_allocation.rb | 53 ++++++++++++++++++++++++++++++++++++++++++++ test/ruby/test_call.rb | 29 ++++++++++++++++++++++++ 2 files changed, 82 insertions(+) (limited to 'test/ruby') diff --git a/test/ruby/test_allocation.rb b/test/ruby/test_allocation.rb index 6ade391c95..90d7c04f9b 100644 --- a/test/ruby/test_allocation.rb +++ b/test/ruby/test_allocation.rb @@ -527,6 +527,59 @@ class TestAllocation < Test::Unit::TestCase RUBY end + def test_anonymous_splat_parameter + only_block = block.empty? ? block : block[2..] + check_allocations(<<~RUBY) + def self.anon_splat(*#{block}); end + + check_allocations(1, 1, "anon_splat(1, a: 2#{block})") + check_allocations(1, 1, "anon_splat(1, *empty_array, a: 2#{block})") + check_allocations(1, 1, "anon_splat(1, a:2, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat(1, **empty_hash, a: 2#{block})") + + check_allocations(1, 0, "anon_splat(1, **nil#{block})") + check_allocations(1, 0, "anon_splat(1, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat(1, **hash1#{block})") + check_allocations(1, 1, "anon_splat(1, *empty_array, **hash1#{block})") + check_allocations(1, 1, "anon_splat(1, **hash1, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat(1, **empty_hash, **hash1#{block})") + + check_allocations(1, 0, "anon_splat(1, *empty_array#{block})") + check_allocations(1, 0, "anon_splat(1, *empty_array, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "anon_splat(*array1, a: 2#{block})") + + check_allocations(0, 0, "anon_splat(*nil, **nill#{block})") + check_allocations(0, 0, "anon_splat(*array1, **nill#{block})") + check_allocations(0, 0, "anon_splat(*array1, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat(*array1, **hash1#{block})") + check_allocations(1, 1, "anon_splat(*array1, *empty_array, **hash1#{block})") + + check_allocations(1, 0, "anon_splat(*array1, *empty_array#{block})") + check_allocations(1, 0, "anon_splat(*array1, *empty_array, **empty_hash#{block})") + + check_allocations(1, 1, "anon_splat(*array1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat(*array1, *empty_array, **hash1, **empty_hash#{block})") + + check_allocations(0, 0, "anon_splat(#{only_block})") + check_allocations(1, 1, "anon_splat(a: 2#{block})") + check_allocations(0, 0, "anon_splat(**empty_hash#{block})") + + check_allocations(1, 1, "anon_splat(1, *empty_array, a: 2, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat(1, *empty_array, **hash1, **empty_hash#{block})") + check_allocations(1, 1, "anon_splat(*array1, **empty_hash, a: 2#{block})") + check_allocations(1, 1, "anon_splat(*array1, **hash1, **empty_hash#{block})") + + unless defined?(RubyVM::YJIT.enabled?) && RubyVM::YJIT.enabled? + check_allocations(0, 0, "anon_splat(*array1, **nil#{block})") + check_allocations(1, 0, "anon_splat(*r2k_empty_array#{block})") + check_allocations(1, 1, "anon_splat(*r2k_array#{block})") + check_allocations(1, 0, "anon_splat(*r2k_empty_array1#{block})") + check_allocations(1, 1, "anon_splat(*r2k_array1#{block})") + end + RUBY + end + def test_anonymous_splat_and_anonymous_keyword_splat_parameters only_block = block.empty? ? block : block[2..] check_allocations(<<~RUBY) diff --git a/test/ruby/test_call.rb b/test/ruby/test_call.rb index 7843f3b476..1b30ed34d8 100644 --- a/test/ruby/test_call.rb +++ b/test/ruby/test_call.rb @@ -423,6 +423,35 @@ class TestCall < Test::Unit::TestCase assert_equal([1, 2, {}], s(*r2ka)) end + def test_anon_splat_mutated_bug_21757 + args = [1, 2] + kw = {bug: true} + + def self.m(*); end + m(*args, bug: true) + assert_equal(2, args.length) + + proc = ->(*) { } + proc.(*args, bug: true) + assert_equal(2, args.length) + + def self.m2(*); end + m2(*args, **kw) + assert_equal(2, args.length) + + proc = ->(*) { } + proc.(*args, **kw) + assert_equal(2, args.length) + + def self.m3(*, **nil); end + assert_raise(ArgumentError) { m3(*args, bug: true) } + assert_equal(2, args.length) + + proc = ->(*, **nil) { } + assert_raise(ArgumentError) { proc.(*args, bug: true) } + assert_equal(2, args.length) + end + def test_kwsplat_block_eval_order def self.t(**kw, &b) [kw, b] end -- cgit v1.2.3