Add Module#ruby2_keywords for passing keywords through regular argument splats

This approach uses a flag bit on the final hash object in the regular splat,
as opposed to a previous approach that used a VM frame flag.  The hash flag
approach is less invasive, and handles some cases that the VM frame flag
approach does not, such as saving the argument splat array and splatting it
later:

  ruby2_keywords def foo(*args)
    @args = args
    bar
  end
  def bar
    baz(*@args)
  end
  def baz(*args, **kw)
    [args, kw]
  end
  foo(a:1)    #=> [[], {a: 1}]
  foo({a: 1}, **{}) #=> [[{a: 1}], {}]

  foo({a: 1}) #=> 2.7: [[], {a: 1}] # and warning
  foo({a: 1}) #=> 3.0: [[{a: 1}], {}]

It doesn't handle some cases that the VM frame flag handles, such as when
the final hash object is replaced using Hash#merge, but those cases are
probably less common and are unlikely to properly support keyword
argument separation.

Use ruby2_keywords to handle argument delegation in the delegate library.
This commit is contained in:
Jeremy Evans 2019-09-21 09:03:36 -07:00
Родитель 80b5a0ff2a
Коммит 3b302ea8c9
8 изменённых файлов: 450 добавлений и 28 удалений

Просмотреть файл

@ -815,6 +815,7 @@ struct RComplex {
#define RCOMPLEX_SET_IMAG(cmp, i) RB_OBJ_WRITE((cmp), &((struct RComplex *)(cmp))->imag,(i))
enum ruby_rhash_flags {
RHASH_PASS_AS_KEYWORDS = FL_USER1, /* FL 1 */
RHASH_PROC_DEFAULT = FL_USER2, /* FL 2 */
RHASH_ST_TABLE_FLAG = FL_USER3, /* FL 3 */
#define RHASH_AR_TABLE_MAX_SIZE SIZEOF_VALUE

Просмотреть файл

@ -75,7 +75,7 @@ class Delegator < BasicObject
#
# Handles the magic of delegation through \_\_getobj\_\_.
#
def method_missing(m, *args, &block)
ruby2_keywords def method_missing(m, *args, &block)
r = true
target = self.__getobj__ {r = false}

Просмотреть файл

@ -2306,6 +2306,278 @@ class TestKeywordArguments < Test::Unit::TestCase
assert_raise(ArgumentError) { m.call(42, a: 1, **h2) }
end
def test_ruby2_keywords
c = Class.new do
ruby2_keywords def foo(meth, *args)
send(meth, *args)
end
ruby2_keywords def foo_bar(*args)
bar(*args)
end
ruby2_keywords def foo_baz(*args)
baz(*args)
end
ruby2_keywords def foo_mod(meth, *args)
args << 1
send(meth, *args)
end
ruby2_keywords def foo_bar_mod(*args)
args << 1
bar(*args)
end
ruby2_keywords def foo_baz_mod(*args)
args << 1
baz(*args)
end
def bar(*args, **kw)
[args, kw]
end
def baz(*args)
args
end
ruby2_keywords def foo_dbar(*args)
dbar(*args)
end
ruby2_keywords def foo_dbaz(*args)
dbaz(*args)
end
define_method(:dbar) do |*args, **kw|
[args, kw]
end
define_method(:dbaz) do |*args|
args
end
ruby2_keywords def block(*args)
->(*args, **kw){[args, kw]}.(*args)
end
ruby2_keywords def cfunc(*args)
self.class.new(*args).init_args
end
ruby2_keywords def store_foo(meth, *args)
@stored_args = args
use(meth)
end
def use(meth)
send(meth, *@stored_args)
end
attr_reader :init_args
def initialize(*args, **kw)
@init_args = [args, kw]
end
end
mmkw = Class.new do
def method_missing(*args, **kw)
[args, kw]
end
end
mmnokw = Class.new do
def method_missing(*args)
args
end
end
implicit_super = Class.new(c) do
ruby2_keywords def bar(*args)
super
end
ruby2_keywords def baz(*args)
super
end
end
explicit_super = Class.new(c) do
ruby2_keywords def bar(*args)
super(*args)
end
ruby2_keywords def baz(*args)
super(*args)
end
end
h1 = {a: 1}
o = c.new
assert_equal([[1], h1], o.foo(:bar, 1, :a=>1))
assert_equal([1, h1], o.foo(:baz, 1, :a=>1))
assert_equal([[1], h1], o.store_foo(:bar, 1, :a=>1))
assert_equal([1, h1], o.store_foo(:baz, 1, :a=>1))
assert_equal([[1], h1], o.foo_bar(1, :a=>1))
assert_equal([1, h1], o.foo_baz(1, :a=>1))
assert_equal([[1], h1], o.foo(:bar, 1, **h1))
assert_equal([1, h1], o.foo(:baz, 1, **h1))
assert_equal([[1], h1], o.store_foo(:bar, 1, **h1))
assert_equal([1, h1], o.store_foo(:baz, 1, **h1))
assert_equal([[1], h1], o.foo_bar(1, **h1))
assert_equal([1, h1], o.foo_baz(1, **h1))
assert_equal([[h1], {}], o.foo(:bar, h1, **{}))
assert_equal([h1], o.foo(:baz, h1, **{}))
assert_equal([[h1], {}], o.store_foo(:bar, h1, **{}))
assert_equal([h1], o.store_foo(:baz, h1, **{}))
assert_equal([[h1], {}], o.foo_bar(h1, **{}))
assert_equal([h1], o.foo_baz(h1, **{}))
assert_warn(/The last argument is used as the keyword parameter.* for `bar'/m) do
assert_equal([[1], h1], o.foo(:bar, 1, h1))
end
assert_equal([1, h1], o.foo(:baz, 1, h1))
assert_warn(/The last argument is used as the keyword parameter.* for `bar'/m) do
assert_equal([[1], h1], o.store_foo(:bar, 1, h1))
end
assert_equal([1, h1], o.store_foo(:baz, 1, h1))
assert_warn(/The last argument is used as the keyword parameter.* for `bar'/m) do
assert_equal([[1], h1], o.foo_bar(1, h1))
end
assert_equal([1, h1], o.foo_baz(1, h1))
assert_equal([[1, h1, 1], {}], o.foo_mod(:bar, 1, :a=>1))
assert_equal([1, h1, 1], o.foo_mod(:baz, 1, :a=>1))
assert_equal([[1, h1, 1], {}], o.foo_bar_mod(1, :a=>1))
assert_equal([1, h1, 1], o.foo_baz_mod(1, :a=>1))
assert_equal([[1, h1, 1], {}], o.foo_mod(:bar, 1, **h1))
assert_equal([1, h1, 1], o.foo_mod(:baz, 1, **h1))
assert_equal([[1, h1, 1], {}], o.foo_bar_mod(1, **h1))
assert_equal([1, h1, 1], o.foo_baz_mod(1, **h1))
assert_equal([[h1, {}, 1], {}], o.foo_mod(:bar, h1, **{}))
assert_equal([h1, {}, 1], o.foo_mod(:baz, h1, **{}))
assert_equal([[h1, {}, 1], {}], o.foo_bar_mod(h1, **{}))
assert_equal([h1, {}, 1], o.foo_baz_mod(h1, **{}))
assert_equal([[1, h1, 1], {}], o.foo_mod(:bar, 1, h1))
assert_equal([1, h1, 1], o.foo_mod(:baz, 1, h1))
assert_equal([[1, h1, 1], {}], o.foo_bar_mod(1, h1))
assert_equal([1, h1, 1], o.foo_baz_mod(1, h1))
assert_equal([[1], h1], o.foo(:dbar, 1, :a=>1))
assert_equal([1, h1], o.foo(:dbaz, 1, :a=>1))
assert_equal([[1], h1], o.store_foo(:dbar, 1, :a=>1))
assert_equal([1, h1], o.store_foo(:dbaz, 1, :a=>1))
assert_equal([[1], h1], o.foo_dbar(1, :a=>1))
assert_equal([1, h1], o.foo_dbaz(1, :a=>1))
assert_equal([[1], h1], o.foo(:dbar, 1, **h1))
assert_equal([1, h1], o.foo(:dbaz, 1, **h1))
assert_equal([[1], h1], o.store_foo(:dbar, 1, **h1))
assert_equal([1, h1], o.store_foo(:dbaz, 1, **h1))
assert_equal([[1], h1], o.foo_dbar(1, **h1))
assert_equal([1, h1], o.foo_dbaz(1, **h1))
assert_equal([[h1], {}], o.foo(:dbar, h1, **{}))
assert_equal([h1], o.foo(:dbaz, h1, **{}))
assert_equal([[h1], {}], o.store_foo(:dbar, h1, **{}))
assert_equal([h1], o.store_foo(:dbaz, h1, **{}))
assert_equal([[h1], {}], o.foo_dbar(h1, **{}))
assert_equal([h1], o.foo_dbaz(h1, **{}))
assert_warn(/The last argument is used as the keyword parameter.* for method/m) do
assert_equal([[1], h1], o.foo(:dbar, 1, h1))
end
assert_equal([1, h1], o.foo(:dbaz, 1, h1))
assert_warn(/The last argument is used as the keyword parameter.* for method/m) do
assert_equal([[1], h1], o.store_foo(:dbar, 1, h1))
end
assert_equal([1, h1], o.store_foo(:dbaz, 1, h1))
assert_warn(/The last argument is used as the keyword parameter.* for method/m) do
assert_equal([[1], h1], o.foo_dbar(1, h1))
end
assert_equal([1, h1], o.foo_dbaz(1, h1))
assert_equal([[1], h1], o.block(1, :a=>1))
assert_equal([[1], h1], o.block(1, **h1))
assert_warn(/The last argument is used as the keyword parameter.* for `call'/m) do
assert_equal([[1], h1], o.block(1, h1))
end
assert_equal([[h1], {}], o.block(h1, **{}))
assert_equal([[1], h1], o.cfunc(1, :a=>1))
assert_equal([[1], h1], o.cfunc(1, **h1))
assert_warn(/The last argument is used as the keyword parameter.* for `initialize'/m) do
assert_equal([[1], h1], o.cfunc(1, h1))
end
assert_equal([[h1], {}], o.cfunc(h1, **{}))
o = mmkw.new
assert_equal([[:b, 1], h1], o.b(1, :a=>1))
assert_equal([[:b, 1], h1], o.b(1, **h1))
assert_warn(/The last argument is used as the keyword parameter.* for `method_missing'/m) do
assert_equal([[:b, 1], h1], o.b(1, h1))
end
assert_equal([[:b, h1], {}], o.b(h1, **{}))
o = mmnokw.new
assert_equal([:b, 1, h1], o.b(1, :a=>1))
assert_equal([:b, 1, h1], o.b(1, **h1))
assert_equal([:b, 1, h1], o.b(1, h1))
assert_equal([:b, h1], o.b(h1, **{}))
o = implicit_super.new
assert_equal([[1], h1], o.bar(1, :a=>1))
assert_equal([[1], h1], o.bar(1, **h1))
assert_warn(/The last argument is used as the keyword parameter.* for `bar'/m) do
assert_equal([[1], h1], o.bar(1, h1))
end
assert_equal([[h1], {}], o.bar(h1, **{}))
assert_equal([1, h1], o.baz(1, :a=>1))
assert_equal([1, h1], o.baz(1, **h1))
assert_equal([1, h1], o.baz(1, h1))
assert_equal([h1], o.baz(h1, **{}))
o = explicit_super.new
assert_equal([[1], h1], o.bar(1, :a=>1))
assert_equal([[1], h1], o.bar(1, **h1))
assert_warn(/The last argument is used as the keyword parameter.* for `bar'/m) do
assert_equal([[1], h1], o.bar(1, h1))
end
assert_equal([[h1], {}], o.bar(h1, **{}))
assert_equal([1, h1], o.baz(1, :a=>1))
assert_equal([1, h1], o.baz(1, **h1))
assert_equal([1, h1], o.baz(1, h1))
assert_equal([h1], o.baz(h1, **{}))
assert_warn(/Skipping set of ruby2_keywords flag for bar \(method not defined in Ruby, method accepts keywords, or method does not accept argument splat\)/) do
assert_nil(c.send(:ruby2_keywords, :bar))
end
sc = Class.new(c)
assert_warn(/Skipping set of ruby2_keywords flag for bar \(can only set in method defining module\)/) do
sc.send(:ruby2_keywords, :bar)
end
m = Module.new
assert_warn(/Skipping set of ruby2_keywords flag for system \(can only set in method defining module\)/) do
m.send(:ruby2_keywords, :system)
end
assert_raise(NameError) { c.send(:ruby2_keywords, "a5e36ccec4f5080a1d5e63f8") }
assert_raise(NameError) { c.send(:ruby2_keywords, :quux) }
c.freeze
assert_raise(FrozenError) { c.send(:ruby2_keywords, :baz) }
end
def test_dig_kwsplat
kw = {}
h = {:a=>1}

Просмотреть файл

@ -177,6 +177,25 @@ class TestDelegateClass < Test::Unit::TestCase
assert_not_operator(s0, :eql?, "bar")
end
def test_keyword_and_hash
foo = Object.new
def foo.bar(*args)
args
end
def foo.foo(*args, **kw)
[args, kw]
end
d = SimpleDelegator.new(foo)
assert_equal([[], {}], d.foo)
assert_equal([], d.bar)
assert_equal([[], {:a=>1}], d.foo(:a=>1))
assert_equal([{:a=>1}], d.bar(:a=>1))
assert_warn(/The last argument is used as the keyword parameter.* for `foo'/m) do
assert_equal([[], {:a=>1}], d.foo({:a=>1}))
end
assert_equal([{:a=>1}], d.bar({:a=>1}))
end
class Foo
private
def delegate_test_private

Просмотреть файл

@ -671,6 +671,8 @@ setup_parameters_complex(rb_execution_context_t * const ec, const rb_iseq_t * co
VALUE keyword_hash = Qnil;
VALUE * const orig_sp = ec->cfp->sp;
unsigned int i;
int remove_empty_keyword_hash = 1;
VALUE flag_keyword_hash = 0;
vm_check_canary(ec, orig_sp);
/*
@ -720,41 +722,79 @@ setup_parameters_complex(rb_execution_context_t * const ec, const rb_iseq_t * co
args->kw_argv = NULL;
}
if (kw_flag && iseq->body->param.flags.ruby2_keywords) {
remove_empty_keyword_hash = 0;
}
if (ci->flag & VM_CALL_ARGS_SPLAT) {
VALUE rest_last = 0;
int len;
args->rest = locals[--args->argc];
args->rest_index = 0;
given_argc += RARRAY_LENINT(args->rest) - 1;
len = RARRAY_LENINT(args->rest);
given_argc += len - 1;
rest_last = RARRAY_AREF(args->rest, len - 1);
if (!kw_flag && len > 0) {
if (RB_TYPE_P(rest_last, T_HASH) &&
(((struct RHash *)rest_last)->basic.flags & RHASH_PASS_AS_KEYWORDS)) {
kw_flag |= VM_CALL_KW_SPLAT;
} else {
rest_last = 0;
}
}
if (kw_flag & VM_CALL_KW_SPLAT) {
int len = RARRAY_LENINT(args->rest);
if (len > 0 && ignore_keyword_hash_p(RARRAY_AREF(args->rest, len - 1), iseq)) {
if (given_argc != min_argc) {
arg_rest_dup(args);
rb_ary_pop(args->rest);
given_argc--;
kw_flag &= ~VM_CALL_KW_SPLAT;
if (remove_empty_keyword_hash) {
arg_rest_dup(args);
rb_ary_pop(args->rest);
given_argc--;
kw_flag &= ~VM_CALL_KW_SPLAT;
}
else {
flag_keyword_hash = rest_last;
}
}
else {
rb_warn_keyword_to_last_hash(calling, ci, iseq);
}
}
else if (!remove_empty_keyword_hash && rest_last) {
flag_keyword_hash = rest_last;
}
}
}
else {
if (kw_flag & VM_CALL_KW_SPLAT) {
if (ignore_keyword_hash_p(args->argv[args->argc-1], iseq)) {
VALUE last_arg = args->argv[args->argc-1];
if (ignore_keyword_hash_p(last_arg, iseq)) {
if (given_argc != min_argc) {
args->argc--;
given_argc--;
kw_flag &= ~VM_CALL_KW_SPLAT;
if (remove_empty_keyword_hash) {
args->argc--;
given_argc--;
kw_flag &= ~VM_CALL_KW_SPLAT;
}
else {
flag_keyword_hash = last_arg;
}
}
else {
rb_warn_keyword_to_last_hash(calling, ci, iseq);
}
}
else if (!remove_empty_keyword_hash) {
flag_keyword_hash = args->argv[args->argc-1];
}
}
args->rest = Qfalse;
}
if (flag_keyword_hash && RB_TYPE_P(flag_keyword_hash, T_HASH)) {
((struct RHash *)flag_keyword_hash)->basic.flags |= RHASH_PASS_AS_KEYWORDS;
}
if (kw_flag && iseq->body->param.flags.accepts_no_kwarg) {
rb_raise(rb_eArgError, "no keywords accepted");
}

Просмотреть файл

@ -358,6 +358,7 @@ struct rb_iseq_constant_body {
unsigned int ambiguous_param0 : 1; /* {|a|} */
unsigned int accepts_no_kwarg : 1;
unsigned int ruby2_keywords: 1;
} flags;
unsigned int size;

Просмотреть файл

@ -1770,15 +1770,21 @@ rb_iseq_only_kwparam_p(const rb_iseq_t *iseq)
static inline void
CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(struct rb_control_frame_struct *restrict cfp,
struct rb_calling_info *restrict calling,
const struct rb_call_info *restrict ci)
CALLER_SETUP_ARG(struct rb_control_frame_struct *restrict cfp,
struct rb_calling_info *restrict calling,
const struct rb_call_info *restrict ci)
{
if (UNLIKELY(IS_ARGS_SPLAT(ci))) {
/* This expands the rest argument to the stack.
* So, ci->flag & VM_CALL_ARGS_SPLAT is now inconsistent.
*/
vm_caller_setup_arg_splat(cfp, calling);
if (!IS_ARGS_KW_OR_KW_SPLAT(ci) &&
calling->argc > 0 &&
RB_TYPE_P(*(cfp->sp - 1), T_HASH) &&
(((struct RHash *)*(cfp->sp - 1))->basic.flags & RHASH_PASS_AS_KEYWORDS)) {
calling->kw_splat = 1;
}
}
if (UNLIKELY(IS_ARGS_KEYWORD(ci))) {
/* This converts VM_CALL_KWARG style to VM_CALL_KW_SPLAT style
@ -1790,12 +1796,10 @@ CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(struct rb_control_frame_struct *restrict cfp,
}
static inline void
CALLER_SETUP_ARG(struct rb_control_frame_struct *restrict cfp,
struct rb_calling_info *restrict calling,
const struct rb_call_info *restrict ci)
CALLER_REMOVE_EMPTY_KW_SPLAT(struct rb_control_frame_struct *restrict cfp,
struct rb_calling_info *restrict calling,
const struct rb_call_info *restrict ci)
{
CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(cfp, calling, ci);
if (UNLIKELY(calling->kw_splat)) {
/* This removes the last Hash object if it is empty.
* So, ci->flag & VM_CALL_KW_SPLAT is now inconsistent.
@ -1920,6 +1924,7 @@ vm_callee_setup_arg(rb_execution_context_t *ec, struct rb_calling_info *calling,
if (LIKELY(rb_simple_iseq_p(iseq))) {
rb_control_frame_t *cfp = ec->cfp;
CALLER_SETUP_ARG(cfp, calling, ci);
CALLER_REMOVE_EMPTY_KW_SPLAT(cfp, calling, ci);
if (calling->argc != iseq->body->param.lead_num) {
argument_arity_error(ec, iseq, calling->argc, iseq->body->param.lead_num, iseq->body->param.lead_num);
@ -1931,6 +1936,7 @@ vm_callee_setup_arg(rb_execution_context_t *ec, struct rb_calling_info *calling,
else if (rb_iseq_only_optparam_p(iseq)) {
rb_control_frame_t *cfp = ec->cfp;
CALLER_SETUP_ARG(cfp, calling, ci);
CALLER_REMOVE_EMPTY_KW_SPLAT(cfp, calling, ci);
const int lead_num = iseq->body->param.lead_num;
const int opt_num = iseq->body->param.opt_num;
@ -2285,10 +2291,12 @@ vm_call_cfunc_with_frame(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp
static VALUE
vm_call_cfunc(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc)
{
int empty_kw_splat = calling->kw_splat;
int empty_kw_splat;
RB_DEBUG_COUNTER_INC(ccf_cfunc);
CALLER_SETUP_ARG(reg_cfp, calling, ci);
empty_kw_splat = calling->kw_splat;
CALLER_REMOVE_EMPTY_KW_SPLAT(reg_cfp, calling, ci);
if (empty_kw_splat && calling->kw_splat) {
empty_kw_splat = 0;
}
@ -2333,7 +2341,7 @@ vm_call_bmethod(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_c
VALUE *argv;
int argc;
CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(cfp, calling, ci);
CALLER_SETUP_ARG(cfp, calling, ci);
argc = calling->argc;
argv = ALLOCA_N(VALUE, argc);
MEMCPY(argv, cfp->sp - argc, VALUE, argc);
@ -2363,7 +2371,7 @@ vm_call_opt_send(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct
struct rb_call_info_with_kwarg ci_entry;
struct rb_call_cache cc_entry, *cc;
CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(reg_cfp, calling, orig_ci);
CALLER_SETUP_ARG(reg_cfp, calling, orig_ci);
i = calling->argc - 1;
@ -2468,7 +2476,7 @@ vm_call_method_missing(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp,
struct rb_call_cache cc_entry, *cc;
unsigned int argc;
CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(reg_cfp, calling, orig_ci);
CALLER_SETUP_ARG(reg_cfp, calling, orig_ci);
argc = calling->argc+1;
ci_entry.flag = VM_CALL_FCALL | VM_CALL_OPT_SEND | (calling->kw_splat ? VM_CALL_KW_SPLAT : 0);
@ -2673,12 +2681,12 @@ vm_call_method_each_type(rb_execution_context_t *ec, rb_control_frame_t *cfp, st
return vm_call_cfunc(ec, cfp, calling, ci, cc);
case VM_METHOD_TYPE_ATTRSET:
CALLER_SETUP_ARG(cfp, calling, ci);
if (calling->argc == 1 && calling->kw_splat && RHASH_EMPTY_P(cfp->sp[-1])) {
CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(cfp, calling, ci);
rb_warn_keyword_to_last_hash(calling, ci, NULL);
}
else {
CALLER_SETUP_ARG(cfp, calling, ci);
CALLER_REMOVE_EMPTY_KW_SPLAT(cfp, calling, ci);
}
rb_check_arity(calling->argc, 1, 1);
@ -2688,6 +2696,7 @@ vm_call_method_each_type(rb_execution_context_t *ec, rb_control_frame_t *cfp, st
case VM_METHOD_TYPE_IVAR:
CALLER_SETUP_ARG(cfp, calling, ci);
CALLER_REMOVE_EMPTY_KW_SPLAT(cfp, calling, ci);
rb_check_arity(calling->argc, 0, 0);
cc->aux.index = 0;
CC_SET_FASTPATH(cc, vm_call_ivar, !(ci->flag & VM_CALL_ARGS_SPLAT));
@ -2998,12 +3007,12 @@ vm_callee_setup_block_arg(rb_execution_context_t *ec, struct rb_calling_info *ca
rb_control_frame_t *cfp = ec->cfp;
VALUE arg0;
CALLER_SETUP_ARG(cfp, calling, ci);
if (calling->kw_splat && calling->argc == iseq->body->param.lead_num + iseq->body->param.post_num && RHASH_EMPTY_P(cfp->sp[-1])) {
CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(cfp, calling, ci);
rb_warn_keyword_to_last_hash(calling, ci, iseq);
}
else {
CALLER_SETUP_ARG(cfp, calling, ci);
CALLER_REMOVE_EMPTY_KW_SPLAT(cfp, calling, ci);
}
if (arg_setup_type == arg_setup_block &&
@ -3088,7 +3097,7 @@ vm_invoke_symbol_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp,
{
VALUE val;
int argc;
CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(ec->cfp, calling, ci);
CALLER_SETUP_ARG(ec->cfp, calling, ci);
argc = calling->argc;
val = vm_yield_with_symbol(ec, symbol, argc, STACK_ADDR_FROM_TOP(argc), calling->kw_splat, calling->block_handler);
POPN(argc);
@ -3104,6 +3113,7 @@ vm_invoke_ifunc_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp,
int argc;
int kw_splat = calling->kw_splat;
CALLER_SETUP_ARG(ec->cfp, calling, ci);
CALLER_REMOVE_EMPTY_KW_SPLAT(ec->cfp, calling, ci);
if (kw_splat && !calling->kw_splat) {
kw_splat = 2;
}

Просмотреть файл

@ -1745,6 +1745,84 @@ rb_mod_private(int argc, VALUE *argv, VALUE module)
return set_visibility(argc, argv, module, METHOD_VISI_PRIVATE);
}
/*
* call-seq:
* ruby2_keywords(method_name, ...) -> self
*
* For the given method names, marks the method as passing keywords through
* a normal argument splat. This should only be called on methods that
* accept an argument splat (<tt>*args</tt>) but not explicit keywords or
* a keyword splat. It marks the method such that if the method is called
* with keyword arguments, the final hash argument is marked with a special
* flag such that if it is the final element of a normal argument splat to
* another method call, and that method calls does not include explicit
* keywords or a keyword splat, the final element is interpreted as keywords.
* In other words, keywords will be passed through the method to other
* methods.
*
* This should only be used for methods that delegate keywords to another
* method, and only for backwards compatibility with Ruby versions before
* 2.7.
*
* This method will probably be removed at some point, as it exists only
* for backwards compatibility, so always check that the module responds
* to this method before calling it.
*
* module Mod
* def foo(meth, *args, &block)
* send(:"do_#{meth}", *args, &block)
* end
* ruby2_keywords(:foo) if respond_to?(:ruby2_keywords, true)
* end
*/
static VALUE
rb_mod_ruby2_keywords(int argc, VALUE *argv, VALUE module)
{
int i;
VALUE origin_class = RCLASS_ORIGIN(module);
rb_check_frozen(module);
for (i = 0; i < argc; i++) {
VALUE v = argv[i];
ID name = rb_check_id(&v);
rb_method_entry_t *me;
VALUE defined_class;
if (!name) {
rb_print_undef_str(module, v);
}
me = search_method(origin_class, name, &defined_class);
if (!me && RB_TYPE_P(module, T_MODULE)) {
me = search_method(rb_cObject, name, &defined_class);
}
if (UNDEFINED_METHOD_ENTRY_P(me) ||
UNDEFINED_REFINED_METHOD_P(me->def)) {
rb_print_undef(module, name, METHOD_VISI_UNDEF);
}
if (module == defined_class || origin_class == defined_class) {
if (me->def->type == VM_METHOD_TYPE_ISEQ &&
me->def->body.iseq.iseqptr->body->param.flags.has_rest &&
!me->def->body.iseq.iseqptr->body->param.flags.has_kw &&
!me->def->body.iseq.iseqptr->body->param.flags.has_kwrest) {
me->def->body.iseq.iseqptr->body->param.flags.ruby2_keywords = 1;
rb_clear_method_cache_by_class(module);
}
else {
rb_warn("Skipping set of ruby2_keywords flag for %s (method not defined in Ruby, method accepts keywords, or method does not accept argument splat)", rb_id2name(name));
}
}
else {
rb_warn("Skipping set of ruby2_keywords flag for %s (can only set in method defining module)", rb_id2name(name));
}
}
return Qnil;
}
/*
* call-seq:
* mod.public_class_method(symbol, ...) -> mod
@ -2127,6 +2205,7 @@ Init_eval_method(void)
rb_define_private_method(rb_cModule, "protected", rb_mod_protected, -1);
rb_define_private_method(rb_cModule, "private", rb_mod_private, -1);
rb_define_private_method(rb_cModule, "module_function", rb_mod_modfunc, -1);
rb_define_private_method(rb_cModule, "ruby2_keywords", rb_mod_ruby2_keywords, -1);
rb_define_method(rb_cModule, "method_defined?", rb_mod_method_defined, -1);
rb_define_method(rb_cModule, "public_method_defined?", rb_mod_public_method_defined, -1);