diff options
author | Jeremy Evans <code@jeremyevans.net> | 2019-09-21 09:03:36 -0700 |
---|---|---|
committer | Jeremy Evans <code@jeremyevans.net> | 2019-09-25 12:33:52 -0700 |
commit | 3b302ea8c95d34d5ef072d7e3b326f28a611e479 (patch) | |
tree | 5a0a5cadb3511d6a3ecf4f234abffecafbeec9d8 /vm_method.c | |
parent | 80b5a0ff2a7709367178f29d4ebe1c54122b1c27 (diff) |
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.
Notes
Notes:
Merged: https://github.com/ruby/ruby/pull/2477
Diffstat (limited to 'vm_method.c')
-rw-r--r-- | vm_method.c | 79 |
1 files changed, 79 insertions, 0 deletions
diff --git a/vm_method.c b/vm_method.c index a3d56c8baf..554d209110 100644 --- a/vm_method.c +++ b/vm_method.c @@ -1747,6 +1747,84 @@ rb_mod_private(int argc, VALUE *argv, VALUE module) /* * 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 * mod.public_class_method(string, ...) -> 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); |