Port test_new_parser_ll2

This commit is contained in:
Johannes Altmanninger 2023-12-09 20:02:34 +01:00
parent af4b8ccc91
commit d5cfa0e346
3 changed files with 126 additions and 123 deletions

View file

@ -110,7 +110,7 @@ mod parse_constants_ffi {
}
// Statement decorations like 'command' or 'exec'.
#[derive(Clone, Copy, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StatementDecoration {
none,
command,

View file

@ -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::signal::{signal_clear_cancel, signal_reset_handlers, signal_set_handl
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 @@ add_test!("test_new_parser_correctness", || {
}
});
// 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::<Vec<_>>(),
' ',
);
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.

View file

@ -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 <ast::type_t Type>
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<ast::type_t::decorated_statement>(L"function -h");
check_function_help<ast::type_t::decorated_statement>(L"function --help");
check_function_help<ast::type_t::function_header>(L"function --foo; end");
check_function_help<ast::type_t::function_header>(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},