diff --git a/src/parser/tests/expression.rs b/src/parser/tests/expression.rs index 3d47aa3f..8451cd25 100644 --- a/src/parser/tests/expression.rs +++ b/src/parser/tests/expression.rs @@ -3,8 +3,8 @@ use crate::parser::ast::{BinaryOp, Expr, UnaryOp}; use crate::parser::expression::parse_expression; use crate::test_util::{ - bit_slice, call, call_expr, closure, field, field_access, lit_bool, lit_num, lit_str, - method_call, struct_expr, tuple, tuple_index, var, + assert_delimiter_error, assert_parse_error, bit_slice, call, call_expr, closure, field, + field_access, lit_bool, lit_num, lit_str, method_call, struct_expr, tuple, tuple_index, var, }; use rstest::rstest; @@ -93,15 +93,15 @@ fn parses_literals(#[case] src: &str, #[case] expected: Expr) { } #[rstest] -#[case("1 +", 1)] -#[case("(1 + 2", 1)] -#[case("1 ? 2", 1)] -#[case("x :", 1)] -#[case("x as", 1)] -#[case("x =", 1)] -#[case("x ;", 1)] -#[case("x =>", 1)] -#[case("", 1)] +#[case::addition_missing_rhs("1 +", 1)] +#[case::unclosed_parenthesis("(1 + 2", 1)] +#[case::unexpected_question_mark("1 ? 2", 1)] +#[case::trailing_colon("x :", 1)] +#[case::incomplete_as_cast("x as", 1)] +#[case::assignment_missing_rhs("x =", 1)] +#[case::extraneous_semicolon("x ;", 1)] +#[case::unexpected_fat_arrow("x =>", 1)] +#[case::empty_input("", 1)] #[case::bit_slice_missing_comma("e[1 0]", 1)] #[case::bit_slice_missing_rbracket("e[1,0", 1)] #[case::bit_slice_missing_lhs("e[,0]", 1)] @@ -117,3 +117,24 @@ fn reports_errors(#[case] src: &str, #[case] min_errs: usize) { Err(errs) => assert!(errs.len() >= min_errs), } } + +#[rstest] +#[case::trailing_dot("foo.", "expected identifier or tuple index after '.'", 4, 4, false)] +#[case::bit_slice_missing_comma("e[1]", "expected comma", 3, 4, false)] +#[case::bit_slice_unclosed("e[1,0", "expected right bracket", 5, 5, true)] +fn postfix_expression_errors( + #[case] src: &str, + #[case] msg: &str, + #[case] start: usize, + #[case] end: usize, + #[case] unclosed: bool, +) { + let Err(errors) = parse_expression(src) else { + panic!("expected error"); + }; + if unclosed { + assert_delimiter_error(&errors, msg, start, end); + } else { + assert_parse_error(&errors, msg, start, end); + } +} diff --git a/src/parser/tests/functions.rs b/src/parser/tests/functions.rs index 9ddd06ac..30c3dad1 100644 --- a/src/parser/tests/functions.rs +++ b/src/parser/tests/functions.rs @@ -104,13 +104,13 @@ fn function_unclosed_params() -> &'static str { #[rstest] fn extern_function_missing_colon_is_error(extern_function_missing_colon: &str) { let parsed = crate::parse(extern_function_missing_colon); - assert_parse_error(parsed.errors(), "T_LPAREN", 30, 33); + assert_parse_error(parsed.errors(), "left paren", 30, 33); assert!(parsed.root().functions().is_empty()); } #[rstest] -#[case(function_unterminated_body(), "T_RBRACE", function_unterminated_body().len())] -#[case(function_unclosed_params(), "T_RPAREN", function_unclosed_params().len())] +#[case(function_unterminated_body(), "right brace", function_unterminated_body().len())] +#[case(function_unclosed_params(), "right paren", function_unclosed_params().len())] fn function_error_cases(#[case] src: &str, #[case] token: &str, #[case] end: usize) { let parsed = crate::parse(src); assert_parse_error(parsed.errors(), token, 0, end); diff --git a/src/parser/tests/imports.rs b/src/parser/tests/imports.rs index 419b70b8..2fb8280d 100644 --- a/src/parser/tests/imports.rs +++ b/src/parser/tests/imports.rs @@ -30,7 +30,7 @@ fn import_statement_parses(#[case] src: &str, #[case] path: &str, #[case] alias: fn import_statement_invalid_missing_path() { let src = "import as missing_path"; let parsed = parse(src); - assert_parse_error(parsed.errors(), "T_IDENT", 7, 9); + assert_parse_error(parsed.errors(), "identifier", 7, 9); let imports = parsed.root().imports(); assert!(imports.is_empty()); } diff --git a/src/parser/tests/indexes.rs b/src/parser/tests/indexes.rs index 928d3a08..2e962046 100644 --- a/src/parser/tests/indexes.rs +++ b/src/parser/tests/indexes.rs @@ -68,7 +68,7 @@ fn index_missing_on_is_error(index_invalid_missing_on: &str) { fn index_unbalanced_parentheses_is_error(index_unbalanced_parentheses: &str) { let parsed = crate::parse(index_unbalanced_parentheses); let len = index_unbalanced_parentheses.len(); - assert_delimiter_error(parsed.errors(), "T_RPAREN", 0, len); + assert_delimiter_error(parsed.errors(), "right paren", 0, len); assert!(parsed.root().indexes().is_empty()); } diff --git a/src/parser/tests/relations.rs b/src/parser/tests/relations.rs index 42d89bd9..9955ea9e 100644 --- a/src/parser/tests/relations.rs +++ b/src/parser/tests/relations.rs @@ -88,6 +88,6 @@ fn relation_unbalanced_parentheses_is_error(relation_unbalanced_parentheses: &st // opening delimiter position. This preserves branch behaviour while still // asserting the unclosed ')' is detected. let len = relation_unbalanced_parentheses.len(); - assert_delimiter_error(parsed.errors(), "T_RPAREN", 0, len); + assert_delimiter_error(parsed.errors(), "right paren", 0, len); assert!(parsed.root().relations().is_empty()); } diff --git a/src/test_util.rs b/src/test_util.rs index 2202446b..e1ebf21c 100644 --- a/src/test_util.rs +++ b/src/test_util.rs @@ -33,9 +33,34 @@ pub enum ErrorPattern { Custom(String), } +/// Replace internal token names with human-readable forms. +fn normalise_tokens(s: &str) -> String { + [ + ("T_LPAREN", "left paren"), + ("T_RPAREN", "right paren"), + ("T_LBRACKET", "left bracket"), + ("T_RBRACKET", "right bracket"), + ("T_LBRACE", "left brace"), + ("T_RBRACE", "right brace"), + ("T_COMMA", "comma"), + ("T_SEMI", "semicolon"), + ("T_PIPE", "pipe"), + ("T_IDENT", "identifier"), + ("T_NUMBER", "number"), + ] + .into_iter() + .fold(s.to_string(), |acc, (raw, human)| acc.replace(raw, human)) +} + impl ErrorPattern { fn contains_message(&self, rendered: &str) -> bool { - matches!(self, Self::Custom(msg) if rendered.contains(msg)) + let rendered = normalise_tokens(rendered); + match self { + Self::Custom(msg) => { + let msg = normalise_tokens(msg); + rendered.contains(&msg) + } + } } } diff --git a/tests/expression_compound.rs b/tests/expression_compound.rs index af6a64e9..e3cd0ef4 100644 --- a/tests/expression_compound.rs +++ b/tests/expression_compound.rs @@ -41,7 +41,7 @@ fn parses_compound_expressions(#[case] src: &str, #[case] expected: Expr) { #[case("Point { x: 1", "expected", 12, 12, true)] #[case("(1, 2", "expected", 5, 5, true)] #[case("|x|", "invalid expression", 3, 3, false)] -#[case("|x x", "expected T_PIPE", 3, 4, false)] +#[case("|x x", "expected pipe", 3, 4, false)] #[case("||", "invalid expression", 2, 2, false)] fn compound_expression_errors( #[case] src: &str, diff --git a/tests/expression_entry.rs b/tests/expression_entry.rs index 51641752..a46c2c2d 100644 --- a/tests/expression_entry.rs +++ b/tests/expression_entry.rs @@ -33,7 +33,7 @@ fn parses_binary_precedence_chain() { #[rstest] #[case("", "invalid expression", 0, 0)] -#[case("1 2", "unexpected token: T_NUMBER", 2, 3)] +#[case("1 2", "unexpected token: number", 2, 3)] fn parse_expression_errors( #[case] src: &str, #[case] msg: &str, diff --git a/tests/expression_postfix.rs b/tests/expression_postfix.rs new file mode 100644 index 00000000..9e7c11e5 --- /dev/null +++ b/tests/expression_postfix.rs @@ -0,0 +1,43 @@ +//! Integration tests for postfix expressions. +//! +//! These tests cover method calls, field accesses, tuple indexing, and bit +//! slice expressions through the public `parse_expression` API. + +use ddlint::parser::ast::Expr; +use ddlint::parser::expression::parse_expression; +use ddlint::test_util::{ + assert_delimiter_error, assert_parse_error, bit_slice, field_access, lit_num, method_call, + tuple_index, var, +}; +use rstest::rstest; + +#[rstest] +#[case::method_call("foo.bar(x)", method_call(var("foo"), "bar", vec![var("x")]))] +#[case::field_access("foo.bar", field_access(var("foo"), "bar"))] +#[case::bit_slice("e[1,0]", bit_slice(var("e"), lit_num("1"), lit_num("0")))] +#[case::tuple_index("t.0", tuple_index(var("t"), "0"))] +#[case::multi_arg_call("foo.bar(1, 2)", method_call(var("foo"), "bar", vec![lit_num("1"), lit_num("2")]))] +#[case::chained_postfix("foo.bar().baz[1,0]", bit_slice(field_access(method_call(var("foo"), "bar", vec![]), "baz"), lit_num("1"), lit_num("0")))] +fn parses_postfix_expressions(#[case] src: &str, #[case] expected: Expr) { + let expr = parse_expression(src).unwrap_or_else(|e| panic!("source {src:?} errors: {e:?}")); + assert_eq!(expr, expected); +} + +#[rstest] +#[case::unclosed_call("foo.bar(", "invalid expression", 8, 8, true)] +fn postfix_expression_errors( + #[case] src: &str, + #[case] msg: &str, + #[case] start: usize, + #[case] end: usize, + #[case] unclosed: bool, +) { + let Err(errors) = parse_expression(src) else { + panic!("expected error"); + }; + if unclosed { + assert_delimiter_error(&errors, msg, start, end); + } else { + assert_parse_error(&errors, msg, start, end); + } +} diff --git a/tests/expression_prefix.rs b/tests/expression_prefix.rs index b7737c2b..879d4f2e 100644 --- a/tests/expression_prefix.rs +++ b/tests/expression_prefix.rs @@ -32,7 +32,7 @@ fn parses_prefix_forms(#[case] src: &str, #[case] expected: Expr) { #[rstest] #[case("{}", "expected expression", 1, 2, false)] -#[case("|x x", "expected T_PIPE", 3, 4, false)] +#[case("|x x", "expected pipe", 3, 4, false)] fn prefix_form_errors( #[case] src: &str, #[case] msg: &str, diff --git a/tests/expression_var_and_call.rs b/tests/expression_var_and_call.rs index 1219ab3d..c8794466 100644 --- a/tests/expression_var_and_call.rs +++ b/tests/expression_var_and_call.rs @@ -21,7 +21,7 @@ fn parses_vars_and_calls(#[case] src: &str, #[case] expected: Expr) { #[case("foo(", "invalid expression", 4, 4, "unclosed")] #[case("foo(]", "unexpected token", 4, 5, "mismatch")] #[case("foo(x,)", "unexpected trailing comma in argument list", 6, 7, "other")] -#[case("foo(1 2)", "expected T_RPAREN", 6, 7, "other")] +#[case("foo(1 2)", "expected right paren", 6, 7, "other")] fn call_parsing_errors( #[case] src: &str, #[case] msg: &str, diff --git a/tests/function_errors.rs b/tests/function_errors.rs index 3f940322..f2d8062a 100644 --- a/tests/function_errors.rs +++ b/tests/function_errors.rs @@ -8,8 +8,8 @@ use ddlint::test_util::assert_delimiter_error; use rstest::rstest; #[rstest] -#[case("function f(x: int {", "T_RPAREN", 0, 19)] -#[case("function f[x: int) {}", "T_LPAREN", 10, 11)] +#[case("function f(x: int {", "right paren", 0, 19)] +#[case("function f[x: int) {}", "left paren", 10, 11)] fn function_parameter_errors( #[case] src: &str, #[case] msg: &str,