summaryrefslogtreecommitdiff
path: root/vm_method.c
diff options
context:
space:
mode:
authorJeremy Evans <code@jeremyevans.net>2019-09-21 09:03:36 -0700
committerJeremy Evans <code@jeremyevans.net>2019-09-25 12:33:52 -0700
commit3b302ea8c95d34d5ef072d7e3b326f28a611e479 (patch)
tree5a0a5cadb3511d6a3ecf4f234abffecafbeec9d8 /vm_method.c
parent80b5a0ff2a7709367178f29d4ebe1c54122b1c27 (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.c79
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);