From f87dae6fbda0cd31ad8868318f4a00f43fbe30e6 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Wed, 3 Dec 2025 19:23:42 -0300 Subject: [PATCH] chore: Tidy tests --- ...{alternations.rs => alternations_tests.rs} | 38 - .../grammar/{anchors.rs => anchors_tests.rs} | 20 - .../{captures.rs => captures_tests.rs} | 0 .../{definitions.rs => definitions_tests.rs} | 92 -- .../grammar/{fields.rs => fields_tests.rs} | 0 .../src/ast/parser/tests/grammar/mod.rs | 19 +- .../grammar/{nodes.rs => nodes_tests.rs} | 22 - .../{quantifiers.rs => quantifiers_tests.rs} | 0 .../{sequences.rs => sequences_tests.rs} | 0 .../grammar/{special.rs => special_tests.rs} | 0 .../{trivia.rs => grammar/trivia_tests.rs} | 18 - .../plotnik-lib/src/ast/parser/tests/mod.rs | 1 - .../src/ast/parser/tests/recovery/coverage.rs | 1030 ------------ .../parser/tests/recovery/coverage_tests.rs | 301 ++++ .../{incomplete.rs => incomplete_tests.rs} | 210 ++- .../src/ast/parser/tests/recovery/mod.rs | 10 +- .../{unclosed.rs => unclosed_tests.rs} | 107 +- .../{unexpected.rs => unexpected_tests.rs} | 230 ++- .../{validation.rs => validation_tests.rs} | 1447 +++++++++-------- 19 files changed, 1584 insertions(+), 1961 deletions(-) rename crates/plotnik-lib/src/ast/parser/tests/grammar/{alternations.rs => alternations_tests.rs} (93%) rename crates/plotnik-lib/src/ast/parser/tests/grammar/{anchors.rs => anchors_tests.rs} (83%) rename crates/plotnik-lib/src/ast/parser/tests/grammar/{captures.rs => captures_tests.rs} (100%) rename crates/plotnik-lib/src/ast/parser/tests/grammar/{definitions.rs => definitions_tests.rs} (76%) rename crates/plotnik-lib/src/ast/parser/tests/grammar/{fields.rs => fields_tests.rs} (100%) rename crates/plotnik-lib/src/ast/parser/tests/grammar/{nodes.rs => nodes_tests.rs} (91%) rename crates/plotnik-lib/src/ast/parser/tests/grammar/{quantifiers.rs => quantifiers_tests.rs} (100%) rename crates/plotnik-lib/src/ast/parser/tests/grammar/{sequences.rs => sequences_tests.rs} (100%) rename crates/plotnik-lib/src/ast/parser/tests/grammar/{special.rs => special_tests.rs} (100%) rename crates/plotnik-lib/src/ast/parser/tests/{trivia.rs => grammar/trivia_tests.rs} (88%) delete mode 100644 crates/plotnik-lib/src/ast/parser/tests/recovery/coverage.rs create mode 100644 crates/plotnik-lib/src/ast/parser/tests/recovery/coverage_tests.rs rename crates/plotnik-lib/src/ast/parser/tests/recovery/{incomplete.rs => incomplete_tests.rs} (50%) rename crates/plotnik-lib/src/ast/parser/tests/recovery/{unclosed.rs => unclosed_tests.rs} (51%) rename crates/plotnik-lib/src/ast/parser/tests/recovery/{unexpected.rs => unexpected_tests.rs} (63%) rename crates/plotnik-lib/src/ast/parser/tests/recovery/{validation.rs => validation_tests.rs} (58%) diff --git a/crates/plotnik-lib/src/ast/parser/tests/grammar/alternations.rs b/crates/plotnik-lib/src/ast/parser/tests/grammar/alternations_tests.rs similarity index 93% rename from crates/plotnik-lib/src/ast/parser/tests/grammar/alternations.rs rename to crates/plotnik-lib/src/ast/parser/tests/grammar/alternations_tests.rs index 7fd55b68..b9664fe1 100644 --- a/crates/plotnik-lib/src/ast/parser/tests/grammar/alternations.rs +++ b/crates/plotnik-lib/src/ast/parser/tests/grammar/alternations_tests.rs @@ -219,26 +219,6 @@ fn unlabeled_alternation_three_items() { "#); } -#[test] -fn upper_ident_in_alternation_not_followed_by_colon() { - let input = indoc! {r#" - [(Expr) (Statement)] - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: undefined reference: `Expr` - | - 1 | [(Expr) (Statement)] - | ^^^^ undefined reference: `Expr` - error: undefined reference: `Statement` - | - 1 | [(Expr) (Statement)] - | ^^^^^^^^^ undefined reference: `Statement` - "#); -} - #[test] fn tagged_alternation_simple() { let input = indoc! {r#" @@ -581,24 +561,6 @@ fn tagged_alternation_with_sequence() { "#); } -#[test] -fn mixed_tagged_and_untagged() { - let input = indoc! {r#" - [Tagged: (a) (b) Another: (c)] - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: mixed tagged and untagged branches in alternation - | - 1 | [Tagged: (a) (b) Another: (c)] - | ------ ^^^ mixed tagged and untagged branches in alternation - | | - | tagged branch here - "#); -} - #[test] fn tagged_alternation_with_nested_alternation() { let input = indoc! {r#" diff --git a/crates/plotnik-lib/src/ast/parser/tests/grammar/anchors.rs b/crates/plotnik-lib/src/ast/parser/tests/grammar/anchors_tests.rs similarity index 83% rename from crates/plotnik-lib/src/ast/parser/tests/grammar/anchors.rs rename to crates/plotnik-lib/src/ast/parser/tests/grammar/anchors_tests.rs index 9aee634c..39557ff4 100644 --- a/crates/plotnik-lib/src/ast/parser/tests/grammar/anchors.rs +++ b/crates/plotnik-lib/src/ast/parser/tests/grammar/anchors_tests.rs @@ -175,23 +175,3 @@ fn anchor_in_sequence() { BraceClose "}" "#); } - -#[test] -fn capture_space_after_dot_is_anchor() { - let input = indoc! {r#" - (identifier) @foo . (other) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: unnamed definition must be last in file; add a name: `Name = (identifier) @foo` - | - 1 | (identifier) @foo . (other) - | ^^^^^^^^^^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (identifier) @foo` - error: unnamed definition must be last in file; add a name: `Name = .` - | - 1 | (identifier) @foo . (other) - | ^ unnamed definition must be last in file; add a name: `Name = .` - "#); -} diff --git a/crates/plotnik-lib/src/ast/parser/tests/grammar/captures.rs b/crates/plotnik-lib/src/ast/parser/tests/grammar/captures_tests.rs similarity index 100% rename from crates/plotnik-lib/src/ast/parser/tests/grammar/captures.rs rename to crates/plotnik-lib/src/ast/parser/tests/grammar/captures_tests.rs diff --git a/crates/plotnik-lib/src/ast/parser/tests/grammar/definitions.rs b/crates/plotnik-lib/src/ast/parser/tests/grammar/definitions_tests.rs similarity index 76% rename from crates/plotnik-lib/src/ast/parser/tests/grammar/definitions.rs rename to crates/plotnik-lib/src/ast/parser/tests/grammar/definitions_tests.rs index 2c799d8e..efc19328 100644 --- a/crates/plotnik-lib/src/ast/parser/tests/grammar/definitions.rs +++ b/crates/plotnik-lib/src/ast/parser/tests/grammar/definitions_tests.rs @@ -367,58 +367,6 @@ fn named_def_with_type_annotation() { "#); } -#[test] -fn upper_ident_not_followed_by_equals_is_expression() { - let input = indoc! {r#" - (Expr) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: undefined reference: `Expr` - | - 1 | (Expr) - | ^^^^ undefined reference: `Expr` - "#); -} - -#[test] -fn bare_upper_ident_not_followed_by_equals_is_error() { - let input = indoc! {r#" - Expr - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - | - 1 | Expr - | ^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - "#); -} - -#[test] -fn named_def_missing_equals() { - let input = indoc! {r#" - Expr (identifier) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - | - 1 | Expr (identifier) - | ^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - error: unnamed definition must be last in file; add a name: `Name = Expr` - | - 1 | Expr (identifier) - | ^^^^ unnamed definition must be last in file; add a name: `Name = Expr` - "#); -} - #[test] fn unnamed_def_allowed_as_last() { let input = indoc! {r#" @@ -451,43 +399,3 @@ fn unnamed_def_allowed_as_last() { ParenClose ")" "#); } - -#[test] -fn unnamed_def_not_allowed_in_middle() { - let input = indoc! {r#" - (first) - Expr = (identifier) - (last) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: unnamed definition must be last in file; add a name: `Name = (first)` - | - 1 | (first) - | ^^^^^^^ unnamed definition must be last in file; add a name: `Name = (first)` - "#); -} - -#[test] -fn multiple_unnamed_defs_errors_for_all_but_last() { - let input = indoc! {r#" - (first) - (second) - (third) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: unnamed definition must be last in file; add a name: `Name = (first)` - | - 1 | (first) - | ^^^^^^^ unnamed definition must be last in file; add a name: `Name = (first)` - error: unnamed definition must be last in file; add a name: `Name = (second)` - | - 2 | (second) - | ^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (second)` - "#); -} diff --git a/crates/plotnik-lib/src/ast/parser/tests/grammar/fields.rs b/crates/plotnik-lib/src/ast/parser/tests/grammar/fields_tests.rs similarity index 100% rename from crates/plotnik-lib/src/ast/parser/tests/grammar/fields.rs rename to crates/plotnik-lib/src/ast/parser/tests/grammar/fields_tests.rs diff --git a/crates/plotnik-lib/src/ast/parser/tests/grammar/mod.rs b/crates/plotnik-lib/src/ast/parser/tests/grammar/mod.rs index 458881ba..e3d160ae 100644 --- a/crates/plotnik-lib/src/ast/parser/tests/grammar/mod.rs +++ b/crates/plotnik-lib/src/ast/parser/tests/grammar/mod.rs @@ -1,9 +1,10 @@ -mod alternations; -mod anchors; -mod captures; -mod definitions; -mod fields; -mod nodes; -mod quantifiers; -mod sequences; -mod special; +mod alternations_tests; +mod anchors_tests; +mod captures_tests; +mod definitions_tests; +mod fields_tests; +mod nodes_tests; +mod quantifiers_tests; +mod sequences_tests; +mod special_tests; +mod trivia_tests; diff --git a/crates/plotnik-lib/src/ast/parser/tests/grammar/nodes.rs b/crates/plotnik-lib/src/ast/parser/tests/grammar/nodes_tests.rs similarity index 91% rename from crates/plotnik-lib/src/ast/parser/tests/grammar/nodes.rs rename to crates/plotnik-lib/src/ast/parser/tests/grammar/nodes_tests.rs index 4ecf313c..429903bc 100644 --- a/crates/plotnik-lib/src/ast/parser/tests/grammar/nodes.rs +++ b/crates/plotnik-lib/src/ast/parser/tests/grammar/nodes_tests.rs @@ -117,28 +117,6 @@ fn sibling_children() { "#); } -#[test] -fn multiple_expressions() { - let input = indoc! {r#" - (identifier) - (string) - (number) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: unnamed definition must be last in file; add a name: `Name = (identifier)` - | - 1 | (identifier) - | ^^^^^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (identifier)` - error: unnamed definition must be last in file; add a name: `Name = (string)` - | - 2 | (string) - | ^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (string)` - "#); -} - #[test] fn wildcard() { let input = indoc! {r#" diff --git a/crates/plotnik-lib/src/ast/parser/tests/grammar/quantifiers.rs b/crates/plotnik-lib/src/ast/parser/tests/grammar/quantifiers_tests.rs similarity index 100% rename from crates/plotnik-lib/src/ast/parser/tests/grammar/quantifiers.rs rename to crates/plotnik-lib/src/ast/parser/tests/grammar/quantifiers_tests.rs diff --git a/crates/plotnik-lib/src/ast/parser/tests/grammar/sequences.rs b/crates/plotnik-lib/src/ast/parser/tests/grammar/sequences_tests.rs similarity index 100% rename from crates/plotnik-lib/src/ast/parser/tests/grammar/sequences.rs rename to crates/plotnik-lib/src/ast/parser/tests/grammar/sequences_tests.rs diff --git a/crates/plotnik-lib/src/ast/parser/tests/grammar/special.rs b/crates/plotnik-lib/src/ast/parser/tests/grammar/special_tests.rs similarity index 100% rename from crates/plotnik-lib/src/ast/parser/tests/grammar/special.rs rename to crates/plotnik-lib/src/ast/parser/tests/grammar/special_tests.rs diff --git a/crates/plotnik-lib/src/ast/parser/tests/trivia.rs b/crates/plotnik-lib/src/ast/parser/tests/grammar/trivia_tests.rs similarity index 88% rename from crates/plotnik-lib/src/ast/parser/tests/trivia.rs rename to crates/plotnik-lib/src/ast/parser/tests/grammar/trivia_tests.rs index a10720d5..fff7dbc1 100644 --- a/crates/plotnik-lib/src/ast/parser/tests/trivia.rs +++ b/crates/plotnik-lib/src/ast/parser/tests/grammar/trivia_tests.rs @@ -46,24 +46,6 @@ fn comment_preserved() { "#); } -#[test] -fn multiline() { - let input = indoc! {r#" - (a) - - (b) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: unnamed definition must be last in file; add a name: `Name = (a)` - | - 1 | (a) - | ^^^ unnamed definition must be last in file; add a name: `Name = (a)` - "#); -} - #[test] fn comment_inside_expression() { let input = indoc! {r#" diff --git a/crates/plotnik-lib/src/ast/parser/tests/mod.rs b/crates/plotnik-lib/src/ast/parser/tests/mod.rs index e454e496..39f4f702 100644 --- a/crates/plotnik-lib/src/ast/parser/tests/mod.rs +++ b/crates/plotnik-lib/src/ast/parser/tests/mod.rs @@ -1,6 +1,5 @@ mod grammar; mod recovery; -mod trivia; // JSON serialization tests for the error API mod json_serialization { diff --git a/crates/plotnik-lib/src/ast/parser/tests/recovery/coverage.rs b/crates/plotnik-lib/src/ast/parser/tests/recovery/coverage.rs deleted file mode 100644 index c4ba10ca..00000000 --- a/crates/plotnik-lib/src/ast/parser/tests/recovery/coverage.rs +++ /dev/null @@ -1,1030 +0,0 @@ -//! Additional tests for parser coverage gaps. - -use crate::Query; -use indoc::indoc; - -#[test] -fn named_def_missing_equals_with_garbage() { - let input = indoc! {r#" - Expr ^^^ (identifier) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - | - 1 | Expr ^^^ (identifier) - | ^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - | - 1 | Expr ^^^ (identifier) - | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - error: unnamed definition must be last in file; add a name: `Name = Expr` - | - 1 | Expr ^^^ (identifier) - | ^^^^ unnamed definition must be last in file; add a name: `Name = Expr` - "#); -} - -#[test] -fn named_def_missing_equals_recovers_to_next_def() { - let input = indoc! {r#" - Broken ^^^ - Valid = (ok) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - | - 1 | Broken ^^^ - | ^^^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - | - 1 | Broken ^^^ - | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - "#); -} - -#[test] -fn def_name_snake_case_suggests_pascal() { - let input = indoc! {r#" - my_expr = (identifier) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: definition names must start with uppercase - | - 1 | my_expr = (identifier) - | ^^^^^^^ definition names must start with uppercase - | - help: definition names must be PascalCase; use MyExpr instead - | - 1 - my_expr = (identifier) - 1 + MyExpr = (identifier) - | - "); -} - -#[test] -fn def_name_kebab_case_suggests_pascal() { - let input = indoc! {r#" - my-expr = (identifier) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: definition names must start with uppercase - | - 1 | my-expr = (identifier) - | ^^^^^^^ definition names must start with uppercase - | - help: definition names must be PascalCase; use MyExpr instead - | - 1 - my-expr = (identifier) - 1 + MyExpr = (identifier) - | - "); -} - -#[test] -fn def_name_dotted_suggests_pascal() { - let input = indoc! {r#" - my.expr = (identifier) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: definition names must start with uppercase - | - 1 | my.expr = (identifier) - | ^^^^^^^ definition names must start with uppercase - | - help: definition names must be PascalCase; use MyExpr instead - | - 1 - my.expr = (identifier) - 1 + MyExpr = (identifier) - | - "); -} - -#[test] -fn branch_label_snake_case_suggests_pascal() { - let input = indoc! {r#" - [My_branch: (a) Other: (b)] - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: branch labels cannot contain separators - | - 1 | [My_branch: (a) Other: (b)] - | ^^^^^^^^^ branch labels cannot contain separators - | - help: branch labels must be PascalCase; use MyBranch: instead - | - 1 - [My_branch: (a) Other: (b)] - 1 + [MyBranch:: (a) Other: (b)] - | - "); -} - -#[test] -fn branch_label_kebab_case_suggests_pascal() { - let input = indoc! {r#" - [My-branch: (a) Other: (b)] - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: branch labels cannot contain separators - | - 1 | [My-branch: (a) Other: (b)] - | ^^^^^^^^^ branch labels cannot contain separators - | - help: branch labels must be PascalCase; use MyBranch: instead - | - 1 - [My-branch: (a) Other: (b)] - 1 + [MyBranch:: (a) Other: (b)] - | - "); -} - -#[test] -fn branch_label_dotted_suggests_pascal() { - let input = indoc! {r#" - [My.branch: (a) Other: (b)] - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: branch labels cannot contain separators - | - 1 | [My.branch: (a) Other: (b)] - | ^^^^^^^^^ branch labels cannot contain separators - | - help: branch labels must be PascalCase; use MyBranch: instead - | - 1 - [My.branch: (a) Other: (b)] - 1 + [MyBranch:: (a) Other: (b)] - | - "); -} - -#[test] -fn type_annotation_dotted_suggests_pascal() { - let input = indoc! {r#" - (a) @x :: My.Type - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: type names cannot contain dots or hyphens - | - 1 | (a) @x :: My.Type - | ^^^^^^^ type names cannot contain dots or hyphens - | - help: type names cannot contain separators; use ::MyType instead - | - 1 - (a) @x :: My.Type - 1 + (a) @x :: ::MyType - | - "); -} - -#[test] -fn type_annotation_kebab_suggests_pascal() { - let input = indoc! {r#" - (a) @x :: My-Type - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: type names cannot contain dots or hyphens - | - 1 | (a) @x :: My-Type - | ^^^^^^^ type names cannot contain dots or hyphens - | - help: type names cannot contain separators; use ::MyType instead - | - 1 - (a) @x :: My-Type - 1 + (a) @x :: ::MyType - | - "); -} - -#[test] -fn lowercase_branch_label_suggests_capitalized() { - let input = indoc! {r#" - [first: (a) Second: (b)] - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: tagged alternation labels must be Capitalized (they map to enum variants) - | - 1 | [first: (a) Second: (b)] - | ^^^^^ tagged alternation labels must be Capitalized (they map to enum variants) - | - help: capitalize as `First` - | - 1 - [first: (a) Second: (b)] - 1 + [First: (a) Second: (b)] - | - "); -} - -#[test] -fn predicate_in_alternation() { - let input = indoc! {r#" - [(a) #eq? (b)] - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: unexpected token; expected a child expression or closing delimiter - | - 1 | [(a) #eq? (b)] - | ^^^^ unexpected token; expected a child expression or closing delimiter - "); -} - -#[test] -fn predicate_in_sequence() { - let input = indoc! {r#" - {(a) #set! (b)} - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported - | - 1 | {(a) #set! (b)} - | ^^^^^ tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported - "); -} - -#[test] -fn bare_capture_at_root() { - let input = indoc! {r#" - @name - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: capture '@' must follow an expression to capture - | - 1 | @name - | ^ capture '@' must follow an expression to capture - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - | - 1 | @name - | ^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - "); -} - -#[test] -fn capture_at_start_of_alternation() { - let input = indoc! {r#" - [@x (a)] - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: unexpected token; expected a child expression or closing delimiter - | - 1 | [@x (a)] - | ^ unexpected token; expected a child expression or closing delimiter - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - | - 1 | [@x (a)] - | ^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - "); -} - -#[test] -fn deeply_nested_trees_hit_recursion_limit() { - let depth = 128; - let mut input = String::new(); - for _ in 0..depth + 1 { - input.push_str("(a "); - } - for _ in 0..depth { - input.push(')'); - } - - let result = Query::builder(&input) - .with_recursion_fuel(Some(depth)) - .build(); - - assert!( - matches!(result, Err(crate::Error::RecursionLimitExceeded)), - "expected RecursionLimitExceeded error, got {:?}", - result - ); -} - -#[test] -fn deeply_nested_sequences_hit_recursion_limit() { - let depth = 128; - let mut input = String::new(); - for _ in 0..depth + 1 { - input.push_str("{(a) "); - } - for _ in 0..depth { - input.push('}'); - } - - let result = Query::builder(&input) - .with_recursion_fuel(Some(depth)) - .build(); - - assert!( - matches!(result, Err(crate::Error::RecursionLimitExceeded)), - "expected RecursionLimitExceeded error, got {:?}", - result - ); -} - -#[test] -fn deeply_nested_alternations_hit_recursion_limit() { - let depth = 128; - let mut input = String::new(); - for _ in 0..depth + 1 { - input.push_str("[(a) "); - } - for _ in 0..depth { - input.push(']'); - } - - let result = Query::builder(&input) - .with_recursion_fuel(Some(depth)) - .build(); - - assert!( - matches!(result, Err(crate::Error::RecursionLimitExceeded)), - "expected RecursionLimitExceeded error, got {:?}", - result - ); -} - -#[test] -fn unclosed_tree_shows_open_location() { - let input = indoc! {r#" - (call - (identifier) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: unclosed tree; expected ')' - | - 1 | (call - | - tree started here - 2 | (identifier) - | ^ unclosed tree; expected ')' - "); -} - -#[test] -fn unclosed_alternation_shows_open_location() { - let input = indoc! {r#" - [ - (a) - (b) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: unclosed alternation; expected ']' - | - 1 | [ - | - alternation started here - 2 | (a) - 3 | (b) - | ^ unclosed alternation; expected ']' - "); -} - -#[test] -fn unclosed_sequence_shows_open_location() { - let input = indoc! {r#" - { - (a) - (b) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: unclosed sequence; expected '}' - | - 1 | { - | - sequence started here - 2 | (a) - 3 | (b) - | ^ unclosed sequence; expected '}' - "); -} - -#[test] -fn single_colon_type_annotation_with_space() { - let input = indoc! {r#" - (a) @x : Type - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: single colon is not valid for type annotations - | - 1 | (a) @x : Type - | ^ single colon is not valid for type annotations - | - help: use '::' - | - 1 | (a) @x :: Type - | + - "); -} - -#[test] -fn field_equals_typo_in_tree() { - let input = indoc! {r#" - (call name = (identifier)) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: '=' is not valid for field constraints - | - 1 | (call name = (identifier)) - | ^ '=' is not valid for field constraints - | - help: use ':' - | - 1 - (call name = (identifier)) - 1 + (call name : (identifier)) - | - "); -} - -#[test] -fn field_equals_typo_missing_value() { - let input = indoc! {r#" - (call name = ) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: '=' is not valid for field constraints - | - 1 | (call name = ) - | ^ '=' is not valid for field constraints - | - help: use ':' - | - 1 - (call name = ) - 1 + (call name : ) - | - error: expected expression after field name - | - 1 | (call name = ) - | ^ expected expression after field name - "); -} - -#[test] -fn comma_between_defs() { - let input = indoc! {r#" - A = (a), B = (b) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - | - 1 | A = (a), B = (b) - | ^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - "#); -} - -#[test] -fn pipe_between_branches() { - let input = indoc! {r#" - [(a) | (b)] - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: '|' is not valid syntax; plotnik uses whitespace for separation - | - 1 | [(a) | (b)] - | ^ '|' is not valid syntax; plotnik uses whitespace for separation - | - help: remove separator - | - 1 - [(a) | (b)] - 1 + [(a) (b)] - | - "); -} - -#[test] -fn empty_double_quote_string() { - let input = indoc! {r#" - (a "") - "#}; - - let query = Query::new(input).unwrap(); - assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_cst(), @r#" - Root - Def - Tree - ParenOpen "(" - Id "a" - Str - DoubleQuote "\"" - DoubleQuote "\"" - ParenClose ")" - "#); -} - -#[test] -fn empty_single_quote_string() { - let input = indoc! {r#" - (a '') - "#}; - - let query = Query::new(input).unwrap(); - assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_cst(), @r#" - Root - Def - Tree - ParenOpen "(" - Id "a" - Str - SingleQuote "'" - SingleQuote "'" - ParenClose ")" - "#); -} - -#[test] -fn supertype_with_string_arg() { - let input = indoc! {r#" - (expression/binary) - "#}; - - let query = Query::new(input).unwrap(); - assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_cst(), @r#" - Root - Def - Tree - ParenOpen "(" - Id "expression" - Slash "/" - Id "binary" - ParenClose ")" - "#); -} - -#[test] -fn missing_node_syntax() { - let input = indoc! {r#" - (MISSING "identifier") - "#}; - - let query = Query::new(input).unwrap(); - assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_cst(), @r#" - Root - Def - Tree - ParenOpen "(" - KwMissing "MISSING" - DoubleQuote "\"" - StrVal "identifier" - DoubleQuote "\"" - ParenClose ")" - "#); -} - -#[test] -fn error_node_syntax() { - let input = indoc! {r#" - (ERROR) - "#}; - - let query = Query::new(input).unwrap(); - assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_cst(), @r#" - Root - Def - Tree - ParenOpen "(" - KwError "ERROR" - ParenClose ")" - "#); -} - -#[test] -fn capture_name_pascal_case_error() { - let input = indoc! {r#" - (a) @Name - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: capture names must start with lowercase - | - 1 | (a) @Name - | ^^^^ capture names must start with lowercase - | - help: capture names must be snake_case; use @name instead - | - 1 - (a) @Name - 1 + (a) @name - | - "); -} - -#[test] -fn capture_name_pascal_case_with_hyphens_error() { - // This tests to_snake_case with hyphens via the uppercase branch - let input = indoc! {r#" - (a) @My-Name - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: capture names cannot contain hyphens - | - 1 | (a) @My-Name - | ^^^^^^^ capture names cannot contain hyphens - | - help: captures become struct fields; use @my_name instead - | - 1 - (a) @My-Name - 1 + (a) @my_name - | - "); -} - -#[test] -fn capture_name_with_hyphens_error() { - let input = indoc! {r#" - (a) @my-name - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: capture names cannot contain hyphens - | - 1 | (a) @my-name - | ^^^^^^^ capture names cannot contain hyphens - | - help: captures become struct fields; use @my_name instead - | - 1 - (a) @my-name - 1 + (a) @my_name - | - "); -} - -#[test] -fn field_name_pascal_case_error() { - let input = indoc! {r#" - (call Name: (a)) - "#}; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: field names must start with lowercase - | - 1 | (call Name: (a)) - | ^^^^ field names must start with lowercase - | - help: field names must be snake_case; use name: instead - | - 1 - (call Name: (a)) - 1 + (call name:: (a)) - | - "); -} - -#[test] -fn bare_capture_at_eof_triggers_sync() { - // After error_and_bump consumes '@', we're at EOF - // synchronize_to_def_start should return false (eof branch) - let input = "@"; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: capture '@' must follow an expression to capture - | - 1 | @ - | ^ capture '@' must follow an expression to capture - "); -} - -#[test] -fn bare_colon_in_tree() { - let input = "(a : (b))"; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: unexpected token; expected a child expression or closing delimiter - | - 1 | (a : (b)) - | ^ unexpected token; expected a child expression or closing delimiter - "); -} - -#[test] -fn pipe_in_tree() { - let input = "(a | b)"; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: '|' is not valid syntax; plotnik uses whitespace for separation - | - 1 | (a | b) - | ^ '|' is not valid syntax; plotnik uses whitespace for separation - | - help: remove separator - | - 1 - (a | b) - 1 + (a b) - | - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - | - 1 | (a | b) - | ^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - "); -} - -#[test] -fn pipe_in_sequence() { - let input = "{(a) | (b)}"; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: '|' is not valid syntax; plotnik uses whitespace for separation - | - 1 | {(a) | (b)} - | ^ '|' is not valid syntax; plotnik uses whitespace for separation - | - help: remove separator - | - 1 - {(a) | (b)} - 1 + {(a) (b)} - | - "); -} - -#[test] -fn paren_close_inside_alternation() { - let input = "[(a) ) (b)]"; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: expected closing ']' for alternation - | - 1 | [(a) ) (b)] - | ^ expected closing ']' for alternation - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - | - 1 | [(a) ) (b)] - | ^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - error: unnamed definition must be last in file; add a name: `Name = [(a)` - | - 1 | [(a) ) (b)] - | ^^^^ unnamed definition must be last in file; add a name: `Name = [(a)` - "#); -} - -#[test] -fn type_annotation_missing_name() { - let input = "(a) @x ::"; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: expected type name after '::' (e.g., ::MyType or ::string) - | - 1 | (a) @x :: - | ^ expected type name after '::' (e.g., ::MyType or ::string) - "); -} - -#[test] -fn type_annotation_missing_name_with_bracket() { - let input = "[(a) @x :: ]"; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: expected type name after '::' (e.g., ::MyType or ::string) - | - 1 | [(a) @x :: ] - | ^ expected type name after '::' (e.g., ::MyType or ::string) - "); -} - -#[test] -fn predicate_in_tree() { - let input = "(function #eq? @name \"test\")"; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported - | - 1 | (function #eq? @name "test") - | ^^^^ tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported - error: unexpected token; expected a child expression or closing delimiter - | - 1 | (function #eq? @name "test") - | ^ unexpected token; expected a child expression or closing delimiter - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - | - 1 | (function #eq? @name "test") - | ^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - "#); -} - -#[test] -fn single_colon_type_annotation_followed_by_non_id() { - // @x : followed by ( which is not an Id - triggers early return in parse_type_annotation_single_colon - let input = "(a) @x : (b)"; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - | - 1 | (a) @x : (b) - | ^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - error: unnamed definition must be last in file; add a name: `Name = (a) @x` - | - 1 | (a) @x : (b) - | ^^^^^^ unnamed definition must be last in file; add a name: `Name = (a) @x` - "#); -} - -#[test] -fn single_colon_type_annotation_at_eof() { - // @x : at end of input - let input = "(a) @x :"; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - | - 1 | (a) @x : - | ^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - "#); -} - -#[test] -fn bracket_close_inside_sequence() { - // ] inside {} triggers SEQ_RECOVERY break - let input = "{(a) ] (b)}"; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: expected closing '}' for sequence - | - 1 | {(a) ] (b)} - | ^ expected closing '}' for sequence - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - | - 1 | {(a) ] (b)} - | ^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - error: unnamed definition must be last in file; add a name: `Name = {(a)` - | - 1 | {(a) ] (b)} - | ^^^^ unnamed definition must be last in file; add a name: `Name = {(a)` - "#); -} - -#[test] -fn paren_close_inside_sequence() { - // ) inside {} triggers SEQ_RECOVERY break - let input = "{(a) ) (b)}"; - - let query = Query::new(input).unwrap(); - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: expected closing '}' for sequence - | - 1 | {(a) ) (b)} - | ^ expected closing '}' for sequence - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - | - 1 | {(a) ) (b)} - | ^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - error: unnamed definition must be last in file; add a name: `Name = {(a)` - | - 1 | {(a) ) (b)} - | ^^^^ unnamed definition must be last in file; add a name: `Name = {(a)` - "#); -} - -#[test] -fn many_trees_exhaust_exec_fuel() { - let count = 500; - let mut input = String::new(); - for _ in 0..count { - input.push_str("(a) "); - } - - let result = Query::builder(&input).with_exec_fuel(Some(100)).build(); - - assert!( - matches!(result, Err(crate::Error::ExecFuelExhausted)), - "expected ExecFuelExhausted error, got {:?}", - result - ); -} - -#[test] -fn many_branches_exhaust_exec_fuel() { - let count = 500; - let mut input = String::new(); - input.push('['); - for i in 0..count { - if i > 0 { - input.push(' '); - } - input.push_str("(a)"); - } - input.push(']'); - - let result = Query::builder(&input).with_exec_fuel(Some(100)).build(); - - assert!( - matches!(result, Err(crate::Error::ExecFuelExhausted)), - "expected ExecFuelExhausted error, got {:?}", - result - ); -} - -#[test] -fn many_fields_exhaust_exec_fuel() { - let count = 500; - let mut input = String::new(); - input.push('('); - for i in 0..count { - if i > 0 { - input.push(' '); - } - input.push_str("a: (b)"); - } - input.push(')'); - - let result = Query::builder(&input).with_exec_fuel(Some(100)).build(); - - assert!( - matches!(result, Err(crate::Error::ExecFuelExhausted)), - "expected ExecFuelExhausted error, got {:?}", - result - ); -} diff --git a/crates/plotnik-lib/src/ast/parser/tests/recovery/coverage_tests.rs b/crates/plotnik-lib/src/ast/parser/tests/recovery/coverage_tests.rs new file mode 100644 index 00000000..07ba1396 --- /dev/null +++ b/crates/plotnik-lib/src/ast/parser/tests/recovery/coverage_tests.rs @@ -0,0 +1,301 @@ +use crate::Query; +use indoc::indoc; + +#[test] +fn deeply_nested_trees_hit_recursion_limit() { + let depth = 128; + let mut input = String::new(); + for _ in 0..depth + 1 { + input.push_str("(a "); + } + for _ in 0..depth { + input.push(')'); + } + + let result = Query::builder(&input) + .with_recursion_fuel(Some(depth)) + .build(); + + assert!( + matches!(result, Err(crate::Error::RecursionLimitExceeded)), + "expected RecursionLimitExceeded error, got {:?}", + result + ); +} + +#[test] +fn deeply_nested_sequences_hit_recursion_limit() { + let depth = 128; + let mut input = String::new(); + for _ in 0..depth + 1 { + input.push_str("{(a) "); + } + for _ in 0..depth { + input.push('}'); + } + + let result = Query::builder(&input) + .with_recursion_fuel(Some(depth)) + .build(); + + assert!( + matches!(result, Err(crate::Error::RecursionLimitExceeded)), + "expected RecursionLimitExceeded error, got {:?}", + result + ); +} + +#[test] +fn deeply_nested_alternations_hit_recursion_limit() { + let depth = 128; + let mut input = String::new(); + for _ in 0..depth + 1 { + input.push_str("[(a) "); + } + for _ in 0..depth { + input.push(']'); + } + + let result = Query::builder(&input) + .with_recursion_fuel(Some(depth)) + .build(); + + assert!( + matches!(result, Err(crate::Error::RecursionLimitExceeded)), + "expected RecursionLimitExceeded error, got {:?}", + result + ); +} + +#[test] +fn many_trees_exhaust_exec_fuel() { + let count = 500; + let mut input = String::new(); + for _ in 0..count { + input.push_str("(a) "); + } + + let result = Query::builder(&input).with_exec_fuel(Some(100)).build(); + + assert!( + matches!(result, Err(crate::Error::ExecFuelExhausted)), + "expected ExecFuelExhausted error, got {:?}", + result + ); +} + +#[test] +fn many_branches_exhaust_exec_fuel() { + let count = 500; + let mut input = String::new(); + input.push('['); + for i in 0..count { + if i > 0 { + input.push(' '); + } + input.push_str("(a)"); + } + input.push(']'); + + let result = Query::builder(&input).with_exec_fuel(Some(100)).build(); + + assert!( + matches!(result, Err(crate::Error::ExecFuelExhausted)), + "expected ExecFuelExhausted error, got {:?}", + result + ); +} + +#[test] +fn many_fields_exhaust_exec_fuel() { + let count = 500; + let mut input = String::new(); + input.push('('); + for i in 0..count { + if i > 0 { + input.push(' '); + } + input.push_str("a: (b)"); + } + input.push(')'); + + let result = Query::builder(&input).with_exec_fuel(Some(100)).build(); + + assert!( + matches!(result, Err(crate::Error::ExecFuelExhausted)), + "expected ExecFuelExhausted error, got {:?}", + result + ); +} + +#[test] +fn named_def_missing_equals_with_garbage() { + let input = indoc! {r#" + Expr ^^^ (identifier) + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r#" + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | + 1 | Expr ^^^ (identifier) + | ^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | + 1 | Expr ^^^ (identifier) + | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unnamed definition must be last in file; add a name: `Name = Expr` + | + 1 | Expr ^^^ (identifier) + | ^^^^ unnamed definition must be last in file; add a name: `Name = Expr` + "#); +} + +#[test] +fn named_def_missing_equals_recovers_to_next_def() { + let input = indoc! {r#" + Broken ^^^ + Valid = (ok) + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r#" + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | + 1 | Broken ^^^ + | ^^^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | + 1 | Broken ^^^ + | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + "#); +} + +#[test] +fn empty_double_quote_string() { + let input = indoc! {r#" + (a "") + "#}; + + let query = Query::new(input).unwrap(); + assert!(query.is_valid()); + insta::assert_snapshot!(query.dump_cst(), @r#" + Root + Def + Tree + ParenOpen "(" + Id "a" + Str + DoubleQuote "\"" + DoubleQuote "\"" + ParenClose ")" + "#); +} + +#[test] +fn empty_single_quote_string() { + let input = indoc! {r#" + (a '') + "#}; + + let query = Query::new(input).unwrap(); + assert!(query.is_valid()); + insta::assert_snapshot!(query.dump_cst(), @r#" + Root + Def + Tree + ParenOpen "(" + Id "a" + Str + SingleQuote "'" + SingleQuote "'" + ParenClose ")" + "#); +} + +#[test] +fn single_quote_string_is_valid() { + let input = "(node 'if')"; + + let query = Query::new(input).unwrap(); + assert!(query.is_valid()); + insta::assert_snapshot!(query.dump_cst(), @r#" + Root + Def + Tree + ParenOpen "(" + Id "node" + Str + SingleQuote "'" + StrVal "if" + SingleQuote "'" + ParenClose ")" + "#); +} + +#[test] +fn single_quote_in_alternation() { + let input = "['public' 'private']"; + + let query = Query::new(input).unwrap(); + assert!(query.is_valid()); + insta::assert_snapshot!(query.dump_cst(), @r#" + Root + Def + Alt + BracketOpen "[" + Branch + Str + SingleQuote "'" + StrVal "public" + SingleQuote "'" + Branch + Str + SingleQuote "'" + StrVal "private" + SingleQuote "'" + BracketClose "]" + "#); +} + +#[test] +fn single_quote_with_escape() { + let input = r"(node 'it\'s')"; + + let query = Query::new(input).unwrap(); + assert!(query.is_valid()); + insta::assert_snapshot!(query.dump_cst(), @r#" + Root + Def + Tree + ParenOpen "(" + Id "node" + Str + SingleQuote "'" + StrVal "it\\'s" + SingleQuote "'" + ParenClose ")" + "#); +} + +#[test] +fn missing_with_nested_tree_parses() { + let input = "(MISSING (something))"; + + let query = Query::new(input).unwrap(); + assert!(query.is_valid()); + insta::assert_snapshot!(query.dump_cst(), @r#" + Root + Def + Tree + ParenOpen "(" + KwMissing "MISSING" + Tree + ParenOpen "(" + Id "something" + ParenClose ")" + ParenClose ")" + "#); +} diff --git a/crates/plotnik-lib/src/ast/parser/tests/recovery/incomplete.rs b/crates/plotnik-lib/src/ast/parser/tests/recovery/incomplete_tests.rs similarity index 50% rename from crates/plotnik-lib/src/ast/parser/tests/recovery/incomplete.rs rename to crates/plotnik-lib/src/ast/parser/tests/recovery/incomplete_tests.rs index 7561167c..023ad38f 100644 --- a/crates/plotnik-lib/src/ast/parser/tests/recovery/incomplete.rs +++ b/crates/plotnik-lib/src/ast/parser/tests/recovery/incomplete_tests.rs @@ -9,12 +9,12 @@ fn missing_capture_name() { let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" + insta::assert_snapshot!(query.dump_errors(), @r" error: expected capture name after '@' | 1 | (identifier) @ | ^ expected capture name after '@' - "#); + "); } #[test] @@ -55,12 +55,12 @@ fn missing_type_name() { let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" + insta::assert_snapshot!(query.dump_errors(), @r" error: expected type name after '::' (e.g., ::MyType or ::string) | 1 | (identifier) @name :: | ^ expected type name after '::' (e.g., ::MyType or ::string) - "#); + "); } #[test] @@ -71,12 +71,12 @@ fn missing_negated_field_name() { let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" + insta::assert_snapshot!(query.dump_errors(), @r" error: expected field name after '!' (e.g., !value) | 1 | (call !) | ^ expected field name after '!' (e.g., !value) - "#); + "); } #[test] @@ -87,12 +87,12 @@ fn missing_subtype() { let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" + insta::assert_snapshot!(query.dump_errors(), @r" error: expected subtype after '/' (e.g., expression/binary_expression) | 1 | (expression/) | ^ expected subtype after '/' (e.g., expression/binary_expression) - "#); + "); } #[test] @@ -103,36 +103,40 @@ fn tagged_branch_missing_expression() { let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" + insta::assert_snapshot!(query.dump_errors(), @r" error: expected expression after branch label | 1 | [Label:] | ^ expected expression after branch label - "#); + "); } #[test] -fn mixed_valid_invalid_captures() { - let input = indoc! {r#" - (a) @ok @ @name - "#}; +fn type_annotation_missing_name_at_eof() { + let input = "(a) @x ::"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: capture '@' must follow an expression to capture - | - 1 | (a) @ok @ @name - | ^ capture '@' must follow an expression to capture - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + insta::assert_snapshot!(query.dump_errors(), @r" + error: expected type name after '::' (e.g., ::MyType or ::string) | - 1 | (a) @ok @ @name - | ^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - error: unnamed definition must be last in file; add a name: `Name = (a) @ok` + 1 | (a) @x :: + | ^ expected type name after '::' (e.g., ::MyType or ::string) + "); +} + +#[test] +fn type_annotation_missing_name_with_bracket() { + let input = "[(a) @x :: ]"; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: expected type name after '::' (e.g., ::MyType or ::string) | - 1 | (a) @ok @ @name - | ^^^^^^^ unnamed definition must be last in file; add a name: `Name = (a) @ok` - "#); + 1 | [(a) @x :: ] + | ^ expected type name after '::' (e.g., ::MyType or ::string) + "); } #[test] @@ -162,49 +166,161 @@ fn type_annotation_invalid_token_after() { } #[test] -fn error_with_unexpected_content() { +fn field_value_is_garbage() { let input = indoc! {r#" - (ERROR (something)) + (call name: %%%) "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: (ERROR) takes no arguments + insta::assert_snapshot!(query.dump_errors(), @r" + error: expected expression after field name | - 1 | (ERROR (something)) - | ^ (ERROR) takes no arguments - "#); + 1 | (call name: %%%) + | ^^^ expected expression after field name + "); } #[test] -fn bare_error_keyword() { +fn capture_with_invalid_char() { let input = indoc! {r#" - ERROR + (identifier) @123 "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...) + insta::assert_snapshot!(query.dump_errors(), @r" + error: expected capture name after '@' | - 1 | ERROR - | ^^^^^ ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...) - "#); + 1 | (identifier) @123 + | ^^^ expected capture name after '@' + "); } #[test] -fn bare_missing_keyword() { +fn bare_capture_at_eof_triggers_sync() { + let input = "@"; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: capture '@' must follow an expression to capture + | + 1 | @ + | ^ capture '@' must follow an expression to capture + "); +} + +#[test] +fn bare_capture_at_root() { let input = indoc! {r#" - MISSING + @name "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...) + insta::assert_snapshot!(query.dump_errors(), @r" + error: capture '@' must follow an expression to capture + | + 1 | @name + | ^ capture '@' must follow an expression to capture + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | - 1 | MISSING - | ^^^^^^^ ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...) - "#); + 1 | @name + | ^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + "); +} + +#[test] +fn capture_at_start_of_alternation() { + let input = indoc! {r#" + [@x (a)] + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: unexpected token; expected a child expression or closing delimiter + | + 1 | [@x (a)] + | ^ unexpected token; expected a child expression or closing delimiter + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | + 1 | [@x (a)] + | ^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + "); +} + +#[test] +fn mixed_valid_invalid_captures() { + let input = indoc! {r#" + (a) @ok @ @name + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: capture '@' must follow an expression to capture + | + 1 | (a) @ok @ @name + | ^ capture '@' must follow an expression to capture + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | + 1 | (a) @ok @ @name + | ^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: unnamed definition must be last in file; add a name: `Name = (a) @ok` + | + 1 | (a) @ok @ @name + | ^^^^^^^ unnamed definition must be last in file; add a name: `Name = (a) @ok` + "); +} + +#[test] +fn field_equals_typo_missing_value() { + let input = indoc! {r#" + (call name = ) + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: '=' is not valid for field constraints + | + 1 | (call name = ) + | ^ '=' is not valid for field constraints + | + help: use ':' + | + 1 - (call name = ) + 1 + (call name : ) + | + error: expected expression after field name + | + 1 | (call name = ) + | ^ expected expression after field name + "); +} + +#[test] +fn lowercase_branch_label_missing_expression() { + let input = "[label:]"; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: tagged alternation labels must be Capitalized (they map to enum variants) + | + 1 | [label:] + | ^^^^^ tagged alternation labels must be Capitalized (they map to enum variants) + | + help: capitalize as `Label` + | + 1 - [label:] + 1 + [Label:] + | + error: expected expression after branch label + | + 1 | [label:] + | ^ expected expression after branch label + "); } diff --git a/crates/plotnik-lib/src/ast/parser/tests/recovery/mod.rs b/crates/plotnik-lib/src/ast/parser/tests/recovery/mod.rs index cd4bc9cc..95a17cfa 100644 --- a/crates/plotnik-lib/src/ast/parser/tests/recovery/mod.rs +++ b/crates/plotnik-lib/src/ast/parser/tests/recovery/mod.rs @@ -1,5 +1,5 @@ -mod coverage; -mod incomplete; -mod unclosed; -mod unexpected; -mod validation; +mod coverage_tests; +mod incomplete_tests; +mod unclosed_tests; +mod unexpected_tests; +mod validation_tests; diff --git a/crates/plotnik-lib/src/ast/parser/tests/recovery/unclosed.rs b/crates/plotnik-lib/src/ast/parser/tests/recovery/unclosed_tests.rs similarity index 51% rename from crates/plotnik-lib/src/ast/parser/tests/recovery/unclosed.rs rename to crates/plotnik-lib/src/ast/parser/tests/recovery/unclosed_tests.rs index 76bc6e7b..c5328091 100644 --- a/crates/plotnik-lib/src/ast/parser/tests/recovery/unclosed.rs +++ b/crates/plotnik-lib/src/ast/parser/tests/recovery/unclosed_tests.rs @@ -27,14 +27,14 @@ fn missing_bracket() { let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" + insta::assert_snapshot!(query.dump_errors(), @r" error: unclosed alternation; expected ']' | 1 | [(identifier) (string) | - ^ unclosed alternation; expected ']' | | | alternation started here - "#); + "); } #[test] @@ -117,10 +117,111 @@ fn empty_parens() { let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" + insta::assert_snapshot!(query.dump_errors(), @r" error: empty tree expression - expected node type or children | 1 | () | ^ empty tree expression - expected node type or children + "); +} + +#[test] +fn unclosed_tree_shows_open_location() { + let input = indoc! {r#" + (call + (identifier) + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: unclosed tree; expected ')' + | + 1 | (call + | - tree started here + 2 | (identifier) + | ^ unclosed tree; expected ')' + "); +} + +#[test] +fn unclosed_alternation_shows_open_location() { + let input = indoc! {r#" + [ + (a) + (b) + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: unclosed alternation; expected ']' + | + 1 | [ + | - alternation started here + 2 | (a) + 3 | (b) + | ^ unclosed alternation; expected ']' + "); +} + +#[test] +fn unclosed_sequence_shows_open_location() { + let input = indoc! {r#" + { + (a) + (b) + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: unclosed sequence; expected '}' + | + 1 | { + | - sequence started here + 2 | (a) + 3 | (b) + | ^ unclosed sequence; expected '}' + "); +} + +#[test] +fn unclosed_double_quote_string() { + let input = r#"(call "foo)"#; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r#" + error: unexpected token; expected a child expression or closing delimiter + | + 1 | (call "foo) + | ^^^^^ unexpected token; expected a child expression or closing delimiter + error: unclosed tree; expected ')' + | + 1 | (call "foo) + | - ^ unclosed tree; expected ')' + | | + | tree started here "#); } + +#[test] +fn unclosed_single_quote_string() { + let input = "(call 'foo)"; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: unexpected token; expected a child expression or closing delimiter + | + 1 | (call 'foo) + | ^^^^^ unexpected token; expected a child expression or closing delimiter + error: unclosed tree; expected ')' + | + 1 | (call 'foo) + | - ^ unclosed tree; expected ')' + | | + | tree started here + "); +} diff --git a/crates/plotnik-lib/src/ast/parser/tests/recovery/unexpected.rs b/crates/plotnik-lib/src/ast/parser/tests/recovery/unexpected_tests.rs similarity index 63% rename from crates/plotnik-lib/src/ast/parser/tests/recovery/unexpected.rs rename to crates/plotnik-lib/src/ast/parser/tests/recovery/unexpected_tests.rs index d03739c9..df38ff0d 100644 --- a/crates/plotnik-lib/src/ast/parser/tests/recovery/unexpected.rs +++ b/crates/plotnik-lib/src/ast/parser/tests/recovery/unexpected_tests.rs @@ -77,12 +77,12 @@ fn garbage_inside_alternation() { let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" + insta::assert_snapshot!(query.dump_errors(), @r" error: unexpected token; expected a child expression or closing delimiter | 1 | [(a) ^^^ (b)] | ^^^ unexpected token; expected a child expression or closing delimiter - "#); + "); } #[test] @@ -93,7 +93,7 @@ fn garbage_inside_node() { let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" + insta::assert_snapshot!(query.dump_errors(), @r" error: expected capture name after '@' | 1 | (a (b) @@@ (c)) (d) @@ -106,7 +106,7 @@ fn garbage_inside_node() { | 1 | (a (b) @@@ (c)) (d) | ^^^^^^^^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (a (b) @@@ (c))` - "#); + "); } #[test] @@ -153,7 +153,7 @@ fn predicate_unsupported() { let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r##" + insta::assert_snapshot!(query.dump_errors(), @r#" error: tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported | 1 | (a (#eq? @x "foo") b) @@ -170,7 +170,7 @@ fn predicate_unsupported() { | 1 | (a (#eq? @x "foo") b) | ^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - "##); + "#); } #[test] @@ -181,7 +181,7 @@ fn predicate_match() { let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r##" + insta::assert_snapshot!(query.dump_errors(), @r#" error: tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported | 1 | (identifier) #match? @name "test" @@ -198,7 +198,61 @@ fn predicate_match() { | 1 | (identifier) #match? @name "test" | ^^^^ unnamed definition must be last in file; add a name: `Name = name` - "##); + "#); +} + +#[test] +fn predicate_in_tree() { + let input = "(function #eq? @name \"test\")"; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r#" + error: tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported + | + 1 | (function #eq? @name "test") + | ^^^^ tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported + error: unexpected token; expected a child expression or closing delimiter + | + 1 | (function #eq? @name "test") + | ^ unexpected token; expected a child expression or closing delimiter + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | + 1 | (function #eq? @name "test") + | ^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + "#); +} + +#[test] +fn predicate_in_alternation() { + let input = indoc! {r#" + [(a) #eq? (b)] + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: unexpected token; expected a child expression or closing delimiter + | + 1 | [(a) #eq? (b)] + | ^^^^ unexpected token; expected a child expression or closing delimiter + "); +} + +#[test] +fn predicate_in_sequence() { + let input = indoc! {r#" + {(a) #set! (b)} + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported + | + 1 | {(a) #set! (b)} + | ^^^^^ tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported + "); } #[test] @@ -211,7 +265,7 @@ fn multiline_garbage_recovery() { let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" + insta::assert_snapshot!(query.dump_errors(), @r" error: unexpected token; expected a child expression or closing delimiter | 2 | ^^^ @@ -220,39 +274,47 @@ fn multiline_garbage_recovery() { | 3 | b) | ^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - "#); + "); } #[test] -fn capture_with_invalid_char() { +fn top_level_garbage_recovery() { let input = indoc! {r#" - (identifier) @123 + Expr = (a) ^^^ Expr2 = (b) "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r#" - error: expected capture name after '@' + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | - 1 | (identifier) @123 - | ^^^ expected capture name after '@' + 1 | Expr = (a) ^^^ Expr2 = (b) + | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ "#); } #[test] -fn field_value_is_garbage() { +fn multiple_definitions_with_garbage_between() { let input = indoc! {r#" - (call name: %%%) + A = (a) + ^^^ + B = (b) + $$$ + C = (c) "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r" - error: expected expression after field name + insta::assert_snapshot!(query.dump_errors(), @r#" + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | - 1 | (call name: %%%) - | ^^^ expected expression after field name - "); + 2 | ^^^ + | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | + 4 | $$$ + | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + "#); } #[test] @@ -263,7 +325,7 @@ fn alternation_recovery_to_capture() { let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" + insta::assert_snapshot!(query.dump_errors(), @r" error: unexpected token; expected a child expression or closing delimiter | 1 | [^^^ @name] @@ -276,13 +338,13 @@ fn alternation_recovery_to_capture() { | 1 | [^^^ @name] | ^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - "#); + "); } #[test] -fn top_level_garbage_recovery() { +fn comma_between_defs() { let input = indoc! {r#" - Expr = (a) ^^^ Expr2 = (b) + A = (a), B = (b) "#}; let query = Query::new(input).unwrap(); @@ -290,31 +352,119 @@ fn top_level_garbage_recovery() { insta::assert_snapshot!(query.dump_errors(), @r#" error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | - 1 | Expr = (a) ^^^ Expr2 = (b) - | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + 1 | A = (a), B = (b) + | ^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ "#); } #[test] -fn multiple_definitions_with_garbage_between() { - let input = indoc! {r#" - A = (a) - ^^^ - B = (b) - $$$ - C = (c) - "#}; +fn bare_colon_in_tree() { + let input = "(a : (b))"; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: unexpected token; expected a child expression or closing delimiter + | + 1 | (a : (b)) + | ^ unexpected token; expected a child expression or closing delimiter + "); +} + +#[test] +fn paren_close_inside_alternation() { + let input = "[(a) ) (b)]"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r#" + error: expected closing ']' for alternation + | + 1 | [(a) ) (b)] + | ^ expected closing ']' for alternation error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | - 2 | ^^^ - | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + 1 | [(a) ) (b)] + | ^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unnamed definition must be last in file; add a name: `Name = [(a)` + | + 1 | [(a) ) (b)] + | ^^^^ unnamed definition must be last in file; add a name: `Name = [(a)` + "#); +} + +#[test] +fn bracket_close_inside_sequence() { + let input = "{(a) ] (b)}"; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r#" + error: expected closing '}' for sequence + | + 1 | {(a) ] (b)} + | ^ expected closing '}' for sequence error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | - 4 | $$$ - | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + 1 | {(a) ] (b)} + | ^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unnamed definition must be last in file; add a name: `Name = {(a)` + | + 1 | {(a) ] (b)} + | ^^^^ unnamed definition must be last in file; add a name: `Name = {(a)` + "#); +} + +#[test] +fn paren_close_inside_sequence() { + let input = "{(a) ) (b)}"; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r#" + error: expected closing '}' for sequence + | + 1 | {(a) ) (b)} + | ^ expected closing '}' for sequence + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | + 1 | {(a) ) (b)} + | ^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unnamed definition must be last in file; add a name: `Name = {(a)` + | + 1 | {(a) ) (b)} + | ^^^^ unnamed definition must be last in file; add a name: `Name = {(a)` + "#); +} + +#[test] +fn single_colon_type_annotation_followed_by_non_id() { + let input = "(a) @x : (b)"; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r#" + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | + 1 | (a) @x : (b) + | ^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unnamed definition must be last in file; add a name: `Name = (a) @x` + | + 1 | (a) @x : (b) + | ^^^^^^ unnamed definition must be last in file; add a name: `Name = (a) @x` + "#); +} + +#[test] +fn single_colon_type_annotation_at_eof() { + let input = "(a) @x :"; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r#" + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | + 1 | (a) @x : + | ^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ "#); } diff --git a/crates/plotnik-lib/src/ast/parser/tests/recovery/validation.rs b/crates/plotnik-lib/src/ast/parser/tests/recovery/validation_tests.rs similarity index 58% rename from crates/plotnik-lib/src/ast/parser/tests/recovery/validation.rs rename to crates/plotnik-lib/src/ast/parser/tests/recovery/validation_tests.rs index 7c6f8aba..76c61ff7 100644 --- a/crates/plotnik-lib/src/ast/parser/tests/recovery/validation.rs +++ b/crates/plotnik-lib/src/ast/parser/tests/recovery/validation_tests.rs @@ -10,12 +10,12 @@ fn ref_with_children_error() { let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" + insta::assert_snapshot!(query.dump_errors(), @r" error: reference `Expr` cannot contain children | 2 | (Expr (child)) | ^^^^^^^ reference `Expr` cannot contain children - "#); + "); } #[test] @@ -27,12 +27,12 @@ fn ref_with_multiple_children_error() { let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" + insta::assert_snapshot!(query.dump_errors(), @r" error: reference `Expr` cannot contain children | 2 | (Expr (a) (b) @cap) | ^^^^^^^^^^^^ reference `Expr` cannot contain children - "#); + "); } #[test] @@ -44,1097 +44,1272 @@ fn ref_with_field_children_error() { let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" + insta::assert_snapshot!(query.dump_errors(), @r" error: reference `Expr` cannot contain children | 2 | (Expr name: (identifier)) | ^^^^^^^^^^^^^^^^^^ reference `Expr` cannot contain children - "#); + "); +} + +#[test] +fn reference_with_supertype_syntax_error() { + let input = "(RefName/subtype)"; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: references cannot use supertype syntax (/) + | + 1 | (RefName/subtype) + | ^ references cannot use supertype syntax (/) + "); } #[test] -fn ref_without_children_is_valid() { +fn mixed_tagged_and_untagged() { let input = indoc! {r#" - Expr = (identifier) - (program (Expr) @e) + [Tagged: (a) (b) Another: (c)] "#}; let query = Query::new(input).unwrap(); - assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_cst(), @r#" - Root - Def - Id "Expr" - Equals "=" - Tree - ParenOpen "(" - Id "identifier" - ParenClose ")" - Def - Tree - ParenOpen "(" - Id "program" - Capture - Ref - ParenOpen "(" - Id "Expr" - ParenClose ")" - At "@" - Id "e" - ParenClose ")" - "#); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: mixed tagged and untagged branches in alternation + | + 1 | [Tagged: (a) (b) Another: (c)] + | ------ ^^^ mixed tagged and untagged branches in alternation + | | + | tagged branch here + "); } #[test] -fn capture_dotted_error() { +fn error_with_unexpected_content() { let input = indoc! {r#" - (identifier) @foo.bar + (ERROR (something)) "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: capture names cannot contain dots - | - 1 | (identifier) @foo.bar - | ^^^^^^^ capture names cannot contain dots - | - help: captures become struct fields; use @foo_bar instead - | - 1 - (identifier) @foo.bar - 1 + (identifier) @foo_bar + insta::assert_snapshot!(query.dump_errors(), @r" + error: (ERROR) takes no arguments | - "#); + 1 | (ERROR (something)) + | ^ (ERROR) takes no arguments + "); } #[test] -fn capture_dotted_multiple_parts() { +fn bare_error_keyword() { let input = indoc! {r#" - (identifier) @foo.bar.baz + ERROR "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: capture names cannot contain dots - | - 1 | (identifier) @foo.bar.baz - | ^^^^^^^^^^^ capture names cannot contain dots - | - help: captures become struct fields; use @foo_bar_baz instead - | - 1 - (identifier) @foo.bar.baz - 1 + (identifier) @foo_bar_baz + insta::assert_snapshot!(query.dump_errors(), @r" + error: ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...) | - "#); + 1 | ERROR + | ^^^^^ ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...) + "); } #[test] -fn capture_dotted_followed_by_field() { +fn bare_missing_keyword() { let input = indoc! {r#" - (node) @foo.bar name: (other) + MISSING "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: capture names cannot contain dots - | - 1 | (node) @foo.bar name: (other) - | ^^^^^^^ capture names cannot contain dots - | - help: captures become struct fields; use @foo_bar instead - | - 1 - (node) @foo.bar name: (other) - 1 + (node) @foo_bar name: (other) - | - error: unnamed definition must be last in file; add a name: `Name = (node) @foo.bar` + insta::assert_snapshot!(query.dump_errors(), @r" + error: ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...) | - 1 | (node) @foo.bar name: (other) - | ^^^^^^^^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (node) @foo.bar` - "#); + 1 | MISSING + | ^^^^^^^ ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...) + "); } #[test] -fn capture_space_after_dot_breaks_chain() { +fn upper_ident_in_alternation_not_followed_by_colon() { let input = indoc! {r#" - (identifier) @foo. bar + [(Expr) (Statement)] "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: capture names cannot contain dots - | - 1 | (identifier) @foo. bar - | ^^^^ capture names cannot contain dots - | - help: captures become struct fields; use @foo_ instead - | - 1 - (identifier) @foo. bar - 1 + (identifier) @foo_ bar - | - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + insta::assert_snapshot!(query.dump_errors(), @r" + error: undefined reference: `Expr` | - 1 | (identifier) @foo. bar - | ^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - error: unnamed definition must be last in file; add a name: `Name = (identifier) @foo.` + 1 | [(Expr) (Statement)] + | ^^^^ undefined reference: `Expr` + error: undefined reference: `Statement` | - 1 | (identifier) @foo. bar - | ^^^^^^^^^^^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (identifier) @foo.` - "#); + 1 | [(Expr) (Statement)] + | ^^^^^^^^^ undefined reference: `Statement` + "); } #[test] -fn capture_hyphenated_error() { +fn upper_ident_not_followed_by_equals_is_expression() { let input = indoc! {r#" - (identifier) @foo-bar + (Expr) "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: capture names cannot contain hyphens - | - 1 | (identifier) @foo-bar - | ^^^^^^^ capture names cannot contain hyphens - | - help: captures become struct fields; use @foo_bar instead - | - 1 - (identifier) @foo-bar - 1 + (identifier) @foo_bar + insta::assert_snapshot!(query.dump_errors(), @r" + error: undefined reference: `Expr` | - "#); + 1 | (Expr) + | ^^^^ undefined reference: `Expr` + "); } #[test] -fn capture_hyphenated_multiple() { +fn bare_upper_ident_not_followed_by_equals_is_error() { let input = indoc! {r#" - (identifier) @foo-bar-baz + Expr "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: capture names cannot contain hyphens - | - 1 | (identifier) @foo-bar-baz - | ^^^^^^^^^^^ capture names cannot contain hyphens - | - help: captures become struct fields; use @foo_bar_baz instead - | - 1 - (identifier) @foo-bar-baz - 1 + (identifier) @foo_bar_baz + insta::assert_snapshot!(query.dump_errors(), @r" + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | - "#); + 1 | Expr + | ^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + "); } #[test] -fn capture_mixed_dots_and_hyphens() { +fn named_def_missing_equals() { let input = indoc! {r#" - (identifier) @foo.bar-baz + Expr (identifier) "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: capture names cannot contain dots - | - 1 | (identifier) @foo.bar-baz - | ^^^^^^^^^^^ capture names cannot contain dots - | - help: captures become struct fields; use @foo_bar_baz instead + insta::assert_snapshot!(query.dump_errors(), @r" + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | - 1 - (identifier) @foo.bar-baz - 1 + (identifier) @foo_bar_baz + 1 | Expr (identifier) + | ^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: unnamed definition must be last in file; add a name: `Name = Expr` | - "#); + 1 | Expr (identifier) + | ^^^^ unnamed definition must be last in file; add a name: `Name = Expr` + "); } #[test] -fn single_quote_string_is_valid() { - let input = "(node 'if')"; +fn unnamed_def_not_allowed_in_middle() { + let input = indoc! {r#" + (first) + Expr = (identifier) + (last) + "#}; let query = Query::new(input).unwrap(); - assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_cst(), @r#" - Root - Def - Tree - ParenOpen "(" - Id "node" - Str - SingleQuote "'" - StrVal "if" - SingleQuote "'" - ParenClose ")" - "#); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: unnamed definition must be last in file; add a name: `Name = (first)` + | + 1 | (first) + | ^^^^^^^ unnamed definition must be last in file; add a name: `Name = (first)` + "); } #[test] -fn single_quote_in_alternation() { - let input = "['public' 'private']"; +fn multiple_unnamed_defs_errors_for_all_but_last() { + let input = indoc! {r#" + (first) + (second) + (third) + "#}; let query = Query::new(input).unwrap(); - assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_cst(), @r#" - Root - Def - Alt - BracketOpen "[" - Branch - Str - SingleQuote "'" - StrVal "public" - SingleQuote "'" - Branch - Str - SingleQuote "'" - StrVal "private" - SingleQuote "'" - BracketClose "]" - "#); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: unnamed definition must be last in file; add a name: `Name = (first)` + | + 1 | (first) + | ^^^^^^^ unnamed definition must be last in file; add a name: `Name = (first)` + error: unnamed definition must be last in file; add a name: `Name = (second)` + | + 2 | (second) + | ^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (second)` + "); } #[test] -fn single_quote_with_escape() { - let input = r"(node 'it\'s')"; +fn capture_space_after_dot_is_anchor() { + let input = indoc! {r#" + (identifier) @foo . (other) + "#}; let query = Query::new(input).unwrap(); - assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_cst(), @r#" - Root - Def - Tree - ParenOpen "(" - Id "node" - Str - SingleQuote "'" - StrVal "it\\'s" - SingleQuote "'" - ParenClose ")" - "#); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: unnamed definition must be last in file; add a name: `Name = (identifier) @foo` + | + 1 | (identifier) @foo . (other) + | ^^^^^^^^^^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (identifier) @foo` + error: unnamed definition must be last in file; add a name: `Name = .` + | + 1 | (identifier) @foo . (other) + | ^ unnamed definition must be last in file; add a name: `Name = .` + "); } #[test] -fn comma_in_node_children() { - let input = "(node (a), (b))"; +fn def_name_lowercase_error() { + let input = "lowercase = (x)"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: ',' is not valid syntax; plotnik uses whitespace for separation + insta::assert_snapshot!(query.dump_errors(), @r" + error: definition names must start with uppercase | - 1 | (node (a), (b)) - | ^ ',' is not valid syntax; plotnik uses whitespace for separation + 1 | lowercase = (x) + | ^^^^^^^^^ definition names must start with uppercase | - help: remove separator + help: definition names must be PascalCase; use Lowercase instead | - 1 - (node (a), (b)) - 1 + (node (a) (b)) + 1 - lowercase = (x) + 1 + Lowercase = (x) | - "#); + "); } #[test] -fn comma_in_alternation() { - let input = "[(a), (b), (c)]"; +fn def_name_snake_case_suggests_pascal() { + let input = indoc! {r#" + my_expr = (identifier) + "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: ',' is not valid syntax; plotnik uses whitespace for separation - | - 1 | [(a), (b), (c)] - | ^ ',' is not valid syntax; plotnik uses whitespace for separation - | - help: remove separator - | - 1 - [(a), (b), (c)] - 1 + [(a) (b), (c)] - | - error: ',' is not valid syntax; plotnik uses whitespace for separation + insta::assert_snapshot!(query.dump_errors(), @r" + error: definition names must start with uppercase | - 1 | [(a), (b), (c)] - | ^ ',' is not valid syntax; plotnik uses whitespace for separation + 1 | my_expr = (identifier) + | ^^^^^^^ definition names must start with uppercase | - help: remove separator + help: definition names must be PascalCase; use MyExpr instead | - 1 - [(a), (b), (c)] - 1 + [(a), (b) (c)] + 1 - my_expr = (identifier) + 1 + MyExpr = (identifier) | - "#); + "); } #[test] -fn pipe_in_alternation() { - let input = "[(a) | (b) | (c)]"; +fn def_name_kebab_case_suggests_pascal() { + let input = indoc! {r#" + my-expr = (identifier) + "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: '|' is not valid syntax; plotnik uses whitespace for separation - | - 1 | [(a) | (b) | (c)] - | ^ '|' is not valid syntax; plotnik uses whitespace for separation - | - help: remove separator - | - 1 - [(a) | (b) | (c)] - 1 + [(a) (b) | (c)] - | - error: '|' is not valid syntax; plotnik uses whitespace for separation + insta::assert_snapshot!(query.dump_errors(), @r" + error: definition names must start with uppercase | - 1 | [(a) | (b) | (c)] - | ^ '|' is not valid syntax; plotnik uses whitespace for separation + 1 | my-expr = (identifier) + | ^^^^^^^ definition names must start with uppercase | - help: remove separator + help: definition names must be PascalCase; use MyExpr instead | - 1 - [(a) | (b) | (c)] - 1 + [(a) | (b) (c)] + 1 - my-expr = (identifier) + 1 + MyExpr = (identifier) | - "#); + "); } #[test] -fn comma_in_sequence() { - let input = "{(a), (b)}"; +fn def_name_dotted_suggests_pascal() { + let input = indoc! {r#" + my.expr = (identifier) + "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: ',' is not valid syntax; plotnik uses whitespace for separation + insta::assert_snapshot!(query.dump_errors(), @r" + error: definition names must start with uppercase | - 1 | {(a), (b)} - | ^ ',' is not valid syntax; plotnik uses whitespace for separation + 1 | my.expr = (identifier) + | ^^^^^^^ definition names must start with uppercase | - help: remove separator + help: definition names must be PascalCase; use MyExpr instead | - 1 - {(a), (b)} - 1 + {(a) (b)} + 1 - my.expr = (identifier) + 1 + MyExpr = (identifier) | - "#); + "); } #[test] -fn single_colon_type_annotation() { - let input = "(identifier) @name : Type"; +fn def_name_with_underscores_error() { + let input = "Some_Thing = (x)"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: single colon is not valid for type annotations + insta::assert_snapshot!(query.dump_errors(), @r" + error: definition names cannot contain separators | - 1 | (identifier) @name : Type - | ^ single colon is not valid for type annotations + 1 | Some_Thing = (x) + | ^^^^^^^^^^ definition names cannot contain separators | - help: use '::' + help: definition names must be PascalCase; use SomeThing instead | - 1 | (identifier) @name :: Type - | + - "#); + 1 - Some_Thing = (x) + 1 + SomeThing = (x) + | + "); } #[test] -fn single_colon_type_annotation_no_space() { - let input = "(identifier) @name:Type"; +fn def_name_with_hyphens_error() { + let input = "Some-Thing = (x)"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: single colon is not valid for type annotations + insta::assert_snapshot!(query.dump_errors(), @r" + error: definition names cannot contain separators | - 1 | (identifier) @name:Type - | ^ single colon is not valid for type annotations + 1 | Some-Thing = (x) + | ^^^^^^^^^^ definition names cannot contain separators | - help: use '::' + help: definition names must be PascalCase; use SomeThing instead | - 1 | (identifier) @name::Type - | + - "#); + 1 - Some-Thing = (x) + 1 + SomeThing = (x) + | + "); } #[test] -fn single_colon_primitive_type() { - let input = "@val : string"; +fn capture_name_pascal_case_error() { + let input = indoc! {r#" + (a) @Name + "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r" - error: capture '@' must follow an expression to capture + error: capture names must start with lowercase | - 1 | @val : string - | ^ capture '@' must follow an expression to capture - error: expected ':' to separate field name from its value + 1 | (a) @Name + | ^^^^ capture names must start with lowercase | - 1 | @val : string - | ^ expected ':' to separate field name from its value - error: expected expression after field name + help: capture names must be snake_case; use @name instead + | + 1 - (a) @Name + 1 + (a) @name + | + "); +} + +#[test] +fn capture_name_pascal_case_with_hyphens_error() { + let input = indoc! {r#" + (a) @My-Name + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: capture names cannot contain hyphens + | + 1 | (a) @My-Name + | ^^^^^^^ capture names cannot contain hyphens + | + help: captures become struct fields; use @my_name instead + | + 1 - (a) @My-Name + 1 + (a) @my_name + | + "); +} + +#[test] +fn capture_name_with_hyphens_error() { + let input = indoc! {r#" + (a) @my-name + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: capture names cannot contain hyphens + | + 1 | (a) @my-name + | ^^^^^^^ capture names cannot contain hyphens + | + help: captures become struct fields; use @my_name instead + | + 1 - (a) @my-name + 1 + (a) @my_name + | + "); +} + +#[test] +fn capture_dotted_error() { + let input = indoc! {r#" + (identifier) @foo.bar + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: capture names cannot contain dots + | + 1 | (identifier) @foo.bar + | ^^^^^^^ capture names cannot contain dots + | + help: captures become struct fields; use @foo_bar instead + | + 1 - (identifier) @foo.bar + 1 + (identifier) @foo_bar + | + "); +} + +#[test] +fn capture_dotted_multiple_parts() { + let input = indoc! {r#" + (identifier) @foo.bar.baz + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: capture names cannot contain dots + | + 1 | (identifier) @foo.bar.baz + | ^^^^^^^^^^^ capture names cannot contain dots + | + help: captures become struct fields; use @foo_bar_baz instead + | + 1 - (identifier) @foo.bar.baz + 1 + (identifier) @foo_bar_baz + | + "); +} + +#[test] +fn capture_dotted_followed_by_field() { + let input = indoc! {r#" + (node) @foo.bar name: (other) + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: capture names cannot contain dots + | + 1 | (node) @foo.bar name: (other) + | ^^^^^^^ capture names cannot contain dots + | + help: captures become struct fields; use @foo_bar instead + | + 1 - (node) @foo.bar name: (other) + 1 + (node) @foo_bar name: (other) + | + error: unnamed definition must be last in file; add a name: `Name = (node) @foo.bar` + | + 1 | (node) @foo.bar name: (other) + | ^^^^^^^^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (node) @foo.bar` + "); +} + +#[test] +fn capture_space_after_dot_breaks_chain() { + let input = indoc! {r#" + (identifier) @foo. bar + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: capture names cannot contain dots + | + 1 | (identifier) @foo. bar + | ^^^^ capture names cannot contain dots + | + help: captures become struct fields; use @foo_ instead + | + 1 - (identifier) @foo. bar + 1 + (identifier) @foo_ bar | - 1 | @val : string - | ^ expected expression after field name error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | - 1 | @val : string - | ^^^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - error: unnamed definition must be last in file; add a name: `Name = val` + 1 | (identifier) @foo. bar + | ^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: unnamed definition must be last in file; add a name: `Name = (identifier) @foo.` | - 1 | @val : string - | ^^^ unnamed definition must be last in file; add a name: `Name = val` + 1 | (identifier) @foo. bar + | ^^^^^^^^^^^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (identifier) @foo.` "); } #[test] -fn lowercase_branch_label() { +fn capture_hyphenated_error() { let input = indoc! {r#" - [ - left: (a) - right: (b) - ] + (identifier) @foo-bar "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: tagged alternation labels must be Capitalized (they map to enum variants) + insta::assert_snapshot!(query.dump_errors(), @r" + error: capture names cannot contain hyphens | - 2 | left: (a) - | ^^^^ tagged alternation labels must be Capitalized (they map to enum variants) + 1 | (identifier) @foo-bar + | ^^^^^^^ capture names cannot contain hyphens | - help: capitalize as `Left` + help: captures become struct fields; use @foo_bar instead | - 2 - left: (a) - 2 + Left: (a) + 1 - (identifier) @foo-bar + 1 + (identifier) @foo_bar | - error: tagged alternation labels must be Capitalized (they map to enum variants) + "); +} + +#[test] +fn capture_hyphenated_multiple() { + let input = indoc! {r#" + (identifier) @foo-bar-baz + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: capture names cannot contain hyphens | - 3 | right: (b) - | ^^^^^ tagged alternation labels must be Capitalized (they map to enum variants) + 1 | (identifier) @foo-bar-baz + | ^^^^^^^^^^^ capture names cannot contain hyphens | - help: capitalize as `Right` + help: captures become struct fields; use @foo_bar_baz instead | - 3 - right: (b) - 3 + Right: (b) + 1 - (identifier) @foo-bar-baz + 1 + (identifier) @foo_bar_baz | - "#); + "); } #[test] -fn mixed_case_branch_labels() { - let input = "[foo: (a) Bar: (b)]"; +fn capture_mixed_dots_and_hyphens() { + let input = indoc! {r#" + (identifier) @foo.bar-baz + "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: tagged alternation labels must be Capitalized (they map to enum variants) + insta::assert_snapshot!(query.dump_errors(), @r" + error: capture names cannot contain dots | - 1 | [foo: (a) Bar: (b)] - | ^^^ tagged alternation labels must be Capitalized (they map to enum variants) + 1 | (identifier) @foo.bar-baz + | ^^^^^^^^^^^ capture names cannot contain dots | - help: capitalize as `Foo` + help: captures become struct fields; use @foo_bar_baz instead | - 1 - [foo: (a) Bar: (b)] - 1 + [Foo: (a) Bar: (b)] + 1 - (identifier) @foo.bar-baz + 1 + (identifier) @foo_bar_baz | - "#); + "); } #[test] -fn field_equals_typo() { - let input = "(node name = (identifier))"; +fn field_name_pascal_case_error() { + let input = indoc! {r#" + (call Name: (a)) + "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: '=' is not valid for field constraints + insta::assert_snapshot!(query.dump_errors(), @r" + error: field names must start with lowercase | - 1 | (node name = (identifier)) - | ^ '=' is not valid for field constraints + 1 | (call Name: (a)) + | ^^^^ field names must start with lowercase | - help: use ':' + help: field names must be snake_case; use name: instead | - 1 - (node name = (identifier)) - 1 + (node name : (identifier)) + 1 - (call Name: (a)) + 1 + (call name:: (a)) | - "#); + "); } #[test] -fn field_equals_typo_no_space() { - let input = "(node name=(identifier))"; +fn field_name_with_dots_error() { + let input = "(call foo.bar: (x))"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: '=' is not valid for field constraints + insta::assert_snapshot!(query.dump_errors(), @r" + error: field names cannot contain dots | - 1 | (node name=(identifier)) - | ^ '=' is not valid for field constraints + 1 | (call foo.bar: (x)) + | ^^^^^^^ field names cannot contain dots | - help: use ':' + help: field names must be snake_case; use foo_bar: instead | - 1 - (node name=(identifier)) - 1 + (node name:(identifier)) + 1 - (call foo.bar: (x)) + 1 + (call foo_bar:: (x)) | - "#); + "); } #[test] -fn field_equals_typo_no_expression() { - let input = "(call name=)"; +fn field_name_with_hyphens_error() { + let input = "(call foo-bar: (x))"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r" - error: '=' is not valid for field constraints + error: field names cannot contain hyphens | - 1 | (call name=) - | ^ '=' is not valid for field constraints + 1 | (call foo-bar: (x)) + | ^^^^^^^ field names cannot contain hyphens | - help: use ':' + help: field names must be snake_case; use foo_bar: instead | - 1 - (call name=) - 1 + (call name:) + 1 - (call foo-bar: (x)) + 1 + (call foo_bar:: (x)) | - error: expected expression after field name + "); +} + +#[test] +fn negated_field_with_upper_ident_parses() { + let input = indoc! {r#" + (call !Arguments) + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: field names must start with lowercase + | + 1 | (call !Arguments) + | ^^^^^^^^^ field names must start with lowercase + | + help: field names must be snake_case; use arguments: instead + | + 1 - (call !Arguments) + 1 + (call !arguments:) | - 1 | (call name=) - | ^ expected expression after field name "); } #[test] -fn multiple_suggestions_combined() { - let input = "(node name = 'foo', @val : Type)"; +fn branch_label_snake_case_suggests_pascal() { + let input = indoc! {r#" + [My_branch: (a) Other: (b)] + "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r" - error: '=' is not valid for field constraints + error: branch labels cannot contain separators | - 1 | (node name = 'foo', @val : Type) - | ^ '=' is not valid for field constraints + 1 | [My_branch: (a) Other: (b)] + | ^^^^^^^^^ branch labels cannot contain separators | - help: use ':' + help: branch labels must be PascalCase; use MyBranch: instead | - 1 - (node name = 'foo', @val : Type) - 1 + (node name : 'foo', @val : Type) + 1 - [My_branch: (a) Other: (b)] + 1 + [MyBranch:: (a) Other: (b)] | - error: ',' is not valid syntax; plotnik uses whitespace for separation + "); +} + +#[test] +fn branch_label_kebab_case_suggests_pascal() { + let input = indoc! {r#" + [My-branch: (a) Other: (b)] + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: branch labels cannot contain separators | - 1 | (node name = 'foo', @val : Type) - | ^ ',' is not valid syntax; plotnik uses whitespace for separation + 1 | [My-branch: (a) Other: (b)] + | ^^^^^^^^^ branch labels cannot contain separators | - help: remove separator + help: branch labels must be PascalCase; use MyBranch: instead | - 1 - (node name = 'foo', @val : Type) - 1 + (node name = 'foo' @val : Type) + 1 - [My-branch: (a) Other: (b)] + 1 + [MyBranch:: (a) Other: (b)] | - error: unexpected token; expected a child expression or closing delimiter + "); +} + +#[test] +fn branch_label_dotted_suggests_pascal() { + let input = indoc! {r#" + [My.branch: (a) Other: (b)] + "#}; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: branch labels cannot contain separators | - 1 | (node name = 'foo', @val : Type) - | ^ unexpected token; expected a child expression or closing delimiter - error: expected ':' to separate field name from its value + 1 | [My.branch: (a) Other: (b)] + | ^^^^^^^^^ branch labels cannot contain separators | - 1 | (node name = 'foo', @val : Type) - | ^ expected ':' to separate field name from its value - error: expected expression after field name + help: branch labels must be PascalCase; use MyBranch: instead | - 1 | (node name = 'foo', @val : Type) - | ^ expected expression after field name - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + 1 - [My.branch: (a) Other: (b)] + 1 + [MyBranch:: (a) Other: (b)] | - 1 | (node name = 'foo', @val : Type) - | ^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) "); } #[test] -fn double_quotes_no_error() { - let input = r#"(node "if")"#; +fn branch_label_with_underscores_error() { + let input = "[Some_Label: (x)]"; let query = Query::new(input).unwrap(); - assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_cst(), @r#" - Root - Def - Tree - ParenOpen "(" - Id "node" - Str - DoubleQuote "\"" - StrVal "if" - DoubleQuote "\"" - ParenClose ")" - "#); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: branch labels cannot contain separators + | + 1 | [Some_Label: (x)] + | ^^^^^^^^^^ branch labels cannot contain separators + | + help: branch labels must be PascalCase; use SomeLabel: instead + | + 1 - [Some_Label: (x)] + 1 + [SomeLabel:: (x)] + | + "); } #[test] -fn double_colon_no_error() { - let input = "(identifier) @name :: Type"; +fn branch_label_with_hyphens_error() { + let input = "[Some-Label: (x)]"; let query = Query::new(input).unwrap(); - assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_cst(), @r#" - Root - Def - Capture - Tree - ParenOpen "(" - Id "identifier" - ParenClose ")" - At "@" - Id "name" - Type - DoubleColon "::" - Id "Type" - "#); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: branch labels cannot contain separators + | + 1 | [Some-Label: (x)] + | ^^^^^^^^^^ branch labels cannot contain separators + | + help: branch labels must be PascalCase; use SomeLabel: instead + | + 1 - [Some-Label: (x)] + 1 + [SomeLabel:: (x)] + | + "); } #[test] -fn field_colon_no_error() { - let input = "(node name: (identifier))"; +fn lowercase_branch_label() { + let input = indoc! {r#" + [ + left: (a) + right: (b) + ] + "#}; let query = Query::new(input).unwrap(); - assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_cst(), @r#" - Root - Def - Tree - ParenOpen "(" - Id "node" - Field - Id "name" - Colon ":" - Tree - ParenOpen "(" - Id "identifier" - ParenClose ")" - ParenClose ")" - "#); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: tagged alternation labels must be Capitalized (they map to enum variants) + | + 2 | left: (a) + | ^^^^ tagged alternation labels must be Capitalized (they map to enum variants) + | + help: capitalize as `Left` + | + 2 - left: (a) + 2 + Left: (a) + | + error: tagged alternation labels must be Capitalized (they map to enum variants) + | + 3 | right: (b) + | ^^^^^ tagged alternation labels must be Capitalized (they map to enum variants) + | + help: capitalize as `Right` + | + 3 - right: (b) + 3 + Right: (b) + | + "); } #[test] -fn capitalized_branch_label_no_error() { - let input = "[Left: (a) Right: (b)]"; +fn lowercase_branch_label_suggests_capitalized() { + let input = indoc! {r#" + [first: (a) Second: (b)] + "#}; let query = Query::new(input).unwrap(); - assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_cst(), @r#" - Root - Def - Alt - BracketOpen "[" - Branch - Id "Left" - Colon ":" - Tree - ParenOpen "(" - Id "a" - ParenClose ")" - Branch - Id "Right" - Colon ":" - Tree - ParenOpen "(" - Id "b" - ParenClose ")" - BracketClose "]" - "#); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: tagged alternation labels must be Capitalized (they map to enum variants) + | + 1 | [first: (a) Second: (b)] + | ^^^^^ tagged alternation labels must be Capitalized (they map to enum variants) + | + help: capitalize as `First` + | + 1 - [first: (a) Second: (b)] + 1 + [First: (a) Second: (b)] + | + "); } #[test] -fn whitespace_separation_no_error() { - let input = "[(a) (b) (c)]"; +fn mixed_case_branch_labels() { + let input = "[foo: (a) Bar: (b)]"; let query = Query::new(input).unwrap(); - assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_cst(), @r#" - Root - Def - Alt - BracketOpen "[" - Branch - Tree - ParenOpen "(" - Id "a" - ParenClose ")" - Branch - Tree - ParenOpen "(" - Id "b" - ParenClose ")" - Branch - Tree - ParenOpen "(" - Id "c" - ParenClose ")" - BracketClose "]" - "#); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: tagged alternation labels must be Capitalized (they map to enum variants) + | + 1 | [foo: (a) Bar: (b)] + | ^^^ tagged alternation labels must be Capitalized (they map to enum variants) + | + help: capitalize as `Foo` + | + 1 - [foo: (a) Bar: (b)] + 1 + [Foo: (a) Bar: (b)] + | + "); } #[test] -fn field_with_upper_ident_parses() { +fn type_annotation_dotted_suggests_pascal() { let input = indoc! {r#" - (node FieldTypo: (x)) + (a) @x :: My.Type "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: field names must start with lowercase + insta::assert_snapshot!(query.dump_errors(), @r" + error: type names cannot contain dots or hyphens | - 1 | (node FieldTypo: (x)) - | ^^^^^^^^^ field names must start with lowercase + 1 | (a) @x :: My.Type + | ^^^^^^^ type names cannot contain dots or hyphens | - help: field names must be snake_case; use field_typo: instead + help: type names cannot contain separators; use ::MyType instead | - 1 - (node FieldTypo: (x)) - 1 + (node field_typo:: (x)) + 1 - (a) @x :: My.Type + 1 + (a) @x :: ::MyType | - "#); + "); } #[test] -fn capture_with_upper_ident_parses() { +fn type_annotation_kebab_suggests_pascal() { let input = indoc! {r#" - (identifier) @Name + (a) @x :: My-Type "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: capture names must start with lowercase + insta::assert_snapshot!(query.dump_errors(), @r" + error: type names cannot contain dots or hyphens | - 1 | (identifier) @Name - | ^^^^ capture names must start with lowercase + 1 | (a) @x :: My-Type + | ^^^^^^^ type names cannot contain dots or hyphens | - help: capture names must be snake_case; use @name instead + help: type names cannot contain separators; use ::MyType instead | - 1 - (identifier) @Name - 1 + (identifier) @name + 1 - (a) @x :: My-Type + 1 + (a) @x :: ::MyType | - "#); + "); +} + +#[test] +fn type_name_with_dots_error() { + let input = "(x) @name :: Some.Type"; + + let query = Query::new(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: type names cannot contain dots or hyphens + | + 1 | (x) @name :: Some.Type + | ^^^^^^^^^ type names cannot contain dots or hyphens + | + help: type names cannot contain separators; use ::SomeType instead + | + 1 - (x) @name :: Some.Type + 1 + (x) @name :: ::SomeType + | + "); } #[test] -fn negated_field_with_upper_ident_parses() { - let input = indoc! {r#" - (call !Arguments) - "#}; +fn type_name_with_hyphens_error() { + let input = "(x) @name :: Some-Type"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: field names must start with lowercase + insta::assert_snapshot!(query.dump_errors(), @r" + error: type names cannot contain dots or hyphens | - 1 | (call !Arguments) - | ^^^^^^^^^ field names must start with lowercase + 1 | (x) @name :: Some-Type + | ^^^^^^^^^ type names cannot contain dots or hyphens | - help: field names must be snake_case; use arguments: instead + help: type names cannot contain separators; use ::SomeType instead | - 1 - (call !Arguments) - 1 + (call !arguments:) + 1 - (x) @name :: Some-Type + 1 + (x) @name :: ::SomeType | - "#); + "); } #[test] -fn capture_with_type_and_upper_ident() { - let input = indoc! {r#" - (identifier) @Name :: MyType - "#}; +fn comma_in_node_children() { + let input = "(node (a), (b))"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: capture names must start with lowercase + insta::assert_snapshot!(query.dump_errors(), @r" + error: ',' is not valid syntax; plotnik uses whitespace for separation | - 1 | (identifier) @Name :: MyType - | ^^^^ capture names must start with lowercase + 1 | (node (a), (b)) + | ^ ',' is not valid syntax; plotnik uses whitespace for separation | - help: capture names must be snake_case; use @name instead + help: remove separator | - 1 - (identifier) @Name :: MyType - 1 + (identifier) @name :: MyType + 1 - (node (a), (b)) + 1 + (node (a) (b)) | - "#); + "); } #[test] -fn def_name_lowercase_error() { - let input = "lowercase = (x)"; +fn comma_in_alternation() { + let input = "[(a), (b), (c)]"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r" - error: definition names must start with uppercase + error: ',' is not valid syntax; plotnik uses whitespace for separation | - 1 | lowercase = (x) - | ^^^^^^^^^ definition names must start with uppercase + 1 | [(a), (b), (c)] + | ^ ',' is not valid syntax; plotnik uses whitespace for separation | - help: definition names must be PascalCase; use Lowercase instead + help: remove separator | - 1 - lowercase = (x) - 1 + Lowercase = (x) + 1 - [(a), (b), (c)] + 1 + [(a) (b), (c)] + | + error: ',' is not valid syntax; plotnik uses whitespace for separation + | + 1 | [(a), (b), (c)] + | ^ ',' is not valid syntax; plotnik uses whitespace for separation + | + help: remove separator + | + 1 - [(a), (b), (c)] + 1 + [(a), (b) (c)] | "); } #[test] -fn def_name_with_underscores_error() { - let input = "Some_Thing = (x)"; +fn comma_in_sequence() { + let input = "{(a), (b)}"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r" - error: definition names cannot contain separators + error: ',' is not valid syntax; plotnik uses whitespace for separation | - 1 | Some_Thing = (x) - | ^^^^^^^^^^ definition names cannot contain separators + 1 | {(a), (b)} + | ^ ',' is not valid syntax; plotnik uses whitespace for separation | - help: definition names must be PascalCase; use SomeThing instead + help: remove separator | - 1 - Some_Thing = (x) - 1 + SomeThing = (x) + 1 - {(a), (b)} + 1 + {(a) (b)} | "); } #[test] -fn def_name_with_hyphens_error() { - let input = "Some-Thing = (x)"; +fn pipe_in_alternation() { + let input = "[(a) | (b) | (c)]"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r" - error: definition names cannot contain separators + error: '|' is not valid syntax; plotnik uses whitespace for separation | - 1 | Some-Thing = (x) - | ^^^^^^^^^^ definition names cannot contain separators + 1 | [(a) | (b) | (c)] + | ^ '|' is not valid syntax; plotnik uses whitespace for separation | - help: definition names must be PascalCase; use SomeThing instead + help: remove separator | - 1 - Some-Thing = (x) - 1 + SomeThing = (x) + 1 - [(a) | (b) | (c)] + 1 + [(a) (b) | (c)] + | + error: '|' is not valid syntax; plotnik uses whitespace for separation + | + 1 | [(a) | (b) | (c)] + | ^ '|' is not valid syntax; plotnik uses whitespace for separation + | + help: remove separator + | + 1 - [(a) | (b) | (c)] + 1 + [(a) | (b) (c)] | "); } #[test] -fn branch_label_with_underscores_error() { - let input = "[Some_Label: (x)]"; +fn pipe_between_branches() { + let input = indoc! {r#" + [(a) | (b)] + "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r" - error: branch labels cannot contain separators + error: '|' is not valid syntax; plotnik uses whitespace for separation | - 1 | [Some_Label: (x)] - | ^^^^^^^^^^ branch labels cannot contain separators + 1 | [(a) | (b)] + | ^ '|' is not valid syntax; plotnik uses whitespace for separation | - help: branch labels must be PascalCase; use SomeLabel: instead + help: remove separator | - 1 - [Some_Label: (x)] - 1 + [SomeLabel:: (x)] + 1 - [(a) | (b)] + 1 + [(a) (b)] | "); } #[test] -fn branch_label_with_hyphens_error() { - let input = "[Some-Label: (x)]"; +fn pipe_in_tree() { + let input = "(a | b)"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r" - error: branch labels cannot contain separators + error: '|' is not valid syntax; plotnik uses whitespace for separation | - 1 | [Some-Label: (x)] - | ^^^^^^^^^^ branch labels cannot contain separators + 1 | (a | b) + | ^ '|' is not valid syntax; plotnik uses whitespace for separation | - help: branch labels must be PascalCase; use SomeLabel: instead + help: remove separator | - 1 - [Some-Label: (x)] - 1 + [SomeLabel:: (x)] + 1 - (a | b) + 1 + (a b) + | + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | + 1 | (a | b) + | ^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) "); } #[test] -fn lowercase_branch_label_missing_expression() { - let input = "[label:]"; +fn pipe_in_sequence() { + let input = "{(a) | (b)}"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r" - error: tagged alternation labels must be Capitalized (they map to enum variants) - | - 1 | [label:] - | ^^^^^ tagged alternation labels must be Capitalized (they map to enum variants) + error: '|' is not valid syntax; plotnik uses whitespace for separation | - help: capitalize as `Label` + 1 | {(a) | (b)} + | ^ '|' is not valid syntax; plotnik uses whitespace for separation | - 1 - [label:] - 1 + [Label:] + help: remove separator | - error: expected expression after branch label + 1 - {(a) | (b)} + 1 + {(a) (b)} | - 1 | [label:] - | ^ expected expression after branch label "); } #[test] -fn field_name_with_dots_error() { - let input = "(call foo.bar: (x))"; +fn field_equals_typo() { + let input = "(node name = (identifier))"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r" - error: field names cannot contain dots + error: '=' is not valid for field constraints | - 1 | (call foo.bar: (x)) - | ^^^^^^^ field names cannot contain dots + 1 | (node name = (identifier)) + | ^ '=' is not valid for field constraints | - help: field names must be snake_case; use foo_bar: instead + help: use ':' | - 1 - (call foo.bar: (x)) - 1 + (call foo_bar:: (x)) + 1 - (node name = (identifier)) + 1 + (node name : (identifier)) | "); } #[test] -fn field_name_with_hyphens_error() { - let input = "(call foo-bar: (x))"; +fn field_equals_typo_no_space() { + let input = "(node name=(identifier))"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r" - error: field names cannot contain hyphens + error: '=' is not valid for field constraints | - 1 | (call foo-bar: (x)) - | ^^^^^^^ field names cannot contain hyphens + 1 | (node name=(identifier)) + | ^ '=' is not valid for field constraints | - help: field names must be snake_case; use foo_bar: instead + help: use ':' | - 1 - (call foo-bar: (x)) - 1 + (call foo_bar:: (x)) + 1 - (node name=(identifier)) + 1 + (node name:(identifier)) | "); } #[test] -fn type_name_with_dots_error() { - let input = "(x) @name :: Some.Type"; +fn field_equals_typo_no_expression() { + let input = "(call name=)"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r" - error: type names cannot contain dots or hyphens + error: '=' is not valid for field constraints | - 1 | (x) @name :: Some.Type - | ^^^^^^^^^ type names cannot contain dots or hyphens + 1 | (call name=) + | ^ '=' is not valid for field constraints | - help: type names cannot contain separators; use ::SomeType instead + help: use ':' | - 1 - (x) @name :: Some.Type - 1 + (x) @name :: ::SomeType + 1 - (call name=) + 1 + (call name:) + | + error: expected expression after field name | + 1 | (call name=) + | ^ expected expression after field name "); } #[test] -fn type_name_with_hyphens_error() { - let input = "(x) @name :: Some-Type"; +fn field_equals_typo_in_tree() { + let input = indoc! {r#" + (call name = (identifier)) + "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r" - error: type names cannot contain dots or hyphens + error: '=' is not valid for field constraints | - 1 | (x) @name :: Some-Type - | ^^^^^^^^^ type names cannot contain dots or hyphens + 1 | (call name = (identifier)) + | ^ '=' is not valid for field constraints | - help: type names cannot contain separators; use ::SomeType instead + help: use ':' | - 1 - (x) @name :: Some-Type - 1 + (x) @name :: ::SomeType + 1 - (call name = (identifier)) + 1 + (call name : (identifier)) | "); } #[test] -fn unclosed_double_quote_string() { - let input = r#"(call "foo)"#; +fn single_colon_type_annotation() { + let input = "(identifier) @name : Type"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_errors(), @r#" - error: unexpected token; expected a child expression or closing delimiter - | - 1 | (call "foo) - | ^^^^^ unexpected token; expected a child expression or closing delimiter - error: unclosed tree; expected ')' - | - 1 | (call "foo) - | - ^ unclosed tree; expected ')' - | | - | tree started here - "#); + insta::assert_snapshot!(query.dump_errors(), @r" + error: single colon is not valid for type annotations + | + 1 | (identifier) @name : Type + | ^ single colon is not valid for type annotations + | + help: use '::' + | + 1 | (identifier) @name :: Type + | + + "); } #[test] -fn unclosed_single_quote_string() { - let input = "(call 'foo)"; +fn single_colon_type_annotation_no_space() { + let input = "(identifier) @name:Type"; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r" - error: unexpected token; expected a child expression or closing delimiter + error: single colon is not valid for type annotations + | + 1 | (identifier) @name:Type + | ^ single colon is not valid for type annotations | - 1 | (call 'foo) - | ^^^^^ unexpected token; expected a child expression or closing delimiter - error: unclosed tree; expected ')' + help: use '::' | - 1 | (call 'foo) - | - ^ unclosed tree; expected ')' - | | - | tree started here + 1 | (identifier) @name::Type + | + "); } #[test] -fn reference_with_supertype_syntax_error() { - let input = "(RefName/subtype)"; +fn single_colon_type_annotation_with_space() { + let input = indoc! {r#" + (a) @x : Type + "#}; let query = Query::new(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_errors(), @r" - error: references cannot use supertype syntax (/) + error: single colon is not valid for type annotations | - 1 | (RefName/subtype) - | ^ references cannot use supertype syntax (/) + 1 | (a) @x : Type + | ^ single colon is not valid for type annotations + | + help: use '::' + | + 1 | (a) @x :: Type + | + "); } #[test] -fn missing_with_nested_tree_parses() { - let input = "(MISSING (something))"; +fn single_colon_primitive_type() { + let input = "@val : string"; let query = Query::new(input).unwrap(); - assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_cst(), @r#" - Root - Def - Tree - ParenOpen "(" - KwMissing "MISSING" - Tree - ParenOpen "(" - Id "something" - ParenClose ")" - ParenClose ")" - "#); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_errors(), @r" + error: capture '@' must follow an expression to capture + | + 1 | @val : string + | ^ capture '@' must follow an expression to capture + error: expected ':' to separate field name from its value + | + 1 | @val : string + | ^ expected ':' to separate field name from its value + error: expected expression after field name + | + 1 | @val : string + | ^ expected expression after field name + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | + 1 | @val : string + | ^^^^^^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: unnamed definition must be last in file; add a name: `Name = val` + | + 1 | @val : string + | ^^^ unnamed definition must be last in file; add a name: `Name = val` + "); }