ruby-changes:57845
From: Jeremy <ko1@a...>
Date: Fri, 20 Sep 2019 23:49:24 +0900 (JST)
Subject: [ruby-changes:57845] 27b6746872 (master): Make passing empty keywords to dig pass empty keywords to next dig method
https://git.ruby-lang.org/ruby.git/commit/?id=27b6746872 From 27b67468724dc48ed8305d8cb33484a4af98fc05 Mon Sep 17 00:00:00 2001 From: Jeremy Evans <code@j...> Date: Wed, 18 Sep 2019 12:08:14 -0700 Subject: 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. diff --git a/internal.h b/internal.h index 2e8d23b..628cfd9 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 https://github.com/ruby/ruby/blob/trunk/internal.h#L2301 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 de2d72f..9c603a8 100644 --- a/object.c +++ b/object.c @@ -4071,8 +4071,11 @@ rb_obj_dig(int argc, VALUE *argv, VALUE obj, VALUE notfound) https://github.com/ruby/ruby/blob/trunk/object.c#L4071 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 a9e2b1f..0b81179 100644 --- a/test/ruby/test_keyword.rb +++ b/test/ruby/test_keyword.rb @@ -2204,6 +2204,226 @@ class TestKeywordArguments < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_keyword.rb#L2204 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_mi (... truncated) -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/