diff --git a/crates/plotnik-lib/src/parser/grammar/structures.rs b/crates/plotnik-lib/src/parser/grammar/structures.rs index 6252ef06..040281d5 100644 --- a/crates/plotnik-lib/src/parser/grammar/structures.rs +++ b/crates/plotnik-lib/src/parser/grammar/structures.rs @@ -47,10 +47,10 @@ impl Parser<'_, '_> { return; } _ => { - self.start_node_at(checkpoint, SyntaxKind::Tree); - // Warn about tree-sitter style sequence: ((a) (b)) instead of {(a) (b)} - // Only warn when it looks like an expression (not predicates or other invalid tokens) + // Tree-sitter style sequence: ((a) (b)) instead of {(a) (b)} + // Parse as Seq so it works correctly, but warn to encourage {} syntax if self.currently_is_one_of(EXPR_FIRST_TOKENS) { + self.start_node_at(checkpoint, SyntaxKind::Seq); self.diagnostics .report( self.source_id, @@ -58,6 +58,8 @@ impl Parser<'_, '_> { open_paren_span, ) .emit(); + } else { + self.start_node_at(checkpoint, SyntaxKind::Tree); } } } diff --git a/crates/plotnik-lib/src/parser/tests/grammar/sequences_tests.rs b/crates/plotnik-lib/src/parser/tests/grammar/sequences_tests.rs index 060892f4..28f00ad3 100644 --- a/crates/plotnik-lib/src/parser/tests/grammar/sequences_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/grammar/sequences_tests.rs @@ -1,6 +1,79 @@ use crate::Query; use indoc::indoc; +// Tree-sitter compatibility: ((a) (b)) parses as Seq, not as wildcard Tree with children + +#[test] +fn treesitter_sequence_parses_as_seq() { + // Tree-sitter style ((a) (b)) should produce Seq, same structure as {(a) (b)} + let input = "Q = ((a) (b))"; + + let res = Query::expect_cst_with_warnings(input); + + insta::assert_snapshot!(res, @r#" + Root + Def + Id "Q" + Equals "=" + Seq + ParenOpen "(" + Tree + ParenOpen "(" + Id "a" + ParenClose ")" + Tree + ParenOpen "(" + Id "b" + ParenClose ")" + ParenClose ")" + "#); +} + +#[test] +fn treesitter_single_item_sequence_parses_as_seq() { + // Regression test: ((x)) should be Seq containing x, not wildcard Tree with child x + let input = "Q = ((expression_statement))"; + + let res = Query::expect_cst_with_warnings(input); + + insta::assert_snapshot!(res, @r#" + Root + Def + Id "Q" + Equals "=" + Seq + ParenOpen "(" + Tree + ParenOpen "(" + Id "expression_statement" + ParenClose ")" + ParenClose ")" + "#); +} + +#[test] +fn named_node_with_child_remains_tree() { + // (foo (bar)) is a named node with child, NOT a sequence + let input = "Q = (foo (bar))"; + + let res = Query::expect_valid_cst(input); + + insta::assert_snapshot!(res, @r#" + Root + Def + Id "Q" + Equals "=" + Tree + ParenOpen "(" + Id "foo" + Tree + ParenOpen "(" + Id "bar" + ParenClose ")" + ParenClose ")" + "#); +} + #[test] fn simple_sequence() { let input = indoc! {r#" diff --git a/crates/plotnik-lib/src/query/query_tests.rs b/crates/plotnik-lib/src/query/query_tests.rs index afd0eefb..b07d4e8a 100644 --- a/crates/plotnik-lib/src/query/query_tests.rs +++ b/crates/plotnik-lib/src/query/query_tests.rs @@ -177,6 +177,21 @@ impl QueryAnalyzed { query.dump_diagnostics() } + + #[track_caller] + pub fn expect_cst_with_warnings(src: &str) -> String { + let source_map = SourceMap::one_liner(src); + let query = QueryBuilder::new(source_map).parse().unwrap().analyze(); + + if !query.is_valid() { + panic!( + "Expected valid query (warnings ok), got error:\n{}", + query.dump_diagnostics() + ); + } + + query.dump_cst() + } } #[test]