diff --git a/crates/plotnik-lib/src/diagnostics/message.rs b/crates/plotnik-lib/src/diagnostics/message.rs index 8c0a2956..29eafc9c 100644 --- a/crates/plotnik-lib/src/diagnostics/message.rs +++ b/crates/plotnik-lib/src/diagnostics/message.rs @@ -54,6 +54,7 @@ pub enum DiagnosticKind { FieldNameHasHyphens, FieldNameUppercase, TypeNameInvalidChars, + TreeSitterSequenceSyntax, // Valid syntax, invalid semantics DuplicateDefinition, @@ -87,7 +88,7 @@ impl DiagnosticKind { /// Default severity for this kind. Can be overridden by policy. pub fn default_severity(&self) -> Severity { match self { - Self::UnusedBranchLabels => Severity::Warning, + Self::UnusedBranchLabels | Self::TreeSitterSequenceSyntax => Severity::Warning, _ => Severity::Error, } } @@ -171,6 +172,7 @@ impl DiagnosticKind { Self::FieldNameHasHyphens => "field names cannot contain `-`", Self::FieldNameUppercase => "field names must be lowercase", Self::TypeNameInvalidChars => "type names cannot contain `.` or `-`", + Self::TreeSitterSequenceSyntax => "Tree-sitter sequence syntax", // Semantic errors Self::DuplicateDefinition => "name already defined", diff --git a/crates/plotnik-lib/src/parser/grammar/structures.rs b/crates/plotnik-lib/src/parser/grammar/structures.rs index de097bfe..4f892006 100644 --- a/crates/plotnik-lib/src/parser/grammar/structures.rs +++ b/crates/plotnik-lib/src/parser/grammar/structures.rs @@ -15,6 +15,7 @@ impl Parser<'_, '_> { pub(crate) fn parse_tree(&mut self) { let checkpoint = self.checkpoint(); self.push_delimiter(SyntaxKind::ParenOpen); + let open_paren_span = self.current_span(); // save span before bump self.bump(); // consume '(' let mut is_ref = false; @@ -45,6 +46,18 @@ impl Parser<'_, '_> { } _ => { 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) + if self.currently_is_one_of(EXPR_FIRST_TOKENS) { + self.diagnostics + .report( + self.source_id, + DiagnosticKind::TreeSitterSequenceSyntax, + open_paren_span, + ) + .hint("use `{...}` for sequences") + .emit(); + } } } diff --git a/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs index f8c8e151..e3cd8ca8 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs @@ -1309,3 +1309,42 @@ fn single_colon_primitive_type() { | ^^^^^^ "); } + +#[test] +fn treesitter_sequence_syntax_warning() { + // Tree-sitter uses ((a) (b)) for sequences, Plotnik uses {(a) (b)} + let input = "Test = ((a) (b))"; + + let res = Query::expect_warning(input); + + insta::assert_snapshot!(res, @r" + warning: Tree-sitter sequence syntax + | + 1 | Test = ((a) (b)) + | ^ + | + help: use `{...}` for sequences + "); +} + +#[test] +fn treesitter_sequence_single_child_warning() { + let input = "Test = ((a))"; + + let res = Query::expect_warning(input); + + insta::assert_snapshot!(res, @r" + warning: Tree-sitter sequence syntax + | + 1 | Test = ((a)) + | ^ + | + help: use `{...}` for sequences + "); +} + +#[test] +fn named_node_with_children_no_warning() { + // Normal node with children - NOT a tree-sitter sequence + Query::expect_valid("Test = (identifier (child))"); +} diff --git a/crates/plotnik-lib/src/query/query_tests.rs b/crates/plotnik-lib/src/query/query_tests.rs index 4d3f3458..2d0e1ed1 100644 --- a/crates/plotnik-lib/src/query/query_tests.rs +++ b/crates/plotnik-lib/src/query/query_tests.rs @@ -97,6 +97,25 @@ impl QueryAnalyzed { } query.dump_diagnostics() } + + #[track_caller] + pub fn expect_warning(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 with warning, got error:\n{}", + query.dump_diagnostics() + ); + } + + if !query.diagnostics().has_warnings() { + panic!("Expected warning, got none:\n{}", query.dump_cst()); + } + + query.dump_diagnostics() + } } #[test]