From f6875ea1f5f1d8f75ec93a55f7b17f7aabaf5e1b Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Tue, 6 Jan 2026 09:33:31 -0300 Subject: [PATCH] fix: ban empty anonymous nodes with diagnostic --- crates/plotnik-lib/src/diagnostics/message.rs | 3 ++ .../plotnik-lib/src/parser/grammar/atoms.rs | 34 ++++++++++++++++++- .../parser/tests/recovery/coverage_tests.rs | 6 ++-- .../parser/tests/recovery/validation_tests.rs | 32 +++++++++++++++++ 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/crates/plotnik-lib/src/diagnostics/message.rs b/crates/plotnik-lib/src/diagnostics/message.rs index d36a9fd9..9e2b187c 100644 --- a/crates/plotnik-lib/src/diagnostics/message.rs +++ b/crates/plotnik-lib/src/diagnostics/message.rs @@ -30,6 +30,7 @@ pub enum DiagnosticKind { // User wrote something that doesn't belong EmptyTree, + EmptyAnonymousNode, BareIdentifier, InvalidSeparator, AnchorInAlternation, @@ -144,6 +145,7 @@ impl DiagnosticKind { Self::ExpectedTypeName => Some("e.g., `::MyType` or `::string`"), Self::ExpectedFieldName => Some("e.g., `-value`"), Self::EmptyTree => Some("use `(_)` to match any named node, or `_` for any node"), + Self::EmptyAnonymousNode => Some("use a valid anonymous node or remove it"), Self::TreeSitterSequenceSyntax => Some("use `{...}` for sequences"), Self::NegationSyntaxDeprecated => Some("use `-field` instead of `!field`"), Self::MixedAltBranches => { @@ -182,6 +184,7 @@ impl DiagnosticKind { // Invalid syntax Self::EmptyTree => "empty `()` is not allowed", + Self::EmptyAnonymousNode => "empty anonymous node", Self::BareIdentifier => "bare identifier is not valid", Self::InvalidSeparator => "unexpected separator", Self::AnchorInAlternation => "anchors cannot appear directly in alternations", diff --git a/crates/plotnik-lib/src/parser/grammar/atoms.rs b/crates/plotnik-lib/src/parser/grammar/atoms.rs index e0fa046c..daaf23bd 100644 --- a/crates/plotnik-lib/src/parser/grammar/atoms.rs +++ b/crates/plotnik-lib/src/parser/grammar/atoms.rs @@ -1,3 +1,6 @@ +use rowan::TextRange; + +use crate::diagnostics::DiagnosticKind; use crate::parser::Parser; use crate::parser::cst::SyntaxKind; @@ -10,9 +13,38 @@ impl Parser<'_, '_> { /// `"if"` | `'+'` pub(crate) fn parse_str(&mut self) { + let start = self.current_span().start(); self.start_node(SyntaxKind::Str); - self.bump_string_tokens(); + + let open_quote = self.current(); + self.bump(); // opening quote + + let has_content = self.currently_is(SyntaxKind::StrVal); + if has_content { + self.bump(); + } + + let closing = self.current(); + assert_eq!( + closing, open_quote, + "parse_str: expected closing {:?} but found {:?} \ + (lexer should only produce quote tokens from complete strings)", + open_quote, closing + ); + let end = self.current_span().end(); + self.bump(); // closing quote + self.finish_node(); + + if !has_content { + self.diagnostics + .report( + self.source_id, + DiagnosticKind::EmptyAnonymousNode, + TextRange::new(start, end), + ) + .emit(); + } } /// Consume string tokens (quote + optional content + quote) without creating a node. diff --git a/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs index 60a29153..3b3f4bb8 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs @@ -200,7 +200,8 @@ fn empty_double_quote_string() { Q = (a "") "#}; - let res = Query::expect_valid_cst(input); + // Empty anonymous nodes are now invalid, but CST structure is still correct + let res = Query::expect(input).dump_cst(); insta::assert_snapshot!(res, @r#" Root @@ -223,7 +224,8 @@ fn empty_single_quote_string() { Q = (a '') "#}; - let res = Query::expect_valid_cst(input); + // Empty anonymous nodes are now invalid, but CST structure is still correct + let res = Query::expect(input).dump_cst(); insta::assert_snapshot!(res, @r#" Root 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 6b4a161d..07d63838 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs @@ -1407,3 +1407,35 @@ fn negation_syntax_deprecated_warning() { help: use `-field` instead of `!field` "); } + +#[test] +fn empty_anonymous_node_double_quotes() { + let input = r#"(node "")"#; + + let res = Query::expect_invalid(input); + + insta::assert_snapshot!(res, @r#" + error: empty anonymous node + | + 1 | (node "") + | ^^ + | + help: use a valid anonymous node or remove it + "#); +} + +#[test] +fn empty_anonymous_node_single_quotes() { + let input = "(node '')"; + + let res = Query::expect_invalid(input); + + insta::assert_snapshot!(res, @r" + error: empty anonymous node + | + 1 | (node '') + | ^^ + | + help: use a valid anonymous node or remove it + "); +}