summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeremy Evans <code@jeremyevans.net>2019-09-18 12:08:14 -0700
committerJeremy Evans <code@jeremyevans.net>2019-09-20 07:45:11 -0700
commit27b67468724dc48ed8305d8cb33484a4af98fc05 (patch)
treeed3a664dc2f2a7f729894a3d253171ce19b7f25b
parente81a3e6df54842b5a836dad7055a4295cf4155bc (diff)
Make passing empty keywords to dig pass empty keywords to next dig method
If defined in Ruby, dig would be defined as def dig(arg, *rest) end, it would not use keywords. If the last dig argument was an empty hash, it could be treated as keyword arguments by the next dig method. Allow dig to pass along the empty keyword flag if called with an empty keyword, to suppress the previous behavior and force treating the hash as a positional argument and not keywords. Also handle the case where dig calls method_missing, passing the empty keyword flag to that as well. This requires adding rb_check_funcall_with_hook_kw functions, so that dig can specify how arguments are treated. It also adds kw_splat arguments to a couple static functions.
-rw-r--r--internal.h2
-rw-r--r--object.c7
-rw-r--r--test/ruby/test_keyword.rb220
-rw-r--r--vm_eval.c31
-rw-r--r--vm_method.c12
5 files changed, 256 insertions, 16 deletions
diff --git a/internal.h b/internal.h
index 2e8d23b085..628cfd9912 100644
--- a/internal.h
+++ b/internal.h
@@ -2301,6 +2301,8 @@ VALUE rb_check_block_call(VALUE, ID, int, const VALUE *, rb_block_call_func_t, V
typedef void rb_check_funcall_hook(int, VALUE, ID, int, const VALUE *, VALUE);
VALUE rb_check_funcall_with_hook(VALUE recv, ID mid, int argc, const VALUE *argv,
rb_check_funcall_hook *hook, VALUE arg);
+VALUE rb_check_funcall_with_hook_kw(VALUE recv, ID mid, int argc, const VALUE *argv,
+ rb_check_funcall_hook *hook, VALUE arg, int kw_splat);
const char *rb_type_str(enum ruby_value_type type);
VALUE rb_check_funcall_default(VALUE, ID, int, const VALUE *, VALUE);
VALUE rb_yield_1(VALUE val);
diff --git a/object.c b/object.c
index de2d72fae3..9c603a8f10 100644
--- a/object.c
+++ b/object.c
@@ -4071,8 +4071,11 @@ rb_obj_dig(int argc, VALUE *argv, VALUE obj, VALUE notfound)
break;
}
}
- return rb_check_funcall_with_hook(obj, id_dig, argc, argv,
- no_dig_method, obj);
+ return rb_check_funcall_with_hook_kw(obj, id_dig, argc, argv,
+ no_dig_method, obj,
+ rb_empty_keyword_given_p() ?
+ RB_PASS_EMPTY_KEYWORDS :
+ RB_NO_KEYWORDS);
}
return obj;
}
diff --git a/test/ruby/test_keyword.rb b/test/ruby/test_keyword.rb
index a9e2b1f1c3..0b81179817 100644
--- a/test/ruby/test_keyword.rb
+++ b/test/ruby/test_keyword.rb
@@ -2204,6 +2204,226 @@ class TestKeywordArguments < Test::Unit::TestCase
assert_raise(ArgumentError) { m.call(42, a: 1, **h2) }
end
+ def test_dig_kwsplat
+ kw = {}
+ h = {:a=>1}
+ h2 = {'a'=>1}
+ h3 = {'a'=>1, :a=>1}
+
+ c = Object.new
+ def c.dig(*args)
+ args
+ end
+ assert_equal(c, [c].dig(0, **{}))
+ assert_equal(c, [c].dig(0, **kw))
+ assert_equal([h], [c].dig(0, **h))
+ assert_equal([h], [c].dig(0, a: 1))
+ assert_equal([h2], [c].dig(0, **h2))
+ assert_equal([h3], [c].dig(0, **h3))
+ assert_equal([h3], [c].dig(0, a: 1, **h2))
+
+ c.singleton_class.remove_method(:dig)
+ def c.dig; end
+ assert_equal(c, [c].dig(0, **{}))
+ assert_equal(c, [c].dig(0, **kw))
+ assert_raise(ArgumentError) { [c].dig(0, **h) }
+ assert_raise(ArgumentError) { [c].dig(0, a: 1) }
+ assert_raise(ArgumentError) { [c].dig(0, **h2) }
+ assert_raise(ArgumentError) { [c].dig(0, **h3) }
+ assert_raise(ArgumentError) { [c].dig(0, a: 1, **h2) }
+
+ c.singleton_class.remove_method(:dig)
+ def c.dig(args)
+ args
+ end
+ assert_equal(c, [c].dig(0, **{}))
+ assert_equal(c, [c].dig(0, **kw))
+ assert_equal(kw, [c].dig(0, kw, **kw))
+ assert_equal(h, [c].dig(0, **h))
+ assert_equal(h, [c].dig(0, a: 1))
+ assert_equal(h2, [c].dig(0, **h2))
+ assert_equal(h3, [c].dig(0, **h3))
+ assert_equal(h3, [c].dig(0, a: 1, **h2))
+
+ c.singleton_class.remove_method(:dig)
+ def c.dig(**args)
+ args
+ end
+ assert_equal(c, [c].dig(0, **{}))
+ assert_equal(c, [c].dig(0, **kw))
+ assert_warn(/The last argument is used as the keyword parameter.*for `dig'/m) do
+ assert_equal(h, [c].dig(0, **h))
+ end
+ assert_warn(/The last argument is used as the keyword parameter.*for `dig'/m) do
+ assert_equal(h, [c].dig(0, a: 1))
+ end
+ assert_warn(/The last argument is split into positional and keyword parameters.*for `dig'/m) do
+ assert_raise(ArgumentError) { [c].dig(0, **h3) }
+ end
+ assert_warn(/The last argument is split into positional and keyword parameters.*for `dig'/m) do
+ assert_raise(ArgumentError) { [c].dig(0, a: 1, **h2) }
+ end
+ assert_warn(/The last argument is used as the keyword parameter.*for `dig'/m) do
+ assert_equal(h, [c].dig(0, h))
+ end
+ assert_raise(ArgumentError) { [c].dig(0, h2) }
+ assert_warn(/The last argument is split into positional and keyword parameters.*for `dig'/m) do
+ assert_raise(ArgumentError) { [c].dig(0, h3) }
+ end
+
+ c.singleton_class.remove_method(:dig)
+ def c.dig(arg, **args)
+ [arg, args]
+ end
+ assert_equal(c, [c].dig(0, **{}))
+ assert_equal(c, [c].dig(0, **kw))
+ assert_equal([h, kw], [c].dig(0, **h))
+ assert_equal([h, kw], [c].dig(0, a: 1))
+ assert_equal([h2, kw], [c].dig(0, **h2))
+ assert_equal([h3, kw], [c].dig(0, **h3))
+ assert_equal([h3, kw], [c].dig(0, a: 1, **h2))
+
+ c.singleton_class.remove_method(:dig)
+ def c.dig(arg=1, **args)
+ [arg, args]
+ end
+ assert_equal(c, [c].dig(0, **{}))
+ assert_equal(c, [c].dig(0, **kw))
+ assert_warn(/The last argument is used as the keyword parameter.*for `dig'/m) do
+ assert_equal([1, h], [c].dig(0, **h))
+ end
+ assert_warn(/The last argument is used as the keyword parameter.*for `dig'/m) do
+ assert_equal([1, h], [c].dig(0, a: 1))
+ end
+ assert_equal([h2, kw], [c].dig(0, **h2))
+ assert_warn(/The last argument is split into positional and keyword parameters.*for `dig'/m) do
+ assert_equal([h2, h], [c].dig(0, **h3))
+ end
+ assert_warn(/The last argument is split into positional and keyword parameters.*for `dig'/m) do
+ assert_equal([h2, h], [c].dig(0, a: 1, **h2))
+ end
+ assert_warn(/The last argument is used as the keyword parameter.*for `dig'/m) do
+ assert_equal([1, h], [c].dig(0, h))
+ end
+ assert_equal([h2, kw], [c].dig(0, h2))
+ assert_warn(/The last argument is split into positional and keyword parameters.*for `dig'/m) do
+ assert_equal([h2, h], [c].dig(0, h3))
+ end
+ assert_equal([h, kw], [c].dig(0, h, **{}))
+ assert_equal([h2, kw], [c].dig(0, h2, **{}))
+ assert_equal([h3, kw], [c].dig(0, h3, **{}))
+ end
+
+ def test_dig_method_missing_kwsplat
+ kw = {}
+ h = {:a=>1}
+ h2 = {'a'=>1}
+ h3 = {'a'=>1, :a=>1}
+
+ c = Object.new
+ def c.method_missing(_, *args)
+ args
+ end
+ assert_equal(c, [c].dig(0, **{}))
+ assert_equal(c, [c].dig(0, **kw))
+ assert_equal([h], [c].dig(0, **h))
+ assert_equal([h], [c].dig(0, a: 1))
+ assert_equal([h2], [c].dig(0, **h2))
+ assert_equal([h3], [c].dig(0, **h3))
+ assert_equal([h3], [c].dig(0, a: 1, **h2))
+
+ c.singleton_class.remove_method(:method_missing)
+ def c.method_missing; end
+ assert_equal(c, [c].dig(0, **{}))
+ assert_equal(c, [c].dig(0, **kw))
+ assert_raise(ArgumentError) { [c].dig(0, **h) }
+ assert_raise(ArgumentError) { [c].dig(0, a: 1) }
+ assert_raise(ArgumentError) { [c].dig(0, **h2) }
+ assert_raise(ArgumentError) { [c].dig(0, **h3) }
+ assert_raise(ArgumentError) { [c].dig(0, a: 1, **h2) }
+
+ c.singleton_class.remove_method(:method_missing)
+ def c.method_missing(_, args)
+ args
+ end
+ assert_equal(c, [c].dig(0, **{}))
+ assert_equal(c, [c].dig(0, **kw))
+ assert_equal(kw, [c].dig(0, kw, **kw))
+ assert_equal(h, [c].dig(0, **h))
+ assert_equal(h, [c].dig(0, a: 1))
+ assert_equal(h2, [c].dig(0, **h2))
+ assert_equal(h3, [c].dig(0, **h3))
+ assert_equal(h3, [c].dig(0, a: 1, **h2))
+
+ c.singleton_class.remove_method(:method_missing)
+ def c.method_missing(_, **args)
+ args
+ end
+ assert_equal(c, [c].dig(0, **{}))
+ assert_equal(c, [c].dig(0, **kw))
+ assert_warn(/The last argument is used as the keyword parameter.*for `method_missing'/m) do
+ assert_equal(h, [c].dig(0, **h))
+ end
+ assert_warn(/The last argument is used as the keyword parameter.*for `method_missing'/m) do
+ assert_equal(h, [c].dig(0, a: 1))
+ end
+ assert_warn(/The last argument is split into positional and keyword parameters.*for `method_missing'/m) do
+ assert_raise(ArgumentError) { [c].dig(0, **h3) }
+ end
+ assert_warn(/The last argument is split into positional and keyword parameters.*for `method_missing'/m) do
+ assert_raise(ArgumentError) { [c].dig(0, a: 1, **h2) }
+ end
+ assert_warn(/The last argument is used as the keyword parameter.*for `method_missing'/m) do
+ assert_equal(h, [c].dig(0, h))
+ end
+ assert_raise(ArgumentError) { [c].dig(0, h2) }
+ assert_warn(/The last argument is split into positional and keyword parameters.*for `method_missing'/m) do
+ assert_raise(ArgumentError) { [c].dig(0, h3) }
+ end
+
+ c.singleton_class.remove_method(:method_missing)
+ def c.method_missing(_, arg, **args)
+ [arg, args]
+ end
+ assert_equal(c, [c].dig(0, **{}))
+ assert_equal(c, [c].dig(0, **kw))
+ assert_equal([h, kw], [c].dig(0, **h))
+ assert_equal([h, kw], [c].dig(0, a: 1))
+ assert_equal([h2, kw], [c].dig(0, **h2))
+ assert_equal([h3, kw], [c].dig(0, **h3))
+ assert_equal([h3, kw], [c].dig(0, a: 1, **h2))
+
+ c.singleton_class.remove_method(:method_missing)
+ def c.method_missing(_, arg=1, **args)
+ [arg, args]
+ end
+ assert_equal(c, [c].dig(0, **{}))
+ assert_equal(c, [c].dig(0, **kw))
+ assert_warn(/The last argument is used as the keyword parameter.*for `method_missing'/m) do
+ assert_equal([1, h], [c].dig(0, **h))
+ end
+ assert_warn(/The last argument is used as the keyword parameter.*for `method_missing'/m) do
+ assert_equal([1, h], [c].dig(0, a: 1))
+ end
+ assert_equal([h2, kw], [c].dig(0, **h2))
+ assert_warn(/The last argument is split into positional and keyword parameters.*for `method_missing'/m) do
+ assert_equal([h2, h], [c].dig(0, **h3))
+ end
+ assert_warn(/The last argument is split into positional and keyword parameters.*for `method_missing'/m) do
+ assert_equal([h2, h], [c].dig(0, a: 1, **h2))
+ end
+ assert_warn(/The last argument is used as the keyword parameter.*for `method_missing'/m) do
+ assert_equal([1, h], [c].dig(0, h))
+ end
+ assert_equal([h2, kw], [c].dig(0, h2))
+ assert_warn(/The last argument is split into positional and keyword parameters.*for `method_missing'/m) do
+ assert_equal([h2, h], [c].dig(0, h3))
+ end
+ assert_equal([h, kw], [c].dig(0, h, **{}))
+ assert_equal([h2, kw], [c].dig(0, h2, **{}))
+ assert_equal([h3, kw], [c].dig(0, h3, **{}))
+ end
+
def p1
Proc.new do |str: "foo", num: 424242|
[str, num]
diff --git a/vm_eval.c b/vm_eval.c
index 706301ebaa..3a0645d28d 100644
--- a/vm_eval.c
+++ b/vm_eval.c
@@ -417,6 +417,7 @@ struct rescue_funcall_args {
unsigned int respond_to_missing: 1;
int argc;
const VALUE *argv;
+ int kw_splat;
};
static VALUE
@@ -425,7 +426,7 @@ check_funcall_exec(VALUE v)
struct rescue_funcall_args *args = (void *)v;
return call_method_entry(args->ec, args->defined_class,
args->recv, idMethodMissing,
- args->me, args->argc, args->argv);
+ args->me, args->argc, args->argv, args->kw_splat);
}
static VALUE
@@ -466,7 +467,7 @@ check_funcall_callable(rb_execution_context_t *ec, const rb_callable_method_entr
}
static VALUE
-check_funcall_missing(rb_execution_context_t *ec, VALUE klass, VALUE recv, ID mid, int argc, const VALUE *argv, int respond, VALUE def)
+check_funcall_missing(rb_execution_context_t *ec, VALUE klass, VALUE recv, ID mid, int argc, const VALUE *argv, int respond, VALUE def, int kw_splat)
{
struct rescue_funcall_args args;
const rb_method_entry_t *me;
@@ -498,6 +499,7 @@ check_funcall_missing(rb_execution_context_t *ec, VALUE klass, VALUE recv, ID mi
args.mid = mid;
args.argc = argc + 1;
args.argv = new_args;
+ args.kw_splat = kw_splat;
ret = rb_rescue2(check_funcall_exec, (VALUE)&args,
check_funcall_failed, (VALUE)&args,
rb_eNoMethodError, (VALUE)0);
@@ -526,19 +528,20 @@ rb_check_funcall_default(VALUE recv, ID mid, int argc, const VALUE *argv, VALUE
me = rb_search_method_entry(recv, mid);
if (!check_funcall_callable(ec, me)) {
VALUE ret = check_funcall_missing(ec, klass, recv, mid, argc, argv,
- respond, def);
+ respond, def, RB_NO_KEYWORDS);
if (ret == Qundef) ret = def;
return ret;
}
stack_check(ec);
- return rb_vm_call0(ec, recv, mid, argc, argv, me, VM_NO_KEYWORDS);
+ return rb_vm_call0(ec, recv, mid, argc, argv, me, RB_NO_KEYWORDS);
}
VALUE
-rb_check_funcall_with_hook(VALUE recv, ID mid, int argc, const VALUE *argv,
- rb_check_funcall_hook *hook, VALUE arg)
+rb_check_funcall_with_hook_kw(VALUE recv, ID mid, int argc, const VALUE *argv,
+ rb_check_funcall_hook *hook, VALUE arg, int kw_splat)
{
VALUE klass = CLASS_OF(recv);
+ VALUE v, ret;
const rb_callable_method_entry_t *me;
rb_execution_context_t *ec = GET_EC();
int respond = check_funcall_respond_to(ec, klass, recv, mid);
@@ -550,14 +553,24 @@ rb_check_funcall_with_hook(VALUE recv, ID mid, int argc, const VALUE *argv,
me = rb_search_method_entry(recv, mid);
if (!check_funcall_callable(ec, me)) {
- VALUE ret = check_funcall_missing(ec, klass, recv, mid, argc, argv,
- respond, Qundef);
+ ret = check_funcall_missing(ec, klass, recv, mid, argc, argv,
+ respond, Qundef, kw_splat);
(*hook)(ret != Qundef, recv, mid, argc, argv, arg);
return ret;
}
stack_check(ec);
(*hook)(TRUE, recv, mid, argc, argv, arg);
- return rb_vm_call0(ec, recv, mid, argc, argv, me, VM_NO_KEYWORDS);
+ v = rb_adjust_argv_kw_splat(&argc, &argv, &kw_splat);
+ ret = rb_vm_call0(ec, recv, mid, argc, argv, me, kw_splat);
+ rb_free_tmp_buffer(&v);
+ return ret;
+}
+
+VALUE
+rb_check_funcall_with_hook(VALUE recv, ID mid, int argc, const VALUE *argv,
+ rb_check_funcall_hook *hook, VALUE arg)
+{
+ return rb_check_funcall_with_hook_kw(recv, mid, argc, argv, hook, arg, RB_NO_KEYWORDS);
}
const char *
diff --git a/vm_method.c b/vm_method.c
index 7421bc96c2..63dc73f94b 100644
--- a/vm_method.c
+++ b/vm_method.c
@@ -1916,12 +1916,14 @@ rb_method_basic_definition_p(VALUE klass, ID id)
static VALUE
call_method_entry(rb_execution_context_t *ec, VALUE defined_class, VALUE obj, ID id,
- const rb_method_entry_t *me, int argc, const VALUE *argv)
+ const rb_method_entry_t *me, int argc, const VALUE *argv, int kw_splat)
{
const rb_callable_method_entry_t *cme =
- prepare_callable_method_entry(defined_class, id, me);
+ prepare_callable_method_entry(defined_class, id, me);
VALUE passed_block_handler = vm_passed_block_handler(ec);
- VALUE result = rb_vm_call0(ec, obj, id, argc, argv, cme, VM_NO_KEYWORDS);
+ VALUE v = rb_adjust_argv_kw_splat(&argc, &argv, &kw_splat);
+ VALUE result = rb_vm_call0(ec, obj, id, argc, argv, cme, kw_splat);
+ rb_free_tmp_buffer(&v);
vm_passed_block_handler_set(ec, passed_block_handler);
return result;
}
@@ -1938,7 +1940,7 @@ basic_obj_respond_to_missing(rb_execution_context_t *ec, VALUE klass, VALUE obj,
if (!me || METHOD_ENTRY_BASIC(me)) return Qundef;
args[0] = mid;
args[1] = priv;
- return call_method_entry(ec, defined_class, obj, rtmid, me, 2, args);
+ return call_method_entry(ec, defined_class, obj, rtmid, me, 2, args, RB_NO_KEYWORDS);
}
static inline int
@@ -2005,7 +2007,7 @@ vm_respond_to(rb_execution_context_t *ec, VALUE klass, VALUE obj, ID id, int pri
}
}
}
- result = call_method_entry(ec, defined_class, obj, resid, me, argc, args);
+ result = call_method_entry(ec, defined_class, obj, resid, me, argc, args, RB_NO_KEYWORDS);
return RTEST(result);
}
}