From 7f776495a220bc33374a51b08786fdf915ee6c08 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 3 Jan 2026 09:30:00 -0300 Subject: [PATCH] refactor: change negated field syntax from !field to -field --- crates/plotnik-lib/src/analyze/link_tests.rs | 14 +++++------ crates/plotnik-lib/src/diagnostics/message.rs | 9 +++++-- crates/plotnik-lib/src/emit/codegen_tests.rs | 2 +- ...__emit__codegen_tests__fields_negated.snap | 2 +- crates/plotnik-lib/src/parser/ast_tests.rs | 4 +-- crates/plotnik-lib/src/parser/cst.rs | 4 +++ .../src/parser/grammar/expressions.rs | 2 +- .../plotnik-lib/src/parser/grammar/fields.rs | 20 +++++++++++++-- .../src/parser/tests/grammar/fields_tests.rs | 8 +++--- .../parser/tests/recovery/incomplete_tests.rs | 6 ++--- .../parser/tests/recovery/validation_tests.rs | 25 ++++++++++++++++--- crates/plotnik-lib/src/query/printer.rs | 2 +- crates/plotnik-lib/src/query/printer_tests.rs | 8 +++--- 13 files changed, 74 insertions(+), 32 deletions(-) diff --git a/crates/plotnik-lib/src/analyze/link_tests.rs b/crates/plotnik-lib/src/analyze/link_tests.rs index 5e555aa4..e6c8a70b 100644 --- a/crates/plotnik-lib/src/analyze/link_tests.rs +++ b/crates/plotnik-lib/src/analyze/link_tests.rs @@ -127,7 +127,7 @@ fn field_not_on_node_type_with_suggestion() { #[test] fn negated_field_unknown() { let input = indoc! {r#" - Q = (function_declaration !nme) @fn + Q = (function_declaration -nme) @fn "#}; let res = Query::expect_invalid_linking(input); @@ -135,7 +135,7 @@ fn negated_field_unknown() { insta::assert_snapshot!(res, @r" error: `nme` is not a valid field | - 1 | Q = (function_declaration !nme) @fn + 1 | Q = (function_declaration -nme) @fn | ^^^ | help: did you mean `name`? @@ -145,7 +145,7 @@ fn negated_field_unknown() { #[test] fn negated_field_not_on_node_type() { let input = indoc! {r#" - Q = (function_declaration !condition) @fn + Q = (function_declaration -condition) @fn "#}; let res = Query::expect_invalid_linking(input); @@ -153,7 +153,7 @@ fn negated_field_not_on_node_type() { insta::assert_snapshot!(res, @r" error: field `condition` is not valid on this node type | - 1 | Q = (function_declaration !condition) @fn + 1 | Q = (function_declaration -condition) @fn | -------------------- ^^^^^^^^^ | | | on `function_declaration` @@ -165,7 +165,7 @@ fn negated_field_not_on_node_type() { #[test] fn negated_field_not_on_node_type_with_suggestion() { let input = indoc! {r#" - Q = (function_declaration !parameter) @fn + Q = (function_declaration -parameter) @fn "#}; let res = Query::expect_invalid_linking(input); @@ -173,7 +173,7 @@ fn negated_field_not_on_node_type_with_suggestion() { insta::assert_snapshot!(res, @r" error: field `parameter` is not valid on this node type | - 1 | Q = (function_declaration !parameter) @fn + 1 | Q = (function_declaration -parameter) @fn | -------------------- ^^^^^^^^^ | | | on `function_declaration` @@ -186,7 +186,7 @@ fn negated_field_not_on_node_type_with_suggestion() { #[test] fn negated_field_valid() { let input = indoc! {r#" - Q = (function_declaration !name) @fn + Q = (function_declaration -name) @fn "#}; Query::expect_valid_linking(input); diff --git a/crates/plotnik-lib/src/diagnostics/message.rs b/crates/plotnik-lib/src/diagnostics/message.rs index 5f3a93fa..b40ccb99 100644 --- a/crates/plotnik-lib/src/diagnostics/message.rs +++ b/crates/plotnik-lib/src/diagnostics/message.rs @@ -56,6 +56,7 @@ pub enum DiagnosticKind { FieldNameUppercase, TypeNameInvalidChars, TreeSitterSequenceSyntax, + NegationSyntaxDeprecated, // Valid syntax, invalid semantics DuplicateDefinition, @@ -90,7 +91,9 @@ impl DiagnosticKind { /// Default severity for this kind. Can be overridden by policy. pub fn default_severity(&self) -> Severity { match self { - Self::UnusedBranchLabels | Self::TreeSitterSequenceSyntax => Severity::Warning, + Self::UnusedBranchLabels + | Self::TreeSitterSequenceSyntax + | Self::NegationSyntaxDeprecated => Severity::Warning, _ => Severity::Error, } } @@ -137,9 +140,10 @@ impl DiagnosticKind { match self { Self::ExpectedSubtype => Some("e.g., `expression/binary_expression`"), Self::ExpectedTypeName => Some("e.g., `::MyType` or `::string`"), - Self::ExpectedFieldName => Some("e.g., `!value`"), + Self::ExpectedFieldName => Some("e.g., `-value`"), Self::EmptyTree => Some("use `(_)` to match any named node, or `_` for any node"), Self::TreeSitterSequenceSyntax => Some("use `{...}` for sequences"), + Self::NegationSyntaxDeprecated => Some("use `-field` instead of `!field`"), Self::MixedAltBranches => { Some("use all labels for a tagged union, or none for a merged struct") } @@ -202,6 +206,7 @@ impl DiagnosticKind { Self::FieldNameUppercase => "field names must be lowercase", Self::TypeNameInvalidChars => "type names cannot contain `.` or `-`", Self::TreeSitterSequenceSyntax => "tree-sitter sequence syntax", + Self::NegationSyntaxDeprecated => "deprecated negation syntax", // Semantic errors Self::DuplicateDefinition => "duplicate definition", diff --git a/crates/plotnik-lib/src/emit/codegen_tests.rs b/crates/plotnik-lib/src/emit/codegen_tests.rs index 00834de6..05a883e5 100644 --- a/crates/plotnik-lib/src/emit/codegen_tests.rs +++ b/crates/plotnik-lib/src/emit/codegen_tests.rs @@ -154,7 +154,7 @@ fn fields_multiple() { #[test] fn fields_negated() { snap!(indoc! {r#" - Test = (function_declaration name: (identifier) @name !type_parameters) + Test = (function_declaration name: (identifier) @name -type_parameters) "#}); } diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_negated.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_negated.snap index 5442c91f..82261381 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_negated.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_negated.snap @@ -1,7 +1,7 @@ --- source: crates/plotnik-lib/src/emit/codegen_tests.rs --- -Test = (function_declaration name: (identifier) @name !type_parameters) +Test = (function_declaration name: (identifier) @name -type_parameters) --- [flags] linked = false diff --git a/crates/plotnik-lib/src/parser/ast_tests.rs b/crates/plotnik-lib/src/parser/ast_tests.rs index 1b76c8ce..651012ad 100644 --- a/crates/plotnik-lib/src/parser/ast_tests.rs +++ b/crates/plotnik-lib/src/parser/ast_tests.rs @@ -203,12 +203,12 @@ fn anchor() { #[test] fn negated_field() { - let res = Query::expect_valid_ast("Q = (function !async)"); + let res = Query::expect_valid_ast("Q = (function -async)"); insta::assert_snapshot!(res, @r" Root Def Q NamedNode function - NegatedField !async + NegatedField -async "); } diff --git a/crates/plotnik-lib/src/parser/cst.rs b/crates/plotnik-lib/src/parser/cst.rs index e6cb307b..36535cab 100644 --- a/crates/plotnik-lib/src/parser/cst.rs +++ b/crates/plotnik-lib/src/parser/cst.rs @@ -45,6 +45,9 @@ pub enum SyntaxKind { #[token("!")] Negation, + #[token("-")] + Minus, + #[token("~")] Tilde, @@ -274,6 +277,7 @@ pub mod token_sets { SingleQuote, Dot, Negation, + Minus, KwError, KwMissing, ]); diff --git a/crates/plotnik-lib/src/parser/grammar/expressions.rs b/crates/plotnik-lib/src/parser/grammar/expressions.rs index 850eb18c..f5168eb7 100644 --- a/crates/plotnik-lib/src/parser/grammar/expressions.rs +++ b/crates/plotnik-lib/src/parser/grammar/expressions.rs @@ -59,7 +59,7 @@ impl Parser<'_, '_> { SyntaxKind::Underscore => self.parse_wildcard(), SyntaxKind::SingleQuote | SyntaxKind::DoubleQuote => self.parse_str(), SyntaxKind::Dot => self.parse_anchor(), - SyntaxKind::Negation => self.parse_negated_field(), + SyntaxKind::Negation | SyntaxKind::Minus => self.parse_negated_field(), SyntaxKind::Id => self.parse_tree_or_field(), SyntaxKind::KwError | SyntaxKind::KwMissing => { self.error_and_bump(DiagnosticKind::ErrorMissingOutsideParens); diff --git a/crates/plotnik-lib/src/parser/grammar/fields.rs b/crates/plotnik-lib/src/parser/grammar/fields.rs index 680050a6..f87c71ce 100644 --- a/crates/plotnik-lib/src/parser/grammar/fields.rs +++ b/crates/plotnik-lib/src/parser/grammar/fields.rs @@ -75,10 +75,26 @@ impl Parser<'_, '_> { self.finish_node(); } - /// Negated field assertion: `!field` (field must be absent) + /// Negated field assertion: `-field` (field must be absent) + /// + /// Also accepts deprecated `!field` syntax with a warning. pub(crate) fn parse_negated_field(&mut self) { self.start_node(SyntaxKind::NegatedField); - self.expect(SyntaxKind::Negation, "'!' for negated field"); + + // Accept both `-` (preferred) and `!` (deprecated) + if self.currently_is(SyntaxKind::Negation) { + let span = self.current_span(); + self.diagnostics + .report( + self.source_id, + DiagnosticKind::NegationSyntaxDeprecated, + span, + ) + .emit(); + self.bump(); + } else { + self.expect(SyntaxKind::Minus, "'-' for negated field"); + } if !self.currently_is(SyntaxKind::Id) { self.error(DiagnosticKind::ExpectedFieldName); diff --git a/crates/plotnik-lib/src/parser/tests/grammar/fields_tests.rs b/crates/plotnik-lib/src/parser/tests/grammar/fields_tests.rs index 2c72200d..e6a6901a 100644 --- a/crates/plotnik-lib/src/parser/tests/grammar/fields_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/grammar/fields_tests.rs @@ -67,7 +67,7 @@ fn multiple_fields() { #[test] fn negated_field() { let input = indoc! {r#" - Q = (function !async) + Q = (function -async) "#}; let res = Query::expect_valid_cst(input); @@ -81,7 +81,7 @@ fn negated_field() { ParenOpen "(" Id "function" NegatedField - Negation "!" + Minus "-" Id "async" ParenClose ")" "#); @@ -91,7 +91,7 @@ fn negated_field() { fn negated_and_regular_fields() { let input = indoc! {r#" Q = (function - !async + -async name: (identifier)) "#}; @@ -106,7 +106,7 @@ fn negated_and_regular_fields() { ParenOpen "(" Id "function" NegatedField - Negation "!" + Minus "-" Id "async" Field Id "name" diff --git a/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs index d8ad83d3..1977191d 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs @@ -68,7 +68,7 @@ fn missing_type_name() { #[test] fn missing_negated_field_name() { let input = indoc! {r#" - (call !) + (call -) "#}; let res = Query::expect_invalid(input); @@ -76,10 +76,10 @@ fn missing_negated_field_name() { insta::assert_snapshot!(res, @r" error: expected field name | - 1 | (call !) + 1 | (call -) | ^ | - help: e.g., `!value` + help: e.g., `-value` "); } 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 135eb469..04c709eb 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs @@ -730,7 +730,7 @@ fn field_name_with_hyphens_error() { #[test] fn negated_field_with_upper_ident_parses() { let input = indoc! {r#" - (call !Arguments) + (call -Arguments) "#}; let res = Query::expect_invalid(input); @@ -738,13 +738,13 @@ fn negated_field_with_upper_ident_parses() { insta::assert_snapshot!(res, @r" error: field names must be lowercase: field names become struct fields | - 1 | (call !Arguments) + 1 | (call -Arguments) | ^^^^^^^^^ | help: use `arguments:` | - 1 - (call !Arguments) - 1 + (call !arguments:) + 1 - (call -Arguments) + 1 + (call -arguments:) | "); } @@ -1396,3 +1396,20 @@ fn named_node_with_children_no_warning() { // Normal node with children - NOT a tree-sitter sequence Query::expect_valid("Test = (identifier (child))"); } + +#[test] +fn negation_syntax_deprecated_warning() { + // Old syntax `!field` is deprecated in favor of `-field` + let input = "Test = (call !name)"; + + let res = Query::expect_warning(input); + + insta::assert_snapshot!(res, @r" + warning: deprecated negation syntax + | + 1 | Test = (call !name) + | ^ + | + help: use `-field` instead of `!field` + "); +} diff --git a/crates/plotnik-lib/src/query/printer.rs b/crates/plotnik-lib/src/query/printer.rs index 05fd0f69..339cd752 100644 --- a/crates/plotnik-lib/src/query/printer.rs +++ b/crates/plotnik-lib/src/query/printer.rs @@ -359,7 +359,7 @@ impl<'q> QueryPrinter<'q> { let prefix = indent(depth); let span = self.span_str(nf.text_range()); let name = nf.name().map(|t| t.text().to_string()).unwrap_or_default(); - writeln!(w, "{}NegatedField{} !{}", prefix, span, name) + writeln!(w, "{}NegatedField{} -{}", prefix, span, name) } fn format_branch( diff --git a/crates/plotnik-lib/src/query/printer_tests.rs b/crates/plotnik-lib/src/query/printer_tests.rs index 88d23e57..6423117b 100644 --- a/crates/plotnik-lib/src/query/printer_tests.rs +++ b/crates/plotnik-lib/src/query/printer_tests.rs @@ -131,7 +131,7 @@ fn printer_field() { #[test] fn printer_negated_field() { - let input = "Q = (call !name)"; + let input = "Q = (call -name)"; let q = Query::expect(input); let res = q.printer().dump(); @@ -140,7 +140,7 @@ fn printer_negated_field() { Root Def Q NamedNode call - NegatedField !name + NegatedField -name "); } @@ -291,7 +291,7 @@ fn printer_symbols_broken_ref() { #[test] fn printer_spans_comprehensive() { let input = indoc! {r#" - Foo = (call name: (id) !bar) + Foo = (call name: (id) -bar) Q = [(a) (b)] "#}; let q = Query::expect(input); @@ -304,7 +304,7 @@ fn printer_spans_comprehensive() { NamedNode [6..28] call FieldExpr [12..22] name: NamedNode [18..22] id - NegatedField [23..27] !bar + NegatedField [23..27] -bar Def [29..42] Q Alt [33..42] Branch [34..37]