mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
Add support for anonymous rest and keyword rest argument forwarding
This allows for the following syntax: ```ruby def foo(*) bar(*) end def baz(**) quux(**) end ``` This is a natural addition after the introduction of anonymous block forwarding. Anonymous rest and keyword rest arguments were already supported in method parameters, this just allows them to be used as arguments to other methods. The same advantages of anonymous block forwarding apply to rest and keyword rest argument forwarding. This has some minor changes to #parameters output. Now, instead of `[:rest], [:keyrest]`, you get `[:rest, :*], [:keyrest, :**]`. These were already used for `...` forwarding, so I think it makes it more consistent to include them in other cases. If we want to use `[:rest], [:keyrest]` in both cases, that is also possible. I don't think the previous behavior of `[:rest], [:keyrest]` in the non-... case and `[:rest, :*], [:keyrest, :**]` in the ... case makes sense, but if we did want that behavior, we'll have to make more substantial changes, such as using a different ID in the ... forwarding case. Implements [Feature #18351]
This commit is contained in:
parent
2d2ee338f3
commit
f53dfab95c
Notes:
git
2021-12-31 07:38:08 +09:00
10 changed files with 146 additions and 21 deletions
15
NEWS.md
15
NEWS.md
|
@ -7,6 +7,19 @@ Note that each entry is kept to a minimum, see links for details.
|
|||
|
||||
## Language changes
|
||||
|
||||
* Anonymous rest and keyword rest arguments can now be passed as
|
||||
arguments, instead of just used in method parameters.
|
||||
[[Feature #18351]]
|
||||
|
||||
```ruby
|
||||
def foo(*)
|
||||
bar(*)
|
||||
end
|
||||
def baz(**)
|
||||
quux(**)
|
||||
end
|
||||
```
|
||||
|
||||
## Command line options
|
||||
|
||||
## Core classes updates
|
||||
|
@ -52,3 +65,5 @@ Note: Excluding feature bug fixes.
|
|||
## IRB Autocomplete and Document Display
|
||||
|
||||
## Miscellaneous changes
|
||||
|
||||
[Feature #18351]: https://bugs.ruby-lang.org/issues/18351
|
||||
|
|
|
@ -441,6 +441,13 @@ Also, note that a bare <code>*</code> can be used to ignore arguments:
|
|||
def ignore_arguments(*)
|
||||
end
|
||||
|
||||
You can also use a bare <code>*</code> when calling a method to pass the
|
||||
arguments directly to another method:
|
||||
|
||||
def delegate_arguments(*)
|
||||
other_method(*)
|
||||
end
|
||||
|
||||
=== Keyword Arguments
|
||||
|
||||
Keyword arguments are similar to positional arguments with default values:
|
||||
|
@ -481,6 +488,13 @@ Also, note that <code>**</code> can be used to ignore keyword arguments:
|
|||
def ignore_keywords(**)
|
||||
end
|
||||
|
||||
You can also use <code>**</code> when calling a method to delegate
|
||||
keyword arguments to another method:
|
||||
|
||||
def delegate_keywords(**)
|
||||
other_method(**)
|
||||
end
|
||||
|
||||
To mark a method as accepting keywords, but not actually accepting
|
||||
keywords, you can use the <code>**nil</code>:
|
||||
|
||||
|
|
39
parse.y
39
parse.y
|
@ -427,6 +427,8 @@ static void token_info_drop(struct parser_params *p, const char *token, rb_code_
|
|||
#define lambda_beginning_p() (p->lex.lpar_beg == p->lex.paren_nest)
|
||||
|
||||
#define ANON_BLOCK_ID '&'
|
||||
#define ANON_REST_ID '*'
|
||||
#define ANON_KEYWORD_REST_ID idPow
|
||||
|
||||
static enum yytokentype yylex(YYSTYPE*, YYLTYPE*, struct parser_params*);
|
||||
|
||||
|
@ -2890,6 +2892,16 @@ args : arg_value
|
|||
/*% %*/
|
||||
/*% ripper: args_add_star!(args_new!, $2) %*/
|
||||
}
|
||||
| tSTAR
|
||||
{
|
||||
/*%%%*/
|
||||
if (!local_id(p, ANON_REST_ID)) {
|
||||
compile_error(p, "no anonymous rest parameter");
|
||||
}
|
||||
$$ = NEW_SPLAT(NEW_LVAR(ANON_REST_ID, &@1), &@$);
|
||||
/*% %*/
|
||||
/*% ripper: args_add_star!(args_new!, Qnil) %*/
|
||||
}
|
||||
| args ',' arg_value
|
||||
{
|
||||
/*%%%*/
|
||||
|
@ -2904,6 +2916,16 @@ args : arg_value
|
|||
/*% %*/
|
||||
/*% ripper: args_add_star!($1, $4) %*/
|
||||
}
|
||||
| args ',' tSTAR
|
||||
{
|
||||
/*%%%*/
|
||||
if (!local_id(p, ANON_REST_ID)) {
|
||||
compile_error(p, "no anonymous rest parameter");
|
||||
}
|
||||
$$ = rest_arg_append(p, $1, NEW_LVAR(ANON_REST_ID, &@3), &@$);
|
||||
/*% %*/
|
||||
/*% ripper: args_add_star!($1, Qnil) %*/
|
||||
}
|
||||
;
|
||||
|
||||
/* value */
|
||||
|
@ -5479,8 +5501,7 @@ f_kwrest : kwrest_mark tIDENTIFIER
|
|||
| kwrest_mark
|
||||
{
|
||||
/*%%%*/
|
||||
$$ = internal_id(p);
|
||||
arg_var(p, $$);
|
||||
arg_var(p, shadowing_lvar(p, get_id(ANON_KEYWORD_REST_ID)));
|
||||
/*% %*/
|
||||
/*% ripper: kwrest_param!(Qnil) %*/
|
||||
}
|
||||
|
@ -5555,8 +5576,7 @@ f_rest_arg : restarg_mark tIDENTIFIER
|
|||
| restarg_mark
|
||||
{
|
||||
/*%%%*/
|
||||
$$ = internal_id(p);
|
||||
arg_var(p, $$);
|
||||
arg_var(p, shadowing_lvar(p, get_id(ANON_REST_ID)));
|
||||
/*% %*/
|
||||
/*% ripper: rest_param!(Qnil) %*/
|
||||
}
|
||||
|
@ -5710,6 +5730,17 @@ assoc : arg_value tASSOC arg_value
|
|||
/*% %*/
|
||||
/*% ripper: assoc_splat!($2) %*/
|
||||
}
|
||||
| tDSTAR
|
||||
{
|
||||
/*%%%*/
|
||||
if (!local_id(p, ANON_KEYWORD_REST_ID)) {
|
||||
compile_error(p, "no anonymous keyword rest parameter");
|
||||
}
|
||||
$$ = list_append(p, NEW_LIST(0, &@$),
|
||||
NEW_LVAR(ANON_KEYWORD_REST_ID, &@$));
|
||||
/*% %*/
|
||||
/*% ripper: assoc_splat!(Qnil) %*/
|
||||
}
|
||||
;
|
||||
|
||||
operation : tIDENTIFIER
|
||||
|
|
16
proc.c
16
proc.c
|
@ -3124,6 +3124,16 @@ method_inspect(VALUE method)
|
|||
|
||||
rb_str_buf_cat2(str, "(");
|
||||
|
||||
if (RARRAY_LEN(params) == 3 &&
|
||||
RARRAY_AREF(RARRAY_AREF(params, 0), 0) == rest &&
|
||||
RARRAY_AREF(RARRAY_AREF(params, 0), 1) == ID2SYM('*') &&
|
||||
RARRAY_AREF(RARRAY_AREF(params, 1), 0) == keyrest &&
|
||||
RARRAY_AREF(RARRAY_AREF(params, 1), 1) == ID2SYM(idPow) &&
|
||||
RARRAY_AREF(RARRAY_AREF(params, 2), 0) == block &&
|
||||
RARRAY_AREF(RARRAY_AREF(params, 2), 1) == ID2SYM('&')) {
|
||||
forwarding = 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < RARRAY_LEN(params); i++) {
|
||||
pair = RARRAY_AREF(params, i);
|
||||
kind = RARRAY_AREF(pair, 0);
|
||||
|
@ -3159,8 +3169,7 @@ method_inspect(VALUE method)
|
|||
}
|
||||
else if (kind == rest) {
|
||||
if (name == ID2SYM('*')) {
|
||||
forwarding = 1;
|
||||
rb_str_cat_cstr(str, "...");
|
||||
rb_str_cat_cstr(str, forwarding ? "..." : "*");
|
||||
}
|
||||
else {
|
||||
rb_str_catf(str, "*%"PRIsVALUE, name);
|
||||
|
@ -3173,6 +3182,9 @@ method_inspect(VALUE method)
|
|||
else if (i > 0) {
|
||||
rb_str_set_len(str, RSTRING_LEN(str) - 2);
|
||||
}
|
||||
else {
|
||||
rb_str_cat_cstr(str, "**");
|
||||
}
|
||||
}
|
||||
else if (kind == block) {
|
||||
if (name == ID2SYM('&')) {
|
||||
|
|
|
@ -222,9 +222,18 @@ describe "Method#parameters" do
|
|||
m.method(:handled_via_method_missing).parameters.should == [[:rest]]
|
||||
end
|
||||
|
||||
it "adds nameless rest arg for \"star\" argument" do
|
||||
m = MethodSpecs::Methods.new
|
||||
m.method(:one_unnamed_splat).parameters.should == [[:rest]]
|
||||
ruby_version_is '3.1' do
|
||||
it "adds * rest arg for \"star\" argument" do
|
||||
m = MethodSpecs::Methods.new
|
||||
m.method(:one_unnamed_splat).parameters.should == [[:rest, :*]]
|
||||
end
|
||||
end
|
||||
|
||||
ruby_version_is ''...'3.1' do
|
||||
it "adds nameless rest arg for \"star\" argument" do
|
||||
m = MethodSpecs::Methods.new
|
||||
m.method(:one_unnamed_splat).parameters.should == [[:rest]]
|
||||
end
|
||||
end
|
||||
|
||||
it "returns the args and block for a splat and block argument" do
|
||||
|
|
|
@ -80,8 +80,16 @@ describe "Proc#parameters" do
|
|||
-> x {}.parameters.should == [[:req, :x]]
|
||||
end
|
||||
|
||||
it "adds nameless rest arg for \"star\" argument" do
|
||||
-> x, * {}.parameters.should == [[:req, :x], [:rest]]
|
||||
ruby_version_is '3.1' do
|
||||
it "adds * rest arg for \"star\" argument" do
|
||||
-> x, * {}.parameters.should == [[:req, :x], [:rest, :*]]
|
||||
end
|
||||
end
|
||||
|
||||
ruby_version_is ''...'3.1' do
|
||||
it "adds nameless rest arg for \"star\" argument" do
|
||||
-> x, * {}.parameters.should == [[:req, :x], [:rest]]
|
||||
end
|
||||
end
|
||||
|
||||
it "does not add locals as block options with a block and splat" do
|
||||
|
|
|
@ -162,7 +162,7 @@ class TestISeq < Test::Unit::TestCase
|
|||
end
|
||||
obj = Object.new
|
||||
def obj.foo(*) nil.instance_eval{ ->{super} } end
|
||||
assert_raise_with_message(Ractor::IsolationError, /hidden variable/) do
|
||||
assert_raise_with_message(Ractor::IsolationError, /refer unshareable object \[\] from variable `\*'/) do
|
||||
Ractor.make_shareable(obj.foo)
|
||||
end
|
||||
end
|
||||
|
@ -392,10 +392,18 @@ class TestISeq < Test::Unit::TestCase
|
|||
|
||||
def anon_star(*); end
|
||||
|
||||
def test_anon_param_in_disasm
|
||||
def test_anon_rest_param_in_disasm
|
||||
iseq = RubyVM::InstructionSequence.of(method(:anon_star))
|
||||
param_names = iseq.to_a[iseq.to_a.index(:method) + 1]
|
||||
assert_equal [2], param_names
|
||||
assert_equal [:*], param_names
|
||||
end
|
||||
|
||||
def anon_keyrest(**); end
|
||||
|
||||
def test_anon_keyrest_param_in_disasm
|
||||
iseq = RubyVM::InstructionSequence.of(method(:anon_keyrest))
|
||||
param_names = iseq.to_a[iseq.to_a.index(:method) + 1]
|
||||
assert_equal [:**], param_names
|
||||
end
|
||||
|
||||
def anon_block(&); end
|
||||
|
|
|
@ -566,9 +566,9 @@ class TestMethod < Test::Unit::TestCase
|
|||
assert_equal([[:req, :a], [:rest, :b], [:req, :c]], method(:mo5).parameters)
|
||||
assert_equal([[:req, :a], [:rest, :b], [:req, :c], [:block, :d]], method(:mo6).parameters)
|
||||
assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:block, :e]], method(:mo7).parameters)
|
||||
assert_equal([[:req, :a], [:opt, :b], [:rest], [:req, :d], [:block, :e]], method(:mo8).parameters)
|
||||
assert_equal([[:req, :a], [:opt, :b], [:rest, :*], [:req, :d], [:block, :e]], method(:mo8).parameters)
|
||||
assert_equal([[:req], [:block, :b]], method(:ma1).parameters)
|
||||
assert_equal([[:keyrest]], method(:mk1).parameters)
|
||||
assert_equal([[:keyrest, :**]], method(:mk1).parameters)
|
||||
assert_equal([[:keyrest, :o]], method(:mk2).parameters)
|
||||
assert_equal([[:req, :a], [:keyrest, :o]], method(:mk3).parameters)
|
||||
assert_equal([[:opt, :a], [:keyrest, :o]], method(:mk4).parameters)
|
||||
|
@ -592,9 +592,9 @@ class TestMethod < Test::Unit::TestCase
|
|||
assert_equal([[:req, :a], [:rest, :b], [:req, :c]], self.class.instance_method(:mo5).parameters)
|
||||
assert_equal([[:req, :a], [:rest, :b], [:req, :c], [:block, :d]], self.class.instance_method(:mo6).parameters)
|
||||
assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:block, :e]], self.class.instance_method(:mo7).parameters)
|
||||
assert_equal([[:req, :a], [:opt, :b], [:rest], [:req, :d], [:block, :e]], self.class.instance_method(:mo8).parameters)
|
||||
assert_equal([[:req, :a], [:opt, :b], [:rest, :*], [:req, :d], [:block, :e]], self.class.instance_method(:mo8).parameters)
|
||||
assert_equal([[:req], [:block, :b]], self.class.instance_method(:ma1).parameters)
|
||||
assert_equal([[:keyrest]], self.class.instance_method(:mk1).parameters)
|
||||
assert_equal([[:keyrest, :**]], self.class.instance_method(:mk1).parameters)
|
||||
assert_equal([[:keyrest, :o]], self.class.instance_method(:mk2).parameters)
|
||||
assert_equal([[:req, :a], [:keyrest, :o]], self.class.instance_method(:mk3).parameters)
|
||||
assert_equal([[:opt, :a], [:keyrest, :o]], self.class.instance_method(:mk4).parameters)
|
||||
|
@ -619,7 +619,7 @@ class TestMethod < Test::Unit::TestCase
|
|||
assert_equal([[:req, :a], [:rest, :b], [:req, :c], [:block, :d]], method(:pmo6).parameters)
|
||||
assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:block, :e]], method(:pmo7).parameters)
|
||||
assert_equal([[:req], [:block, :b]], method(:pma1).parameters)
|
||||
assert_equal([[:keyrest]], method(:pmk1).parameters)
|
||||
assert_equal([[:keyrest, :**]], method(:pmk1).parameters)
|
||||
assert_equal([[:keyrest, :o]], method(:pmk2).parameters)
|
||||
assert_equal([[:req, :a], [:keyrest, :o]], method(:pmk3).parameters)
|
||||
assert_equal([[:opt, :a], [:keyrest, :o]], method(:pmk4).parameters)
|
||||
|
@ -643,7 +643,7 @@ class TestMethod < Test::Unit::TestCase
|
|||
assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:block, :e]], self.class.instance_method(:pmo7).parameters)
|
||||
assert_equal([[:req], [:block, :b]], self.class.instance_method(:pma1).parameters)
|
||||
assert_equal([[:req], [:block, :b]], self.class.instance_method(:pma1).parameters)
|
||||
assert_equal([[:keyrest]], self.class.instance_method(:pmk1).parameters)
|
||||
assert_equal([[:keyrest, :**]], self.class.instance_method(:pmk1).parameters)
|
||||
assert_equal([[:keyrest, :o]], self.class.instance_method(:pmk2).parameters)
|
||||
assert_equal([[:req, :a], [:keyrest, :o]], self.class.instance_method(:pmk3).parameters)
|
||||
assert_equal([[:opt, :a], [:keyrest, :o]], self.class.instance_method(:pmk4).parameters)
|
||||
|
|
|
@ -1261,7 +1261,7 @@ class TestProc < Test::Unit::TestCase
|
|||
assert_equal([[:req, :a], [:rest, :b], [:req, :c], [:block, :d]], method(:pmo6).to_proc.parameters)
|
||||
assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:block, :e]], method(:pmo7).to_proc.parameters)
|
||||
assert_equal([[:req], [:block, :b]], method(:pma1).to_proc.parameters)
|
||||
assert_equal([[:keyrest]], method(:pmk1).to_proc.parameters)
|
||||
assert_equal([[:keyrest, :**]], method(:pmk1).to_proc.parameters)
|
||||
assert_equal([[:keyrest, :o]], method(:pmk2).to_proc.parameters)
|
||||
assert_equal([[:req, :a], [:keyrest, :o]], method(:pmk3).to_proc.parameters)
|
||||
assert_equal([[:opt, :a], [:keyrest, :o]], method(:pmk4).to_proc.parameters)
|
||||
|
|
|
@ -78,6 +78,34 @@ class TestSyntax < Test::Unit::TestCase
|
|||
end;
|
||||
end
|
||||
|
||||
def test_anonymous_rest_forwarding
|
||||
assert_syntax_error("def b; c(*); end", /no anonymous rest parameter/)
|
||||
assert_syntax_error("def b; c(1, *); end", /no anonymous rest parameter/)
|
||||
assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}")
|
||||
begin;
|
||||
def b(*); c(*) end
|
||||
def c(*a); a end
|
||||
def d(*); b(*, *) end
|
||||
assert_equal([1, 2], b(1, 2))
|
||||
assert_equal([1, 2, 1, 2], d(1, 2))
|
||||
end;
|
||||
end
|
||||
|
||||
def test_anonymous_keyword_rest_forwarding
|
||||
assert_syntax_error("def b; c(**); end", /no anonymous keyword rest parameter/)
|
||||
assert_syntax_error("def b; c(k: 1, **); end", /no anonymous keyword rest parameter/)
|
||||
assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}")
|
||||
begin;
|
||||
def b(**); c(**) end
|
||||
def c(**kw); kw end
|
||||
def d(**); b(k: 1, **) end
|
||||
def e(**); b(**, k: 1) end
|
||||
assert_equal({a: 1, k: 3}, b(a: 1, k: 3))
|
||||
assert_equal({a: 1, k: 3}, d(a: 1, k: 3))
|
||||
assert_equal({a: 1, k: 1}, e(a: 1, k: 3))
|
||||
end;
|
||||
end
|
||||
|
||||
def test_newline_in_block_parameters
|
||||
bug = '[ruby-dev:45292]'
|
||||
["", "a", "a, b"].product(["", ";x", [";", "x"]]) do |params|
|
||||
|
|
Loading…
Reference in a new issue