From c08f46f836a362391c0c68145a651ec6802ec165 Mon Sep 17 00:00:00 2001 From: Yash Thakur <45539777+ysthakur@users.noreply.github.com> Date: Tue, 30 Jan 2024 00:49:42 -0500 Subject: [PATCH] Respect SyntaxShape when parsing spread operator (#11674) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes an issue brought up by nihilander in [Discord](https://discord.com/channels/601130461678272522/614593951969574961/1201594105986285649). # Description Nushell panics when the spread operator is used like this (the `...$rest` shouldn't actually be parsed as a spread operator at all): ```nu $ def foo [...rest: string] {...$rest} $ foo bar baz thread 'main' panicked at /root/.cargo/registry/src/index.crates.io-6f17d22bba15001f/nu-protocol-0.89.0/src/signature.rs:650:9: Internal error: can't run a predeclaration without a body stack backtrace: 0: rust_begin_unwind 1: core::panicking::panic_fmt 2: ::run 3: nu_engine::eval::eval_call 4: nu_engine::eval::eval_expression_with_input 5: nu_engine::eval::eval_element_with_input 6: nu_engine::eval::eval_block 7: nu_cli::util::eval_source 8: nu_cli::repl::evaluate_repl 9: nu::run::run_repl 10: nu::main note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace. ``` The problem was that whenever the parser saw something like `{...$`, `{...(`, or `{...[`, it would treat that as a record with a spread expression, ignoring the syntax shape of the block it was parsing. This should now be fixed, and the snippet above instead gives the following error: ```nu Error: nu::shell::external_command × External command failed ╭─[entry #1:1:1] 1 │ def foo [...rest] {...$rest} · ────┬─── · ╰── executable was not found ╰──── help: No such file or directory (os error 2) ``` # User-Facing Changes Stuff like `do { ...$rest }` will now try to run a command `...$rest` rather than complaining that variable `$rest` doesn't exist. # Tests + Formatting # After Submitting Sorry about the issue, I am not touching the parser again for a long time :) --- crates/nu-parser/src/parser.rs | 12 +++++++----- src/tests/test_spread.rs | 12 ++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 288ad4e2b3..ef1955c6e4 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -1700,16 +1700,18 @@ pub fn parse_brace_expr( parse_closure_expression(working_set, shape, span) } else if matches!(third_token, Some(b":")) { parse_full_cell_path(working_set, None, span) - } else if second_token.is_some_and(|c| { - c.len() > 3 && c.starts_with(b"...") && (c[3] == b'$' || c[3] == b'{' || c[3] == b'(') - }) { - parse_record(working_set, span) - } else if matches!(shape, SyntaxShape::Closure(_)) || matches!(shape, SyntaxShape::Any) { + } else if matches!(shape, SyntaxShape::Closure(_)) { parse_closure_expression(working_set, shape, span) } else if matches!(shape, SyntaxShape::Block) { parse_block_expression(working_set, span) } else if matches!(shape, SyntaxShape::MatchBlock) { parse_match_block_expression(working_set, span) + } else if second_token.is_some_and(|c| { + c.len() > 3 && c.starts_with(b"...") && (c[3] == b'$' || c[3] == b'{' || c[3] == b'(') + }) { + parse_record(working_set, span) + } else if matches!(shape, SyntaxShape::Any) { + parse_closure_expression(working_set, shape, span) } else { working_set.error(ParseError::ExpectedWithStringMsg( format!("non-block value: {shape}"), diff --git a/src/tests/test_spread.rs b/src/tests/test_spread.rs index 31a6183b1a..f031c91988 100644 --- a/src/tests/test_spread.rs +++ b/src/tests/test_spread.rs @@ -190,3 +190,15 @@ fn deprecate_implicit_spread_for_externals() { .contains("Automatically spreading lists is deprecated")); assert_eq!(result.out, "1 2"); } + +#[test] +fn respect_shape() -> TestResult { + fail_test( + "def foo [...rest] { ...$rest }; foo bar baz", + "executable was not found", + ) + .unwrap(); + fail_test("module foo { ...$bar }", "expected_keyword").unwrap(); + run_test(r#"def "...$foo" [] {2}; do { ...$foo }"#, "2").unwrap(); + run_test(r#"match "...$foo" { ...$foo => 5 }"#, "5") +}