From 3a4e102d806beef410b383b942db01631d526312 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Wed, 17 Sep 2025 19:06:55 +0200 Subject: [PATCH] Reject argument command call taking a block with more trailing arguments https://bugs.ruby-lang.org/issues/21168#note-5 The added code samples align with `parse.y`, except for `foo(bar baz do end)` which `parse.y` currently rejects but shouldn't. --- snapshots/command_method_call_2.txt | 96 +++++++++++++++++++ src/prism.c | 11 +++ test/prism/errors/command_calls.txt | 7 ++ test/prism/errors/command_calls_34.txt | 24 +++++ test/prism/fixtures/command_method_call_2.txt | 3 + test/prism/fixtures_test.rb | 2 + test/prism/lex_test.rb | 3 + test/prism/ruby/parser_test.rb | 3 + test/prism/ruby/ripper_test.rb | 3 + test/prism/ruby/ruby_parser_test.rb | 3 + 10 files changed, 155 insertions(+) create mode 100644 snapshots/command_method_call_2.txt create mode 100644 test/prism/errors/command_calls_34.txt create mode 100644 test/prism/fixtures/command_method_call_2.txt diff --git a/snapshots/command_method_call_2.txt b/snapshots/command_method_call_2.txt new file mode 100644 index 0000000000..7f09f88128 --- /dev/null +++ b/snapshots/command_method_call_2.txt @@ -0,0 +1,96 @@ +@ ProgramNode (location: (1,0)-(3,17)) +├── flags: ∅ +├── locals: [] +└── statements: + @ StatementsNode (location: (1,0)-(3,17)) + ├── flags: ∅ + └── body: (length: 2) + ├── @ CallNode (location: (1,0)-(1,19)) + │ ├── flags: newline, ignore_visibility + │ ├── receiver: ∅ + │ ├── call_operator_loc: ∅ + │ ├── name: :foo + │ ├── message_loc: (1,0)-(1,3) = "foo" + │ ├── opening_loc: (1,3)-(1,4) = "(" + │ ├── arguments: + │ │ @ ArgumentsNode (location: (1,4)-(1,18)) + │ │ ├── flags: ∅ + │ │ └── arguments: (length: 1) + │ │ └── @ CallNode (location: (1,4)-(1,18)) + │ │ ├── flags: ignore_visibility + │ │ ├── receiver: ∅ + │ │ ├── call_operator_loc: ∅ + │ │ ├── name: :bar + │ │ ├── message_loc: (1,4)-(1,7) = "bar" + │ │ ├── opening_loc: ∅ + │ │ ├── arguments: + │ │ │ @ ArgumentsNode (location: (1,8)-(1,11)) + │ │ │ ├── flags: ∅ + │ │ │ └── arguments: (length: 1) + │ │ │ └── @ CallNode (location: (1,8)-(1,11)) + │ │ │ ├── flags: variable_call, ignore_visibility + │ │ │ ├── receiver: ∅ + │ │ │ ├── call_operator_loc: ∅ + │ │ │ ├── name: :baz + │ │ │ ├── message_loc: (1,8)-(1,11) = "baz" + │ │ │ ├── opening_loc: ∅ + │ │ │ ├── arguments: ∅ + │ │ │ ├── closing_loc: ∅ + │ │ │ └── block: ∅ + │ │ ├── closing_loc: ∅ + │ │ └── block: + │ │ @ BlockNode (location: (1,12)-(1,18)) + │ │ ├── flags: ∅ + │ │ ├── locals: [] + │ │ ├── parameters: ∅ + │ │ ├── body: ∅ + │ │ ├── opening_loc: (1,12)-(1,14) = "do" + │ │ └── closing_loc: (1,15)-(1,18) = "end" + │ ├── closing_loc: (1,18)-(1,19) = ")" + │ └── block: ∅ + └── @ CallNode (location: (3,0)-(3,17)) + ├── flags: newline, ignore_visibility + ├── receiver: ∅ + ├── call_operator_loc: ∅ + ├── name: :foo + ├── message_loc: (3,0)-(3,3) = "foo" + ├── opening_loc: (3,3)-(3,4) = "(" + ├── arguments: + │ @ ArgumentsNode (location: (3,4)-(3,16)) + │ ├── flags: ∅ + │ └── arguments: (length: 1) + │ └── @ CallNode (location: (3,4)-(3,16)) + │ ├── flags: ignore_visibility + │ ├── receiver: ∅ + │ ├── call_operator_loc: ∅ + │ ├── name: :bar + │ ├── message_loc: (3,4)-(3,7) = "bar" + │ ├── opening_loc: ∅ + │ ├── arguments: + │ │ @ ArgumentsNode (location: (3,8)-(3,16)) + │ │ ├── flags: ∅ + │ │ └── arguments: (length: 2) + │ │ ├── @ CallNode (location: (3,8)-(3,11)) + │ │ │ ├── flags: variable_call, ignore_visibility + │ │ │ ├── receiver: ∅ + │ │ │ ├── call_operator_loc: ∅ + │ │ │ ├── name: :baz + │ │ │ ├── message_loc: (3,8)-(3,11) = "baz" + │ │ │ ├── opening_loc: ∅ + │ │ │ ├── arguments: ∅ + │ │ │ ├── closing_loc: ∅ + │ │ │ └── block: ∅ + │ │ └── @ CallNode (location: (3,13)-(3,16)) + │ │ ├── flags: variable_call, ignore_visibility + │ │ ├── receiver: ∅ + │ │ ├── call_operator_loc: ∅ + │ │ ├── name: :bat + │ │ ├── message_loc: (3,13)-(3,16) = "bat" + │ │ ├── opening_loc: ∅ + │ │ ├── arguments: ∅ + │ │ ├── closing_loc: ∅ + │ │ └── block: ∅ + │ ├── closing_loc: ∅ + │ └── block: ∅ + ├── closing_loc: (3,16)-(3,17) = ")" + └── block: ∅ diff --git a/src/prism.c b/src/prism.c index 42a821e535..6dd0b2ae73 100644 --- a/src/prism.c +++ b/src/prism.c @@ -14443,6 +14443,17 @@ parse_arguments(pm_parser_t *parser, pm_arguments_t *arguments, bool accepts_for if (accepted_newline) { pm_parser_err_previous(parser, PM_ERR_INVALID_COMMA); } + + // If this is a command call and an argument takes a block, + // there can be no further arguments. For example, + // `foo(bar 1 do end, 2)` should be rejected. + if (PM_NODE_TYPE_P(argument, PM_CALL_NODE)) { + pm_call_node_t *call = (pm_call_node_t *) argument; + if (call->opening_loc.start == NULL && call->arguments != NULL && call->block != NULL) { + pm_parser_err_previous(parser, PM_ERR_INVALID_COMMA); + break; + } + } } else { // If there is no comma at the end of the argument list then we're // done parsing arguments and can break out of this loop. diff --git a/test/prism/errors/command_calls.txt b/test/prism/errors/command_calls.txt index 19812a1d0a..6601e5fbbc 100644 --- a/test/prism/errors/command_calls.txt +++ b/test/prism/errors/command_calls.txt @@ -1,3 +1,10 @@ [a b] ^ unexpected local variable or method; expected a `,` separator for the array elements + +[ + a b do + ^ unexpected local variable or method; expected a `,` separator for the array elements + end, +] + diff --git a/test/prism/errors/command_calls_34.txt b/test/prism/errors/command_calls_34.txt new file mode 100644 index 0000000000..ce62bc1492 --- /dev/null +++ b/test/prism/errors/command_calls_34.txt @@ -0,0 +1,24 @@ +foo(bar 1 do end, 2) + ^ invalid comma + ^ unexpected integer; expected a `)` to close the arguments + ^ unexpected integer, expecting end-of-input + ^ unexpected ')', expecting end-of-input + ^ unexpected ')', ignoring it + +foo(bar 1 do end,) + ^ invalid comma + +foo(1, bar 2 do end) + ^ unexpected integer; expected a `)` to close the arguments + ^ unexpected integer, expecting end-of-input + ^~ unexpected 'do', expecting end-of-input + ^~ unexpected 'do', ignoring it + ^~~ unexpected 'end', ignoring it + ^ unexpected ')', ignoring it + +foo(1, bar 2) + ^ unexpected integer; expected a `)` to close the arguments + ^ unexpected integer, expecting end-of-input + ^ unexpected ')', expecting end-of-input + ^ unexpected ')', ignoring it + diff --git a/test/prism/fixtures/command_method_call_2.txt b/test/prism/fixtures/command_method_call_2.txt new file mode 100644 index 0000000000..165c45987a --- /dev/null +++ b/test/prism/fixtures/command_method_call_2.txt @@ -0,0 +1,3 @@ +foo(bar baz do end) + +foo(bar baz, bat) diff --git a/test/prism/fixtures_test.rb b/test/prism/fixtures_test.rb index b4b656fcf4..ddb6ffb40c 100644 --- a/test/prism/fixtures_test.rb +++ b/test/prism/fixtures_test.rb @@ -27,6 +27,8 @@ class FixturesTest < TestCase # Leaving these out until they are supported by parse.y. except << "leading_logical.txt" except << "endless_methods_command_call.txt" + # https://bugs.ruby-lang.org/issues/21168#note-5 + except << "command_method_call_2.txt" Fixture.each(except: except) do |fixture| define_method(fixture.test_name) { assert_valid_syntax(fixture.read) } diff --git a/test/prism/lex_test.rb b/test/prism/lex_test.rb index 4eacbab3e1..3a0da1a2d8 100644 --- a/test/prism/lex_test.rb +++ b/test/prism/lex_test.rb @@ -48,6 +48,9 @@ class LexTest < TestCase # https://bugs.ruby-lang.org/issues/17398#note-12 except << "endless_methods_command_call.txt" + # https://bugs.ruby-lang.org/issues/21168#note-5 + except << "command_method_call_2.txt" + Fixture.each(except: except) do |fixture| define_method(fixture.test_name) { assert_lex(fixture) } end diff --git a/test/prism/ruby/parser_test.rb b/test/prism/ruby/parser_test.rb index 98740f0973..10b5fca5ea 100644 --- a/test/prism/ruby/parser_test.rb +++ b/test/prism/ruby/parser_test.rb @@ -70,6 +70,9 @@ class ParserTest < TestCase # Ruby >= 3.5 specific syntax "endless_methods_command_call.txt", + + # https://bugs.ruby-lang.org/issues/21168#note-5 + "command_method_call_2.txt", ] # These files contain code that is being parsed incorrectly by the parser diff --git a/test/prism/ruby/ripper_test.rb b/test/prism/ruby/ripper_test.rb index 39325137ba..4916ec8d9d 100644 --- a/test/prism/ruby/ripper_test.rb +++ b/test/prism/ruby/ripper_test.rb @@ -33,6 +33,9 @@ class RipperTest < TestCase # https://bugs.ruby-lang.org/issues/17398#note-12 "endless_methods_command_call.txt", + + # https://bugs.ruby-lang.org/issues/21168#note-5 + "command_method_call_2.txt", ] # Skip these tests that we haven't implemented yet. diff --git a/test/prism/ruby/ruby_parser_test.rb b/test/prism/ruby/ruby_parser_test.rb index b21ad81391..ec55e41967 100644 --- a/test/prism/ruby/ruby_parser_test.rb +++ b/test/prism/ruby/ruby_parser_test.rb @@ -79,6 +79,9 @@ class RubyParserTest < TestCase # Ruby >= 3.5 specific syntax "endless_methods_command_call.txt", + + # https://bugs.ruby-lang.org/issues/21168#note-5 + "command_method_call_2.txt", ] Fixture.each(except: failures) do |fixture|