Do not respect ruby2_keywords on method/proc with post arguments

Previously, ruby2_keywords could be used on a method or proc with
post arguments, but I don't think the behavior is desired:

```ruby
def a(*c, **kw) [c, kw] end
def b(*a, b) a(*a, b) end
ruby2_keywords(:b)

b({foo: 1}, bar: 1)
```

This changes ruby2_keywords to emit a warning and not set the
flag on a method/proc with post arguments.

While here, fix the ruby2_keywords specs for warnings, since they
weren't testing what they should be testing.  They all warned
because the method didn't accept a rest argument, not because it
accepted a keyword or keyword rest argument.

[Backport #21402]
This commit is contained in:
Jeremy Evans 2025-05-29 22:32:09 -07:00 committed by Takashi Kokubun
parent d49c7d0661
commit fd036dbc3f
5 changed files with 62 additions and 10 deletions

3
proc.c
View File

@ -3958,12 +3958,13 @@ proc_ruby2_keywords(VALUE procval)
switch (proc->block.type) {
case block_type_iseq:
if (ISEQ_BODY(proc->block.as.captured.code.iseq)->param.flags.has_rest &&
!ISEQ_BODY(proc->block.as.captured.code.iseq)->param.flags.has_post &&
!ISEQ_BODY(proc->block.as.captured.code.iseq)->param.flags.has_kw &&
!ISEQ_BODY(proc->block.as.captured.code.iseq)->param.flags.has_kwrest) {
ISEQ_BODY(proc->block.as.captured.code.iseq)->param.flags.ruby2_keywords = 1;
}
else {
rb_warn("Skipping set of ruby2_keywords flag for proc (proc accepts keywords or proc does not accept argument splat)");
rb_warn("Skipping set of ruby2_keywords flag for proc (proc accepts keywords or post arguments or proc does not accept argument splat)");
}
break;
default:

View File

@ -275,7 +275,7 @@ describe "Module#ruby2_keywords" do
it "prints warning when a method accepts keywords" do
obj = Object.new
def obj.foo(a:, b:) end
def obj.foo(*a, b:) end
-> {
obj.singleton_class.class_exec do
@ -286,7 +286,7 @@ describe "Module#ruby2_keywords" do
it "prints warning when a method accepts keyword splat" do
obj = Object.new
def obj.foo(**a) end
def obj.foo(*a, **b) end
-> {
obj.singleton_class.class_exec do
@ -294,4 +294,17 @@ describe "Module#ruby2_keywords" do
end
}.should complain(/Skipping set of ruby2_keywords flag for/)
end
ruby_version_is "3.5" do
it "prints warning when a method accepts post arguments" do
obj = Object.new
def obj.foo(*a, b) end
-> {
obj.singleton_class.class_exec do
ruby2_keywords :foo
end
}.should complain(/Skipping set of ruby2_keywords flag for/)
end
end
end

View File

@ -39,7 +39,7 @@ describe "Proc#ruby2_keywords" do
end
it "prints warning when a proc accepts keywords" do
f = -> a:, b: { }
f = -> *a, b: { }
-> {
f.ruby2_keywords
@ -47,10 +47,20 @@ describe "Proc#ruby2_keywords" do
end
it "prints warning when a proc accepts keyword splat" do
f = -> **a { }
f = -> *a, **b { }
-> {
f.ruby2_keywords
}.should complain(/Skipping set of ruby2_keywords flag for/)
end
ruby_version_is "3.5" do
it "prints warning when a proc accepts post arguments" do
f = -> *a, b { }
-> {
f.ruby2_keywords
}.should complain(/Skipping set of ruby2_keywords flag for/)
end
end
end

View File

@ -2424,6 +2424,21 @@ class TestKeywordArguments < Test::Unit::TestCase
assert_raise(ArgumentError) { m.call(42, a: 1, **h2) }
end
def test_ruby2_keywords_post_arg
def self.a(*c, **kw) [c, kw] end
def self.b(*a, b) a(*a, b) end
assert_warn(/Skipping set of ruby2_keywords flag for b \(method accepts keywords or post arguments or method does not accept argument splat\)/) do
assert_nil(singleton_class.send(:ruby2_keywords, :b))
end
assert_equal([[{foo: 1}, {bar: 1}], {}], b({foo: 1}, bar: 1))
b = ->(*a, b){a(*a, b)}
assert_warn(/Skipping set of ruby2_keywords flag for proc \(proc accepts keywords or post arguments or proc does not accept argument splat\)/) do
b.ruby2_keywords
end
assert_equal([[{foo: 1}, {bar: 1}], {}], b.({foo: 1}, bar: 1))
end
def test_proc_ruby2_keywords
h1 = {:a=>1}
foo = ->(*args, &block){block.call(*args)}
@ -2436,8 +2451,8 @@ class TestKeywordArguments < Test::Unit::TestCase
assert_raise(ArgumentError) { foo.call(:a=>1, &->(arg, **kw){[arg, kw]}) }
assert_equal(h1, foo.call(:a=>1, &->(arg){arg}))
[->(){}, ->(arg){}, ->(*args, **kw){}, ->(*args, k: 1){}, ->(*args, k: ){}].each do |pr|
assert_warn(/Skipping set of ruby2_keywords flag for proc \(proc accepts keywords or proc does not accept argument splat\)/) do
[->(){}, ->(arg){}, ->(*args, x){}, ->(*args, **kw){}, ->(*args, k: 1){}, ->(*args, k: ){}].each do |pr|
assert_warn(/Skipping set of ruby2_keywords flag for proc \(proc accepts keywords or post arguments or proc does not accept argument splat\)/) do
pr.ruby2_keywords
end
end
@ -2790,10 +2805,21 @@ class TestKeywordArguments < Test::Unit::TestCase
assert_equal(:opt, o.clear_last_opt(a: 1))
assert_nothing_raised(ArgumentError) { o.clear_last_empty_method(a: 1) }
assert_warn(/Skipping set of ruby2_keywords flag for bar \(method accepts keywords or method does not accept argument splat\)/) do
assert_warn(/Skipping set of ruby2_keywords flag for bar \(method accepts keywords or post arguments or method does not accept argument splat\)/) do
assert_nil(c.send(:ruby2_keywords, :bar))
end
c.class_eval do
def bar_post(*a, x) = nil
define_method(:bar_post_bmethod) { |*a, x| }
end
assert_warn(/Skipping set of ruby2_keywords flag for bar_post \(method accepts keywords or post arguments or method does not accept argument splat\)/) do
assert_nil(c.send(:ruby2_keywords, :bar_post))
end
assert_warn(/Skipping set of ruby2_keywords flag for bar_post_bmethod \(method accepts keywords or post arguments or method does not accept argument splat\)/) do
assert_nil(c.send(:ruby2_keywords, :bar_post_bmethod))
end
utf16_sym = "abcdef".encode("UTF-16LE").to_sym
c.send(:define_method, utf16_sym, c.instance_method(:itself))
assert_warn(/abcdef/) do

View File

@ -2599,13 +2599,14 @@ rb_mod_ruby2_keywords(int argc, VALUE *argv, VALUE module)
switch (me->def->type) {
case VM_METHOD_TYPE_ISEQ:
if (ISEQ_BODY(me->def->body.iseq.iseqptr)->param.flags.has_rest &&
!ISEQ_BODY(me->def->body.iseq.iseqptr)->param.flags.has_post &&
!ISEQ_BODY(me->def->body.iseq.iseqptr)->param.flags.has_kw &&
!ISEQ_BODY(me->def->body.iseq.iseqptr)->param.flags.has_kwrest) {
ISEQ_BODY(me->def->body.iseq.iseqptr)->param.flags.ruby2_keywords = 1;
rb_clear_method_cache(module, name);
}
else {
rb_warn("Skipping set of ruby2_keywords flag for %"PRIsVALUE" (method accepts keywords or method does not accept argument splat)", QUOTE_ID(name));
rb_warn("Skipping set of ruby2_keywords flag for %"PRIsVALUE" (method accepts keywords or post arguments or method does not accept argument splat)", QUOTE_ID(name));
}
break;
case VM_METHOD_TYPE_BMETHOD: {
@ -2618,13 +2619,14 @@ rb_mod_ruby2_keywords(int argc, VALUE *argv, VALUE module)
const struct rb_captured_block *captured = VM_BH_TO_ISEQ_BLOCK(procval);
const rb_iseq_t *iseq = rb_iseq_check(captured->code.iseq);
if (ISEQ_BODY(iseq)->param.flags.has_rest &&
!ISEQ_BODY(iseq)->param.flags.has_post &&
!ISEQ_BODY(iseq)->param.flags.has_kw &&
!ISEQ_BODY(iseq)->param.flags.has_kwrest) {
ISEQ_BODY(iseq)->param.flags.ruby2_keywords = 1;
rb_clear_method_cache(module, name);
}
else {
rb_warn("Skipping set of ruby2_keywords flag for %"PRIsVALUE" (method accepts keywords or method does not accept argument splat)", QUOTE_ID(name));
rb_warn("Skipping set of ruby2_keywords flag for %"PRIsVALUE" (method accepts keywords or post arguments or method does not accept argument splat)", QUOTE_ID(name));
}
break;
}