From 6a9ce1fea89bc5c6518dd6bb7ff3b824a9321976 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Fri, 19 Apr 2019 22:19:41 +0900 Subject: [PATCH] Support **nil syntax for specifying a method does not accept keyword arguments This syntax means the method should be treated as a method that uses keyword arguments, but no specific keyword arguments are supported, and therefore calling the method with keyword arguments will raise an ArgumentError. It is still allowed to double splat an empty hash when calling the method, as that does not pass any keyword arguments. --- compile.c | 3 +++ node.h | 1 + parse.y | 14 ++++++++++++++ test/ruby/test_keyword.rb | 28 ++++++++++++++++++++++++++++ test/ruby/test_syntax.rb | 16 ++++++++++++++++ vm_args.c | 4 ++++ vm_core.h | 1 + 7 files changed, 67 insertions(+) diff --git a/compile.c b/compile.c index 63b893d7de..d11e2f9f32 100644 --- a/compile.c +++ b/compile.c @@ -1700,6 +1700,9 @@ iseq_set_arguments(rb_iseq_t *iseq, LINK_ANCHOR *const optargs, const NODE *cons body->param.keyword = keyword; body->param.flags.has_kwrest = TRUE; } + else if (args->no_kwarg) { + body->param.flags.accepts_no_kwarg = TRUE; + } if (block_id) { body->param.block_start = arg_size++; diff --git a/node.h b/node.h index 3741e649ca..3b565fc083 100644 --- a/node.h +++ b/node.h @@ -450,6 +450,7 @@ struct rb_args_info { NODE *kw_rest_arg; NODE *opt_args; + int no_kwarg; }; struct rb_ary_pattern_info { diff --git a/parse.y b/parse.y index 4dfdd5d2a3..c7c43e3278 100644 --- a/parse.y +++ b/parse.y @@ -3245,6 +3245,10 @@ block_args_tail : f_block_kwarg ',' f_kwrest opt_f_block_arg { $$ = new_args_tail(p, Qnone, $1, $2, &@1); } + | f_no_kwarg opt_f_block_arg + { + $$ = new_args_tail(p, Qnone, rb_intern("nil"), $2, &@1); + } | f_block_arg { $$ = new_args_tail(p, Qnone, Qnone, $1, &@1); @@ -4712,6 +4716,10 @@ args_tail : f_kwarg ',' f_kwrest opt_f_block_arg { $$ = new_args_tail(p, Qnone, $1, $2, &@1); } + | f_no_kwarg opt_f_block_arg + { + $$ = new_args_tail(p, Qnone, rb_intern("nil"), $2, &@1); + } | f_block_arg { $$ = new_args_tail(p, Qnone, Qnone, $1, &@1); @@ -4968,6 +4976,9 @@ kwrest_mark : tPOW | tDSTAR ; +f_no_kwarg : kwrest_mark keyword_nil + ; + f_kwrest : kwrest_mark tIDENTIFIER { arg_var(p, shadowing_lvar(p, get_id($2))); @@ -11125,6 +11136,9 @@ new_args_tail(struct parser_params *p, NODE *kw_args, ID kw_rest_arg, ID block, args->kw_rest_arg = NEW_DVAR(kw_rest_arg, loc); args->kw_rest_arg->nd_cflag = kw_bits; } + else if (kw_rest_arg == rb_intern("nil")) { + args->no_kwarg = 1; + } else if (kw_rest_arg) { args->kw_rest_arg = NEW_DVAR(kw_rest_arg, loc); } diff --git a/test/ruby/test_keyword.rb b/test/ruby/test_keyword.rb index 791d60b70a..1e707170fd 100644 --- a/test/ruby/test_keyword.rb +++ b/test/ruby/test_keyword.rb @@ -126,6 +126,34 @@ class TestKeywordArguments < Test::Unit::TestCase assert_equal(1, f10(b: 42)) end + def f11(**nil) + local_variables + end + + def test_f11 + h = {} + + assert_equal([], f11) + assert_equal([], f11(**{})) + assert_equal([], f11(**h)) + end + + def f12(**nil, &b) + [b, local_variables] + end + + def test_f12 + h = {} + b = proc{} + + assert_equal([nil, [:b]], f12) + assert_equal([nil, [:b]], f12(**{})) + assert_equal([nil, [:b]], f12(**h)) + assert_equal([b, [:b]], f12(&b)) + assert_equal([b, [:b]], f12(**{}, &b)) + assert_equal([b, [:b]], f12(**h, &b)) + end + def test_method_parameters assert_equal([[:key, :str], [:key, :num]], method(:f1).parameters); assert_equal([[:req, :x], [:key, :str], [:key, :num]], method(:f2).parameters); diff --git a/test/ruby/test_syntax.rb b/test/ruby/test_syntax.rb index 02a95bc60b..72a3cc2fc7 100644 --- a/test/ruby/test_syntax.rb +++ b/test/ruby/test_syntax.rb @@ -249,6 +249,22 @@ class TestSyntax < Test::Unit::TestCase assert_syntax_error('def o.foo(@@foo: a) end', /class variable/) end + def test_keywords_specified_and_not_accepted + assert_syntax_error('def o.foo(a:, **nil) end', /unexpected/) + assert_syntax_error('def o.foo(a:, **nil, &b) end', /unexpected/) + assert_syntax_error('def o.foo(**a, **nil) end', /unexpected/) + assert_syntax_error('def o.foo(**a, **nil, &b) end', /unexpected/) + assert_syntax_error('def o.foo(**nil, **a) end', /unexpected/) + assert_syntax_error('def o.foo(**nil, **a, &b) end', /unexpected/) + + assert_syntax_error('proc do |a:, **nil| end', /unexpected/) + assert_syntax_error('proc do |a:, **nil, &b| end', /unexpected/) + assert_syntax_error('proc do |**a, **nil| end', /unexpected/) + assert_syntax_error('proc do |**a, **nil, &b| end', /unexpected/) + assert_syntax_error('proc do |**nil, **a| end', /unexpected/) + assert_syntax_error('proc do |**nil, **a, &b| end', /unexpected/) + end + def test_optional_self_reference bug9593 = '[ruby-core:61299] [Bug #9593]' o = Object.new diff --git a/vm_args.c b/vm_args.c index 4e404870da..4eccb62da6 100644 --- a/vm_args.c +++ b/vm_args.c @@ -702,6 +702,10 @@ setup_parameters_complex(rb_execution_context_t * const ec, const rb_iseq_t * co args->rest = Qfalse; } + if (kw_flag && iseq->body->param.flags.accepts_no_kwarg) { + rb_raise(rb_eArgError, "no keywords accepted"); + } + switch (arg_setup_type) { case arg_setup_method: break; /* do nothing special */ diff --git a/vm_core.h b/vm_core.h index cb22fc4115..479698932e 100644 --- a/vm_core.h +++ b/vm_core.h @@ -385,6 +385,7 @@ struct rb_iseq_constant_body { unsigned int has_block : 1; unsigned int ambiguous_param0 : 1; /* {|a|} */ + unsigned int accepts_no_kwarg : 1; } flags; unsigned int size;