Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions src/parser/tests/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)]
Expand All @@ -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);
}
}
6 changes: 3 additions & 3 deletions src/parser/tests/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/parser/tests/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
2 changes: 1 addition & 1 deletion src/parser/tests/indexes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down
2 changes: 1 addition & 1 deletion src/parser/tests/relations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
27 changes: 26 additions & 1 deletion src/test_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

Comment on lines +36 to +54
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Speed up token normalisation and widen coverage (optional)

Avoid rebuilding mappings per call and extend coverage to common punctuation.

Add a module‑level constant and reuse it:

-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))
-}
+fn normalise_tokens(s: &str) -> String {
+    TOKEN_MAP
+        .iter()
+        .fold(s.to_string(), |acc, (raw, human)| acc.replace(raw, human))
+}

Add near the top of the module:

const TOKEN_MAP: [(&str, &str); 12] = [
    ("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"),
    ("T_DOT", "dot"),
    ("T_COLON", "colon"),
];
🤖 Prompt for AI Agents
In src/test_util.rs around lines 36 to 54, normalise_tokens currently rebuilds
the token mapping on every call and misses a couple common punctuation tokens;
create a module-level constant (e.g., TOKEN_MAP) with the full list of mappings
including T_DOT and T_COLON, then refactor normalise_tokens to iterate over that
constant instead of constructing the array inline so the mapping is allocated
once and coverage is widened.

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)
}
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion tests/expression_compound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion tests/expression_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions tests/expression_postfix.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 1 addition & 1 deletion tests/expression_prefix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion tests/expression_var_and_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions tests/function_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down