diff --git a/fish-rust/src/parse_constants.rs b/fish-rust/src/parse_constants.rs index 0edf31904..064c7c7e0 100644 --- a/fish-rust/src/parse_constants.rs +++ b/fish-rust/src/parse_constants.rs @@ -110,7 +110,7 @@ pub enum ParseKeyword { } // Statement decorations like 'command' or 'exec'. - #[derive(Clone, Copy, Eq, PartialEq)] + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum StatementDecoration { none, command, diff --git a/fish-rust/src/tests/parser.rs b/fish-rust/src/tests/parser.rs index 6e8e977e7..b662a1261 100644 --- a/fish-rust/src/tests/parser.rs +++ b/fish-rust/src/tests/parser.rs @@ -1,7 +1,8 @@ -use crate::ast::{Ast, List, Node}; +use crate::ast::{self, Ast, List, Node, Traversal}; use crate::builtins::shared::{STATUS_CMD_OK, STATUS_UNMATCHED_WILDCARD}; use crate::expand::ExpandFlags; use crate::io::{IoBufferfill, IoChain}; +use crate::parse_constants::StatementDecoration; use crate::parse_constants::{ParseTreeFlags, ParserTestErrorBits}; use crate::parse_util::{parse_util_detect_errors, parse_util_detect_errors_in_argument}; use crate::parser::Parser; @@ -10,6 +11,7 @@ use crate::tests::prelude::*; use crate::threads::{iothread_drain_all, iothread_perform}; use crate::wchar::prelude::*; +use crate::wcstringutil::join_strings; use libc::SIGINT; use std::time::Duration; @@ -375,6 +377,128 @@ fn string_for_permutation(fuzzes: &[&wstr], len: usize, permutation: usize) -> O } }); +// Test the LL2 (two token lookahead) nature of the parser by exercising the special builtin and +// command handling. In particular, 'command foo' should be a decorated statement 'foo' but 'command +// -help' should be an undecorated statement 'command' with argument '--help', and NOT attempt to +// run a command called '--help'. +add_test!("test_new_parser_ll2", || { + // Parse a statement, returning the command, args (joined by spaces), and the decoration. Returns + // true if successful. + fn test_1_parse_ll2(src: &wstr) -> Option<(WString, WString, StatementDecoration)> { + let ast = Ast::parse(src, ParseTreeFlags::default(), None); + if ast.errored() { + return None; + } + + // Get the statement. Should only have one. + let mut statement = None; + for n in Traversal::new(ast.top()) { + if let Some(tmp) = n.as_decorated_statement() { + assert!( + statement.is_none(), + "More than one decorated statement found in '{}'", + src + ); + statement = Some(tmp); + } + } + let statement = statement.expect("No decorated statement found"); + + // Return its decoration and command. + let out_deco = statement.decoration(); + let out_cmd = statement.command.source(src).to_owned(); + + // Return arguments separated by spaces. + let out_joined_args = join_strings( + &statement + .args_or_redirs + .iter() + .filter(|a| a.is_argument()) + .map(|a| a.source(src)) + .collect::>(), + ' ', + ); + + Some((out_cmd, out_joined_args, out_deco)) + } + macro_rules! validate { + ($src:expr, $cmd:expr, $args:expr, $deco:expr) => { + let (cmd, args, deco) = test_1_parse_ll2(L!($src)).unwrap(); + assert_eq!(cmd, L!($cmd)); + assert_eq!(args, L!($args)); + assert_eq!(deco, $deco); + }; + } + + validate!("echo hello", "echo", "hello", StatementDecoration::none); + validate!( + "command echo hello", + "echo", + "hello", + StatementDecoration::command + ); + validate!( + "exec echo hello", + "echo", + "hello", + StatementDecoration::exec + ); + validate!( + "command command hello", + "command", + "hello", + StatementDecoration::command + ); + validate!( + "builtin command hello", + "command", + "hello", + StatementDecoration::builtin + ); + validate!( + "command --help", + "command", + "--help", + StatementDecoration::none + ); + validate!("command -h", "command", "-h", StatementDecoration::none); + validate!("command", "command", "", StatementDecoration::none); + validate!("command -", "command", "-", StatementDecoration::none); + validate!("command --", "command", "--", StatementDecoration::none); + validate!( + "builtin --names", + "builtin", + "--names", + StatementDecoration::none + ); + validate!("function", "function", "", StatementDecoration::none); + validate!( + "function --help", + "function", + "--help", + StatementDecoration::none + ); + + // Verify that 'function -h' and 'function --help' are plain statements but 'function --foo' is + // not (issue #1240). + macro_rules! check_function_help { + ($src:expr, $typ:expr) => { + let ast = Ast::parse(L!($src), ParseTreeFlags::default(), None); + assert!(!ast.errored()); + assert_eq!( + Traversal::new(ast.top()) + .filter(|n| n.typ() == $typ) + .count(), + 1 + ); + }; + } + check_function_help!("function -h", ast::Type::decorated_statement); + check_function_help!("function --help", ast::Type::decorated_statement); + check_function_help!("function --foo; end", ast::Type::function_header); + check_function_help!("function foo; end", ast::Type::function_header); +}); + add_test!("test_eval_recursion_detection", || { // Ensure that we don't crash on infinite self recursion and mutual recursion. These must use // the principal parser because we cannot yet execute jobs on other parsers. diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp index a6ebe4588..46c911819 100644 --- a/src/fish_tests.cpp +++ b/src/fish_tests.cpp @@ -1077,126 +1077,6 @@ static void test_input() { } } -// todo!("port this") -// Parse a statement, returning the command, args (joined by spaces), and the decoration. Returns -// true if successful. -static bool test_1_parse_ll2(const wcstring &src, wcstring *out_cmd, wcstring *out_joined_args, - statement_decoration_t *out_deco) { - using namespace ast; - out_cmd->clear(); - out_joined_args->clear(); - *out_deco = statement_decoration_t::none; - - auto ast = ast_parse(src); - if (ast->errored()) return false; - - // Get the statement. Should only have one. - const decorated_statement_t *statement = nullptr; - for (auto ast_traversal = new_ast_traversal(*ast->top());;) { - auto n = ast_traversal->next(); - if (!n->has_value()) break; - if (const auto *tmp = n->try_as_decorated_statement()) { - if (statement) { - say(L"More than one decorated statement found in '%ls'", src.c_str()); - return false; - } - statement = tmp; - } - } - if (!statement) { - say(L"No decorated statement found in '%ls'", src.c_str()); - return false; - } - - // Return its decoration and command. - *out_deco = statement->decoration(); - *out_cmd = *statement->command().source(src); - - // Return arguments separated by spaces. - bool first = true; - for (size_t i = 0; i < statement->args_or_redirs().count(); i++) { - const ast::argument_or_redirection_t &arg = *statement->args_or_redirs().at(i); - if (!arg.is_argument()) continue; - if (!first) out_joined_args->push_back(L' '); - out_joined_args->append(*arg.ptr()->source(src)); - first = false; - } - - return true; -} - -// Verify that 'function -h' and 'function --help' are plain statements but 'function --foo' is -// not (issue #1240). -template -static void check_function_help(const wchar_t *src) { - using namespace ast; - auto ast = ast_parse(src); - if (ast->errored()) { - err(L"Failed to parse '%ls'", src); - } - - int count = 0; - for (auto ast_traversal = new_ast_traversal(*ast->top());;) { - auto node = ast_traversal->next(); - if (!node->has_value()) break; - count += (node->typ() == Type); - } - if (count == 0) { - err(L"Failed to find node of type '%ls'", ast_type_to_string(Type)); - } else if (count > 1) { - err(L"Found too many nodes of type '%ls'", ast_type_to_string(Type)); - } -} - -// todo!("port this") -// Test the LL2 (two token lookahead) nature of the parser by exercising the special builtin and -// command handling. In particular, 'command foo' should be a decorated statement 'foo' but 'command -// -help' should be an undecorated statement 'command' with argument '--help', and NOT attempt to -// run a command called '--help'. -static void test_new_parser_ll2() { - say(L"Testing parser two-token lookahead"); - - const struct { - wcstring src; - wcstring cmd; - wcstring args; - statement_decoration_t deco; - } tests[] = {{L"echo hello", L"echo", L"hello", statement_decoration_t::none}, - {L"command echo hello", L"echo", L"hello", statement_decoration_t::command}, - {L"exec echo hello", L"echo", L"hello", statement_decoration_t::exec}, - {L"command command hello", L"command", L"hello", statement_decoration_t::command}, - {L"builtin command hello", L"command", L"hello", statement_decoration_t::builtin}, - {L"command --help", L"command", L"--help", statement_decoration_t::none}, - {L"command -h", L"command", L"-h", statement_decoration_t::none}, - {L"command", L"command", L"", statement_decoration_t::none}, - {L"command -", L"command", L"-", statement_decoration_t::none}, - {L"command --", L"command", L"--", statement_decoration_t::none}, - {L"builtin --names", L"builtin", L"--names", statement_decoration_t::none}, - {L"function", L"function", L"", statement_decoration_t::none}, - {L"function --help", L"function", L"--help", statement_decoration_t::none}}; - - for (const auto &test : tests) { - wcstring cmd, args; - statement_decoration_t deco = statement_decoration_t::none; - bool success = test_1_parse_ll2(test.src, &cmd, &args, &deco); - if (!success) err(L"Parse of '%ls' failed on line %ld", test.cmd.c_str(), (long)__LINE__); - if (cmd != test.cmd) - err(L"When parsing '%ls', expected command '%ls' but got '%ls' on line %ld", - test.src.c_str(), test.cmd.c_str(), cmd.c_str(), (long)__LINE__); - if (args != test.args) - err(L"When parsing '%ls', expected args '%ls' but got '%ls' on line %ld", - test.src.c_str(), test.args.c_str(), args.c_str(), (long)__LINE__); - if (deco != test.deco) - err(L"When parsing '%ls', expected decoration %d but got %d on line %ld", - test.src.c_str(), (int)test.deco, (int)deco, (long)__LINE__); - } - - check_function_help(L"function -h"); - check_function_help(L"function --help"); - check_function_help(L"function --foo; end"); - check_function_help(L"function foo; end"); -} - // todo!("port this") static void test_new_parser_ad_hoc() { using namespace ast; @@ -1713,7 +1593,6 @@ static const test_t s_tests[]{ {TEST_GROUP("enum"), test_enum_set}, {TEST_GROUP("enum"), test_enum_array}, {TEST_GROUP("autosuggestion"), test_autosuggestion_combining}, - {TEST_GROUP("new_parser_ll2"), test_new_parser_ll2}, {TEST_GROUP("test_abbreviations"), test_abbreviations}, {TEST_GROUP("new_parser_ad_hoc"), test_new_parser_ad_hoc}, {TEST_GROUP("new_parser_errors"), test_new_parser_errors},