From d272cf5e86f0d292c34433b4ca616f61c80db9ad Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 6 Dec 2025 08:57:47 -0300 Subject: [PATCH 01/15] refactor: Diagnostics API --- crates/plotnik-lib/src/diagnostics/message.rs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/crates/plotnik-lib/src/diagnostics/message.rs b/crates/plotnik-lib/src/diagnostics/message.rs index b30d1646..2b8731f1 100644 --- a/crates/plotnik-lib/src/diagnostics/message.rs +++ b/crates/plotnik-lib/src/diagnostics/message.rs @@ -1,5 +1,89 @@ use rowan::TextRange; +/// Diagnostic kinds ordered by priority (highest priority first). +/// +/// When two diagnostics have overlapping spans, the higher-priority one +/// suppresses the lower-priority one. This prevents cascading error noise. +/// +/// Priority rationale: +/// - Unclosed delimiters cause massive cascading errors downstream +/// - Expected token errors are root causes the user should fix first +/// - Invalid syntax usage is a specific mistake at a location +/// - Naming validation errors are convention violations +/// - Semantic errors assume valid syntax +/// - Structural observations are often consequences of earlier errors +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DiagnosticKind { + // === Unclosed delimiters (highest priority) === + // These cause cascading errors throughout the rest of the file + UnclosedTree, + UnclosedSequence, + UnclosedAlternation, + + // === Expected token errors === + // User omitted something required - root cause errors + ExpectedExpression, + ExpectedTypeName, + ExpectedCaptureName, + ExpectedFieldName, + ExpectedSubtype, + + // === Invalid token/syntax usage === + // User wrote something that doesn't belong + EmptyTree, + BareIdentifier, + InvalidSeparator, + InvalidFieldEquals, + InvalidSupertypeSyntax, + ErrorTakesNoArguments, + RefCannotHaveChildren, + ErrorMissingOutsideParens, + UnsupportedPredicate, + UnexpectedToken, + + // === Naming validation === + // Convention violations - fixable with suggestions + CaptureNameHasDots, + CaptureNameHasHyphens, + CaptureNameUppercase, + DefNameLowercase, + DefNameHasSeparators, + BranchLabelHasSeparators, + FieldNameHasDots, + FieldNameHasHyphens, + FieldNameUppercase, + TypeNameInvalidChars, + + // === Semantic errors === + // Valid syntax, invalid semantics + DuplicateDefinition, + UndefinedReference, + MixedAltBranches, + RecursionNoEscape, + FieldSequenceValue, + + // === Structural observations (lowest priority) === + // Often consequences of earlier errors + UnnamedDefNotLast, +} + +impl DiagnosticKind { + /// Default severity for this kind. Can be overridden by policy. + pub fn default_severity(&self) -> Severity { + // All current diagnostics are errors. + // Naming conventions could become warnings in a lenient mode. + Severity::Error + } + + /// Whether this kind suppresses `other` when spans overlap. + /// + /// Uses enum discriminant ordering: lower position = higher priority. + /// A higher-priority diagnostic suppresses lower-priority ones in the same span. + pub fn suppresses(&self, other: &DiagnosticKind) -> bool { + self < other + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Severity { #[default] From 2a043ab021e2dd2a3fff2624615cf01ab122a64e Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 6 Dec 2025 09:23:52 -0300 Subject: [PATCH 02/15] Refactor diagnostics to use DiagnosticKind for categorization --- crates/plotnik-lib/src/diagnostics/message.rs | 95 ++++-- crates/plotnik-lib/src/diagnostics/mod.rs | 143 ++++++++- crates/plotnik-lib/src/diagnostics/printer.rs | 45 ++- crates/plotnik-lib/src/diagnostics/tests.rs | 272 +++++++++++++++--- crates/plotnik-lib/src/parser/core.rs | 34 ++- crates/plotnik-lib/src/parser/grammar.rs | 121 ++++++-- .../parser/tests/recovery/coverage_tests.rs | 4 - .../parser/tests/recovery/unexpected_tests.rs | 4 - .../parser/tests/recovery/validation_tests.rs | 4 - crates/plotnik-lib/src/query/alt_kinds.rs | 7 +- crates/plotnik-lib/src/query/dump.rs | 6 +- crates/plotnik-lib/src/query/mod.rs | 15 +- crates/plotnik-lib/src/query/recursion.rs | 11 +- crates/plotnik-lib/src/query/shapes.rs | 13 +- crates/plotnik-lib/src/query/symbol_table.rs | 10 +- 15 files changed, 652 insertions(+), 132 deletions(-) diff --git a/crates/plotnik-lib/src/diagnostics/message.rs b/crates/plotnik-lib/src/diagnostics/message.rs index 2b8731f1..7e9be201 100644 --- a/crates/plotnik-lib/src/diagnostics/message.rs +++ b/crates/plotnik-lib/src/diagnostics/message.rs @@ -35,11 +35,14 @@ pub enum DiagnosticKind { InvalidSeparator, InvalidFieldEquals, InvalidSupertypeSyntax, + InvalidTypeAnnotationSyntax, ErrorTakesNoArguments, RefCannotHaveChildren, ErrorMissingOutsideParens, UnsupportedPredicate, UnexpectedToken, + CaptureWithoutTarget, + LowercaseBranchLabel, // === Naming validation === // Convention violations - fixable with suggestions @@ -70,8 +73,6 @@ pub enum DiagnosticKind { impl DiagnosticKind { /// Default severity for this kind. Can be overridden by policy. pub fn default_severity(&self) -> Severity { - // All current diagnostics are errors. - // Naming conventions could become warnings in a lenient mode. Severity::Error } @@ -82,6 +83,70 @@ impl DiagnosticKind { pub fn suppresses(&self, other: &DiagnosticKind) -> bool { self < other } + + /// Default message for this diagnostic kind. + /// + /// Provides a sensible fallback; callers can override with context-specific messages. + pub fn default_message(&self) -> &'static str { + match self { + // Unclosed delimiters + Self::UnclosedTree => "unclosed tree; expected ')'", + Self::UnclosedSequence => "unclosed sequence; expected '}'", + Self::UnclosedAlternation => "unclosed alternation; expected ']'", + + // Expected token errors + Self::ExpectedExpression => "expected expression", + Self::ExpectedTypeName => "expected type name after '::'", + Self::ExpectedCaptureName => "expected capture name after '@'", + Self::ExpectedFieldName => "expected field name", + Self::ExpectedSubtype => "expected subtype after '/'", + + // Invalid token/syntax usage + Self::EmptyTree => "empty tree expression - expected node type or children", + Self::BareIdentifier => { + "bare identifier not allowed; nodes must be enclosed in parentheses" + } + Self::InvalidSeparator => "invalid separator; plotnik uses whitespace for separation", + Self::InvalidFieldEquals => "'=' is not valid for field constraints; use ':'", + Self::InvalidSupertypeSyntax => "references cannot use supertype syntax (/)", + Self::InvalidTypeAnnotationSyntax => "invalid type annotation syntax", + Self::ErrorTakesNoArguments => "(ERROR) takes no arguments", + Self::RefCannotHaveChildren => "reference cannot contain children", + Self::ErrorMissingOutsideParens => { + "ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...)" + } + Self::UnsupportedPredicate => { + "tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported" + } + Self::UnexpectedToken => "unexpected token", + Self::CaptureWithoutTarget => "capture '@' must follow an expression to capture", + Self::LowercaseBranchLabel => { + "tagged alternation labels must be Capitalized (they map to enum variants)" + } + + // Naming validation + Self::CaptureNameHasDots => "capture names cannot contain dots", + Self::CaptureNameHasHyphens => "capture names cannot contain hyphens", + Self::CaptureNameUppercase => "capture names must start with lowercase", + Self::DefNameLowercase => "definition names must start with uppercase", + Self::DefNameHasSeparators => "definition names cannot contain separators", + Self::BranchLabelHasSeparators => "branch labels cannot contain separators", + Self::FieldNameHasDots => "field names cannot contain dots", + Self::FieldNameHasHyphens => "field names cannot contain hyphens", + Self::FieldNameUppercase => "field names must start with lowercase", + Self::TypeNameInvalidChars => "type names cannot contain dots or hyphens", + + // Semantic errors + Self::DuplicateDefinition => "duplicate definition", + Self::UndefinedReference => "undefined reference", + Self::MixedAltBranches => "mixed tagged and untagged branches in alternation", + Self::RecursionNoEscape => "recursive pattern can never match: no escape path", + Self::FieldSequenceValue => "field value must match a single node, not a sequence", + + // Structural observations + Self::UnnamedDefNotLast => "unnamed definition must be last in file", + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -132,7 +197,7 @@ impl RelatedInfo { #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct DiagnosticMessage { - pub(crate) severity: Severity, + pub(crate) kind: DiagnosticKind, pub(crate) range: TextRange, pub(crate) message: String, pub(crate) fix: Option, @@ -140,9 +205,9 @@ pub(crate) struct DiagnosticMessage { } impl DiagnosticMessage { - pub(crate) fn error(range: TextRange, message: impl Into) -> Self { + pub(crate) fn new(kind: DiagnosticKind, range: TextRange, message: impl Into) -> Self { Self { - severity: Severity::Error, + kind, range, message: message.into(), fix: None, @@ -150,22 +215,20 @@ impl DiagnosticMessage { } } - pub(crate) fn warning(range: TextRange, message: impl Into) -> Self { - Self { - severity: Severity::Warning, - range, - message: message.into(), - fix: None, - related: Vec::new(), - } + pub(crate) fn with_default_message(kind: DiagnosticKind, range: TextRange) -> Self { + Self::new(kind, range, kind.default_message()) + } + + pub(crate) fn severity(&self) -> Severity { + self.kind.default_severity() } pub(crate) fn is_error(&self) -> bool { - self.severity == Severity::Error + self.severity() == Severity::Error } pub(crate) fn is_warning(&self) -> bool { - self.severity == Severity::Warning + self.severity() == Severity::Warning } } @@ -174,7 +237,7 @@ impl std::fmt::Display for DiagnosticMessage { write!( f, "{} at {}..{}: {}", - self.severity, + self.severity(), u32::from(self.range.start()), u32::from(self.range.end()), self.message diff --git a/crates/plotnik-lib/src/diagnostics/mod.rs b/crates/plotnik-lib/src/diagnostics/mod.rs index b265d4d4..aa8644fa 100644 --- a/crates/plotnik-lib/src/diagnostics/mod.rs +++ b/crates/plotnik-lib/src/diagnostics/mod.rs @@ -6,7 +6,7 @@ mod tests; use rowan::TextRange; -pub use message::Severity; +pub use message::{DiagnosticKind, Severity}; pub use printer::DiagnosticsPrinter; use message::{DiagnosticMessage, Fix, RelatedInfo}; @@ -29,17 +29,29 @@ impl Diagnostics { } } + /// Create a diagnostic with the given kind and span. + /// + /// Uses the kind's default message. Call `.message()` on the builder to override. + pub fn report(&mut self, kind: DiagnosticKind, range: TextRange) -> DiagnosticBuilder<'_> { + DiagnosticBuilder { + diagnostics: self, + message: DiagnosticMessage::with_default_message(kind, range), + } + } + + /// Create an error diagnostic (legacy API, prefer `report()`). pub fn error(&mut self, msg: impl Into, range: TextRange) -> DiagnosticBuilder<'_> { DiagnosticBuilder { diagnostics: self, - message: DiagnosticMessage::error(range, msg), + message: DiagnosticMessage::new(DiagnosticKind::UnexpectedToken, range, msg), } } + /// Create a warning diagnostic (legacy API, prefer `report()`). pub fn warning(&mut self, msg: impl Into, range: TextRange) -> DiagnosticBuilder<'_> { DiagnosticBuilder { diagnostics: self, - message: DiagnosticMessage::warning(range, msg), + message: DiagnosticMessage::new(DiagnosticKind::UnexpectedToken, range, msg), } } @@ -67,10 +79,57 @@ impl Diagnostics { self.messages.iter().filter(|d| d.is_warning()).count() } + /// Returns diagnostics with cascading errors suppressed. + /// + /// Suppression rule: when a higher-priority diagnostic's span contains + /// a lower-priority diagnostic's span, the lower-priority one is suppressed. + pub(crate) fn filtered(&self) -> Vec<&DiagnosticMessage> { + if self.messages.is_empty() { + return Vec::new(); + } + + let mut suppressed = vec![false; self.messages.len()]; + + // O(n²) but n is typically small (< 100 diagnostics) + for (i, outer) in self.messages.iter().enumerate() { + for (j, inner) in self.messages.iter().enumerate() { + if i == j || suppressed[j] { + continue; + } + + // Check if outer contains inner and has higher priority + if span_contains(outer.range, inner.range) && outer.kind.suppresses(&inner.kind) { + suppressed[j] = true; + } + } + } + + self.messages + .iter() + .enumerate() + .filter(|(i, _)| !suppressed[*i]) + .map(|(_, m)| m) + .collect() + } + + /// Raw access to all diagnostics (for debugging/testing). + #[allow(dead_code)] + pub(crate) fn raw(&self) -> &[DiagnosticMessage] { + &self.messages + } + pub fn printer<'a>(&'a self, source: &'a str) -> DiagnosticsPrinter<'a> { DiagnosticsPrinter::new(&self.messages, source) } + /// Printer that uses filtered diagnostics. + pub fn filtered_printer<'a>(&'a self, source: &'a str) -> FilteredDiagnosticsPrinter<'a> { + FilteredDiagnosticsPrinter { + diagnostics: self, + source, + } + } + pub fn render(&self, source: &str) -> String { self.printer(source).render() } @@ -79,12 +138,85 @@ impl Diagnostics { self.printer(source).colored(colored).render() } + pub fn render_filtered(&self, source: &str) -> String { + self.filtered_printer(source).render() + } + + pub fn render_filtered_colored(&self, source: &str, colored: bool) -> String { + self.filtered_printer(source).colored(colored).render() + } + pub fn extend(&mut self, other: Diagnostics) { self.messages.extend(other.messages); } } +/// Printer wrapper that uses filtered diagnostics. +pub struct FilteredDiagnosticsPrinter<'a> { + diagnostics: &'a Diagnostics, + source: &'a str, +} + +impl<'a> FilteredDiagnosticsPrinter<'a> { + pub fn path(self, path: &'a str) -> FilteredDiagnosticsPrinterWithPath<'a> { + FilteredDiagnosticsPrinterWithPath { + diagnostics: self.diagnostics, + source: self.source, + path: Some(path), + colored: false, + } + } + + pub fn colored(self, colored: bool) -> FilteredDiagnosticsPrinterWithPath<'a> { + FilteredDiagnosticsPrinterWithPath { + diagnostics: self.diagnostics, + source: self.source, + path: None, + colored, + } + } + + pub fn render(&self) -> String { + let filtered = self.diagnostics.filtered(); + DiagnosticsPrinter::from_refs(&filtered, self.source).render() + } +} + +pub struct FilteredDiagnosticsPrinterWithPath<'a> { + diagnostics: &'a Diagnostics, + source: &'a str, + path: Option<&'a str>, + colored: bool, +} + +impl<'a> FilteredDiagnosticsPrinterWithPath<'a> { + pub fn path(mut self, path: &'a str) -> Self { + self.path = Some(path); + self + } + + pub fn colored(mut self, colored: bool) -> Self { + self.colored = colored; + self + } + + pub fn render(&self) -> String { + let filtered = self.diagnostics.filtered(); + let mut printer = DiagnosticsPrinter::from_refs(&filtered, self.source); + if let Some(p) = self.path { + printer = printer.path(p); + } + printer.colored(self.colored).render() + } +} + impl<'a> DiagnosticBuilder<'a> { + /// Override the default message for this diagnostic kind. + pub fn message(mut self, msg: impl Into) -> Self { + self.message.message = msg.into(); + self + } + pub fn related_to(mut self, msg: impl Into, range: TextRange) -> Self { self.message.related.push(RelatedInfo::new(range, msg)); self @@ -99,3 +231,8 @@ impl<'a> DiagnosticBuilder<'a> { self.diagnostics.messages.push(self.message); } } + +/// Check if outer span fully contains inner span. +fn span_contains(outer: TextRange, inner: TextRange) -> bool { + outer.start() <= inner.start() && inner.end() <= outer.end() +} diff --git a/crates/plotnik-lib/src/diagnostics/printer.rs b/crates/plotnik-lib/src/diagnostics/printer.rs index fe05e936..3ef5608f 100644 --- a/crates/plotnik-lib/src/diagnostics/printer.rs +++ b/crates/plotnik-lib/src/diagnostics/printer.rs @@ -8,16 +8,55 @@ use rowan::TextRange; use super::message::{DiagnosticMessage, Severity}; pub struct DiagnosticsPrinter<'a> { - diagnostics: &'a [DiagnosticMessage], + diagnostics: DiagnosticsSlice<'a>, source: &'a str, path: Option<&'a str>, colored: bool, } +enum DiagnosticsSlice<'a> { + Borrowed(&'a [DiagnosticMessage]), + Refs(Vec<&'a DiagnosticMessage>), +} + +impl<'a> DiagnosticsSlice<'a> { + fn iter(&self) -> impl Iterator { + match self { + DiagnosticsSlice::Borrowed(slice) => DiagnosticsIter::Borrowed(slice.iter()), + DiagnosticsSlice::Refs(vec) => DiagnosticsIter::Refs(vec.iter()), + } + } +} + +enum DiagnosticsIter<'a, 'b> { + Borrowed(std::slice::Iter<'a, DiagnosticMessage>), + Refs(std::slice::Iter<'b, &'a DiagnosticMessage>), +} + +impl<'a, 'b> Iterator for DiagnosticsIter<'a, 'b> { + type Item = &'a DiagnosticMessage; + + fn next(&mut self) -> Option { + match self { + DiagnosticsIter::Borrowed(iter) => iter.next(), + DiagnosticsIter::Refs(iter) => iter.next().copied(), + } + } +} + impl<'a> DiagnosticsPrinter<'a> { pub(crate) fn new(diagnostics: &'a [DiagnosticMessage], source: &'a str) -> Self { Self { - diagnostics, + diagnostics: DiagnosticsSlice::Borrowed(diagnostics), + source, + path: None, + colored: false, + } + } + + pub(crate) fn from_refs(diagnostics: &[&'a DiagnosticMessage], source: &'a str) -> Self { + Self { + diagnostics: DiagnosticsSlice::Refs(diagnostics.to_vec()), source, path: None, colored: false, @@ -68,7 +107,7 @@ impl<'a> DiagnosticsPrinter<'a> { ); } - let level = severity_to_level(diag.severity); + let level = severity_to_level(diag.severity()); let title_group = level.primary_title(&diag.message).element(snippet); let mut report: Vec = vec![title_group]; diff --git a/crates/plotnik-lib/src/diagnostics/tests.rs b/crates/plotnik-lib/src/diagnostics/tests.rs index 37e68726..5d1f6ebc 100644 --- a/crates/plotnik-lib/src/diagnostics/tests.rs +++ b/crates/plotnik-lib/src/diagnostics/tests.rs @@ -9,34 +9,54 @@ fn severity_display() { } #[test] -fn error_builder() { +fn report_with_default_message() { let mut diagnostics = Diagnostics::new(); diagnostics - .error("test error", TextRange::new(0.into(), 5.into())) + .report( + DiagnosticKind::ExpectedTypeName, + TextRange::new(0.into(), 5.into()), + ) + .emit(); + + assert_eq!(diagnostics.len(), 1); + assert!(diagnostics.has_errors()); +} + +#[test] +fn report_with_custom_message() { + let mut diagnostics = Diagnostics::new(); + diagnostics + .report( + DiagnosticKind::ExpectedTypeName, + TextRange::new(0.into(), 5.into()), + ) + .message("expected type name after '::' (e.g., ::MyType)") .emit(); assert_eq!(diagnostics.len(), 1); assert!(diagnostics.has_errors()); - assert!(!diagnostics.has_warnings()); } #[test] -fn warning_builder() { +fn error_builder_legacy() { let mut diagnostics = Diagnostics::new(); diagnostics - .warning("test warning", TextRange::new(0.into(), 5.into())) + .error("test error", TextRange::new(0.into(), 5.into())) .emit(); assert_eq!(diagnostics.len(), 1); - assert!(!diagnostics.has_errors()); - assert!(diagnostics.has_warnings()); + assert!(diagnostics.has_errors()); } #[test] fn builder_with_related() { let mut diagnostics = Diagnostics::new(); diagnostics - .error("primary", TextRange::new(0.into(), 5.into())) + .report( + DiagnosticKind::UnclosedTree, + TextRange::new(0.into(), 5.into()), + ) + .message("primary") .related_to("related info", TextRange::new(6.into(), 10.into())) .emit(); @@ -56,7 +76,11 @@ fn builder_with_related() { fn builder_with_fix() { let mut diagnostics = Diagnostics::new(); diagnostics - .error("fixable", TextRange::new(0.into(), 5.into())) + .report( + DiagnosticKind::InvalidFieldEquals, + TextRange::new(0.into(), 5.into()), + ) + .message("fixable") .fix("apply this fix", "fixed") .emit(); @@ -79,7 +103,11 @@ fn builder_with_fix() { fn builder_with_all_options() { let mut diagnostics = Diagnostics::new(); diagnostics - .error("main error", TextRange::new(0.into(), 5.into())) + .report( + DiagnosticKind::UnclosedTree, + TextRange::new(0.into(), 5.into()), + ) + .message("main error") .related_to("see also", TextRange::new(6.into(), 11.into())) .related_to("and here", TextRange::new(12.into(), 17.into())) .fix("try this", "HELLO") @@ -107,7 +135,11 @@ fn builder_with_all_options() { fn printer_colored() { let mut diagnostics = Diagnostics::new(); diagnostics - .error("test", TextRange::new(0.into(), 5.into())) + .report( + DiagnosticKind::EmptyTree, + TextRange::new(0.into(), 5.into()), + ) + .message("test") .emit(); let result = diagnostics.printer("hello").colored(true).render(); @@ -126,7 +158,11 @@ fn printer_empty_diagnostics() { fn printer_with_path() { let mut diagnostics = Diagnostics::new(); diagnostics - .error("test error", TextRange::new(0.into(), 5.into())) + .report( + DiagnosticKind::UndefinedReference, + TextRange::new(0.into(), 5.into()), + ) + .message("test error") .emit(); let result = diagnostics.printer("hello world").path("test.pql").render(); @@ -143,7 +179,11 @@ fn printer_with_path() { fn printer_zero_width_span() { let mut diagnostics = Diagnostics::new(); diagnostics - .error("zero width error", TextRange::empty(0.into())) + .report( + DiagnosticKind::ExpectedExpression, + TextRange::empty(0.into()), + ) + .message("zero width error") .emit(); let result = diagnostics.printer("hello").render(); @@ -159,7 +199,11 @@ fn printer_zero_width_span() { fn printer_related_zero_width() { let mut diagnostics = Diagnostics::new(); diagnostics - .error("primary", TextRange::new(0.into(), 5.into())) + .report( + DiagnosticKind::UnclosedTree, + TextRange::new(0.into(), 5.into()), + ) + .message("primary") .related_to("zero width related", TextRange::empty(6.into())) .emit(); @@ -178,10 +222,18 @@ fn printer_related_zero_width() { fn printer_multiple_diagnostics() { let mut diagnostics = Diagnostics::new(); diagnostics - .error("first error", TextRange::new(0.into(), 5.into())) + .report( + DiagnosticKind::UnclosedTree, + TextRange::new(0.into(), 5.into()), + ) + .message("first error") .emit(); diagnostics - .error("second error", TextRange::new(6.into(), 10.into())) + .report( + DiagnosticKind::UndefinedReference, + TextRange::new(6.into(), 10.into()), + ) + .message("second error") .emit(); let result = diagnostics.printer("hello world!").render(); @@ -198,35 +250,187 @@ fn printer_multiple_diagnostics() { } #[test] -fn printer_warning() { +fn diagnostics_collection_methods() { let mut diagnostics = Diagnostics::new(); diagnostics - .warning("a warning", TextRange::new(0.into(), 5.into())) + .report(DiagnosticKind::UnclosedTree, TextRange::empty(0.into())) + .emit(); + diagnostics + .report( + DiagnosticKind::UndefinedReference, + TextRange::empty(1.into()), + ) .emit(); - let result = diagnostics.printer("hello").render(); - insta::assert_snapshot!(result, @r" - warning: a warning - | - 1 | hello - | ^^^^^ a warning - "); + assert!(!diagnostics.is_empty()); + assert_eq!(diagnostics.len(), 2); + assert!(diagnostics.has_errors()); + assert_eq!(diagnostics.error_count(), 2); } #[test] -fn diagnostics_collection_methods() { +fn diagnostic_kind_default_severity() { + assert_eq!( + DiagnosticKind::UnclosedTree.default_severity(), + Severity::Error + ); + assert_eq!( + DiagnosticKind::UnnamedDefNotLast.default_severity(), + Severity::Error + ); +} + +#[test] +fn diagnostic_kind_suppression_order() { + // Higher priority (earlier in enum) suppresses lower priority (later in enum) + assert!(DiagnosticKind::UnclosedTree.suppresses(&DiagnosticKind::UnnamedDefNotLast)); + assert!(DiagnosticKind::UnclosedTree.suppresses(&DiagnosticKind::UndefinedReference)); + assert!(DiagnosticKind::ExpectedExpression.suppresses(&DiagnosticKind::UnnamedDefNotLast)); + + // Same kind doesn't suppress itself + assert!(!DiagnosticKind::UnclosedTree.suppresses(&DiagnosticKind::UnclosedTree)); + + // Lower priority doesn't suppress higher priority + assert!(!DiagnosticKind::UnnamedDefNotLast.suppresses(&DiagnosticKind::UnclosedTree)); +} + +#[test] +fn diagnostic_kind_default_messages() { + assert_eq!( + DiagnosticKind::UnclosedTree.default_message(), + "unclosed tree; expected ')'" + ); + assert_eq!( + DiagnosticKind::UnclosedSequence.default_message(), + "unclosed sequence; expected '}'" + ); + assert_eq!( + DiagnosticKind::UnclosedAlternation.default_message(), + "unclosed alternation; expected ']'" + ); + assert_eq!( + DiagnosticKind::ExpectedExpression.default_message(), + "expected expression" + ); +} + +// === Filtering/suppression tests === + +#[test] +fn filtered_no_suppression_disjoint_spans() { let mut diagnostics = Diagnostics::new(); + // Two errors at different positions - both should show diagnostics - .error("error", TextRange::empty(0.into())) + .report( + DiagnosticKind::UnclosedTree, + TextRange::new(0.into(), 5.into()), + ) .emit(); diagnostics - .warning("warning", TextRange::empty(1.into())) + .report( + DiagnosticKind::UndefinedReference, + TextRange::new(10.into(), 15.into()), + ) .emit(); - assert!(!diagnostics.is_empty()); - assert_eq!(diagnostics.len(), 2); - assert!(diagnostics.has_errors()); - assert!(diagnostics.has_warnings()); - assert_eq!(diagnostics.error_count(), 1); - assert_eq!(diagnostics.warning_count(), 1); + let filtered = diagnostics.filtered(); + assert_eq!(filtered.len(), 2); +} + +#[test] +fn filtered_suppresses_lower_priority_contained() { + let mut diagnostics = Diagnostics::new(); + // Higher priority error (UnclosedTree) contains lower priority (UnnamedDefNotLast) + diagnostics + .report( + DiagnosticKind::UnclosedTree, + TextRange::new(0.into(), 20.into()), + ) + .emit(); + diagnostics + .report( + DiagnosticKind::UnnamedDefNotLast, + TextRange::new(5.into(), 15.into()), + ) + .emit(); + + let filtered = diagnostics.filtered(); + assert_eq!(filtered.len(), 1); + assert_eq!(filtered[0].kind, DiagnosticKind::UnclosedTree); +} + +#[test] +fn filtered_does_not_suppress_higher_priority() { + let mut diagnostics = Diagnostics::new(); + // Lower priority error (UnnamedDefNotLast) cannot suppress higher priority (UnclosedTree) + diagnostics + .report( + DiagnosticKind::UnnamedDefNotLast, + TextRange::new(0.into(), 20.into()), + ) + .emit(); + diagnostics + .report( + DiagnosticKind::UnclosedTree, + TextRange::new(5.into(), 15.into()), + ) + .emit(); + + let filtered = diagnostics.filtered(); + // Both should remain - lower priority cannot suppress higher + assert_eq!(filtered.len(), 2); +} + +#[test] +fn filtered_same_span_higher_priority_wins() { + let mut diagnostics = Diagnostics::new(); + // Two errors at exact same span + diagnostics + .report( + DiagnosticKind::UnclosedTree, + TextRange::new(0.into(), 10.into()), + ) + .emit(); + diagnostics + .report( + DiagnosticKind::UnnamedDefNotLast, + TextRange::new(0.into(), 10.into()), + ) + .emit(); + + let filtered = diagnostics.filtered(); + assert_eq!(filtered.len(), 1); + assert_eq!(filtered[0].kind, DiagnosticKind::UnclosedTree); +} + +#[test] +fn filtered_empty_diagnostics() { + let diagnostics = Diagnostics::new(); + let filtered = diagnostics.filtered(); + assert!(filtered.is_empty()); +} + +#[test] +fn render_filtered() { + let mut diagnostics = Diagnostics::new(); + // Add overlapping errors where one should be suppressed + diagnostics + .report( + DiagnosticKind::UnclosedTree, + TextRange::new(0.into(), 20.into()), + ) + .message("unclosed tree") + .emit(); + diagnostics + .report( + DiagnosticKind::UnnamedDefNotLast, + TextRange::new(5.into(), 15.into()), + ) + .message("unnamed def") + .emit(); + + let result = diagnostics.render_filtered("(function_declaration"); + // Should only show the unclosed tree error + assert!(result.contains("unclosed tree")); + assert!(!result.contains("unnamed def")); } diff --git a/crates/plotnik-lib/src/parser/core.rs b/crates/plotnik-lib/src/parser/core.rs index 2d459d6c..4bc2a296 100644 --- a/crates/plotnik-lib/src/parser/core.rs +++ b/crates/plotnik-lib/src/parser/core.rs @@ -6,7 +6,7 @@ use super::ast::Root; use super::cst::token_sets::ROOT_EXPR_FIRST; use super::cst::{SyntaxKind, SyntaxNode, TokenSet}; use super::lexer::{Token, token_text}; -use crate::diagnostics::Diagnostics; +use crate::diagnostics::{DiagnosticKind, Diagnostics}; use crate::Error; @@ -254,22 +254,25 @@ impl<'src> Parser<'src> { if self.eat(kind) { return true; } - self.error(format!("expected {}", what)); + self.error( + DiagnosticKind::UnexpectedToken, + format!("expected {}", what), + ); false } - pub(super) fn error(&mut self, message: impl Into) { + pub(super) fn error(&mut self, kind: DiagnosticKind, message: impl Into) { let range = self.current_span(); let pos = range.start(); if self.last_diagnostic_pos == Some(pos) { return; } self.last_diagnostic_pos = Some(pos); - self.diagnostics.error(message, range).emit(); + self.diagnostics.report(kind, range).message(message).emit(); } - pub(super) fn error_and_bump(&mut self, message: &str) { - self.error(message); + pub(super) fn error_and_bump(&mut self, kind: DiagnosticKind, message: &str) { + self.error(kind, message); if !self.eof() { self.start_node(SyntaxKind::Error); self.bump(); @@ -278,14 +281,19 @@ impl<'src> Parser<'src> { } #[allow(dead_code)] - pub(super) fn error_recover(&mut self, message: &str, recovery: TokenSet) { + pub(super) fn error_recover( + &mut self, + kind: DiagnosticKind, + message: &str, + recovery: TokenSet, + ) { if self.at_set(recovery) || self.should_stop() { - self.error(message); + self.error(kind, message); return; } self.start_node(SyntaxKind::Error); - self.error(message); + self.error(kind, message); while !self.at_set(recovery) && !self.should_stop() { self.bump(); } @@ -354,6 +362,7 @@ impl<'src> Parser<'src> { pub(super) fn error_with_related( &mut self, + kind: DiagnosticKind, message: impl Into, related_msg: impl Into, related_range: TextRange, @@ -365,7 +374,8 @@ impl<'src> Parser<'src> { } self.last_diagnostic_pos = Some(pos); self.diagnostics - .error(message, range) + .report(kind, range) + .message(message) .related_to(related_msg, related_range) .emit(); } @@ -381,6 +391,7 @@ impl<'src> Parser<'src> { pub(super) fn error_with_fix( &mut self, + kind: DiagnosticKind, range: TextRange, message: impl Into, fix_description: impl Into, @@ -392,7 +403,8 @@ impl<'src> Parser<'src> { } self.last_diagnostic_pos = Some(pos); self.diagnostics - .error(message, range) + .report(kind, range) + .message(message) .fix(fix_description, fix_replacement) .emit(); } diff --git a/crates/plotnik-lib/src/parser/grammar.rs b/crates/plotnik-lib/src/parser/grammar.rs index 86d0b807..f979c613 100644 --- a/crates/plotnik-lib/src/parser/grammar.rs +++ b/crates/plotnik-lib/src/parser/grammar.rs @@ -13,6 +13,7 @@ use super::cst::token_sets::{ }; use super::cst::{SyntaxKind, TokenSet}; use super::lexer::token_text; +use crate::diagnostics::DiagnosticKind; impl Parser<'_> { pub fn parse_root(&mut self) { @@ -43,13 +44,11 @@ impl Parser<'_> { for span in &unnamed_def_spans[..unnamed_def_spans.len() - 1] { let def_text = &self.source[usize::from(span.start())..usize::from(span.end())]; self.diagnostics - .error( - format!( - "unnamed definition must be last in file; add a name: `Name = {}`", - def_text.trim() - ), - *span, - ) + .report(DiagnosticKind::UnnamedDefNotLast, *span) + .message(format!( + "unnamed definition must be last in file; add a name: `Name = {}`", + def_text.trim() + )) .emit(); } } @@ -74,7 +73,10 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr(); } else { - self.error("expected expression after '=' in named definition"); + self.error( + DiagnosticKind::ExpectedExpression, + "expected expression after '=' in named definition", + ); } self.finish_node(); @@ -88,15 +90,20 @@ impl Parser<'_> { self.parse_expr(); true } else if kind == SyntaxKind::At { - self.error_and_bump("capture '@' must follow an expression to capture"); + self.error_and_bump( + DiagnosticKind::CaptureWithoutTarget, + "capture '@' must follow an expression to capture", + ); false } else if kind == SyntaxKind::Predicate { self.error_and_bump( + DiagnosticKind::UnsupportedPredicate, "tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported", ); false } else { self.error_and_bump( + DiagnosticKind::UnexpectedToken, "unexpected token; expected an expression like (node), [choice], {sequence}, \"literal\", or _", ); false @@ -137,11 +144,15 @@ impl Parser<'_> { SyntaxKind::Id => self.parse_tree_or_field(), SyntaxKind::KwError | SyntaxKind::KwMissing => { self.error_and_bump( + DiagnosticKind::ErrorMissingOutsideParens, "ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...)", ); } _ => { - self.error_and_bump("unexpected token; expected an expression"); + self.error_and_bump( + DiagnosticKind::UnexpectedToken, + "unexpected token; expected an expression", + ); } } @@ -166,7 +177,10 @@ impl Parser<'_> { match self.peek() { SyntaxKind::ParenClose => { self.start_node_at(checkpoint, SyntaxKind::Tree); - self.error("empty tree expression - expected node type or children"); + self.error( + DiagnosticKind::EmptyTree, + "empty tree expression - expected node type or children", + ); self.pop_delimiter(); self.bump(); // consume ')' self.finish_node(); @@ -191,7 +205,10 @@ impl Parser<'_> { if self.peek() == SyntaxKind::Slash { if is_ref { self.start_node_at(checkpoint, SyntaxKind::Tree); - self.error("references cannot use supertype syntax (/)"); + self.error( + DiagnosticKind::InvalidSupertypeSyntax, + "references cannot use supertype syntax (/)", + ); is_ref = false; } self.bump(); @@ -204,6 +221,7 @@ impl Parser<'_> { } _ => { self.error( + DiagnosticKind::ExpectedSubtype, "expected subtype after '/' (e.g., expression/binary_expression)", ); } @@ -214,7 +232,10 @@ impl Parser<'_> { self.start_node_at(checkpoint, SyntaxKind::Tree); self.bump(); if self.peek() != SyntaxKind::ParenClose { - self.error("(ERROR) takes no arguments"); + self.error( + DiagnosticKind::ErrorTakesNoArguments, + "(ERROR) takes no arguments", + ); self.parse_children(SyntaxKind::ParenClose, TREE_RECOVERY); } self.pop_delimiter(); @@ -258,10 +279,8 @@ impl Parser<'_> { if let Some(name) = &ref_name { self.diagnostics - .error( - format!("reference `{}` cannot contain children", name), - children_span, - ) + .report(DiagnosticKind::RefCannotHaveChildren, children_span) + .message(format!("reference `{}` cannot contain children", name)) .emit(); } } else if is_ref { @@ -286,9 +305,9 @@ impl Parser<'_> { fn parse_children(&mut self, until: SyntaxKind, recovery: TokenSet) { loop { if self.eof() { - let (construct, delim) = match until { - SyntaxKind::ParenClose => ("tree", "')'"), - SyntaxKind::BraceClose => ("sequence", "'}'"), + let (construct, delim, kind) = match until { + SyntaxKind::ParenClose => ("tree", "')'", DiagnosticKind::UnclosedTree), + SyntaxKind::BraceClose => ("sequence", "'}'", DiagnosticKind::UnclosedSequence), _ => panic!( "parse_children: unexpected delimiter {:?} (only ParenClose/BraceClose supported)", until @@ -301,7 +320,7 @@ impl Parser<'_> { (caller must push delimiter before calling)" ) }); - self.error_with_related(msg, format!("{construct} started here"), open.span); + self.error_with_related(kind, msg, format!("{construct} started here"), open.span); break; } if self.has_fatal_error() { @@ -321,6 +340,7 @@ impl Parser<'_> { } if kind == SyntaxKind::Predicate { self.error_and_bump( + DiagnosticKind::UnsupportedPredicate, "tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported", ); continue; @@ -329,6 +349,7 @@ impl Parser<'_> { break; } self.error_and_bump( + DiagnosticKind::UnexpectedToken, "unexpected token; expected a child expression or closing delimiter", ); } @@ -358,7 +379,12 @@ impl Parser<'_> { (caller must push delimiter before calling)" ) }); - self.error_with_related(msg, "alternation started here", open.span); + self.error_with_related( + DiagnosticKind::UnclosedAlternation, + msg, + "alternation started here", + open.span, + ); break; } if self.has_fatal_error() { @@ -394,6 +420,7 @@ impl Parser<'_> { break; } self.error_and_bump( + DiagnosticKind::UnexpectedToken, "unexpected token; expected a child expression or closing delimiter", ); } @@ -414,7 +441,10 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr(); } else { - self.error("expected expression after branch label"); + self.error( + DiagnosticKind::ExpectedExpression, + "expected expression after branch label", + ); } self.finish_node(); @@ -429,6 +459,7 @@ impl Parser<'_> { let capitalized = capitalize_first(label_text); self.error_with_fix( + DiagnosticKind::LowercaseBranchLabel, span, "tagged alternation labels must be Capitalized (they map to enum variants)", format!("capitalize as `{}`", capitalized), @@ -442,7 +473,10 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr(); } else { - self.error("expected expression after branch label"); + self.error( + DiagnosticKind::ExpectedExpression, + "expected expression after branch label", + ); } self.finish_node(); @@ -494,7 +528,10 @@ impl Parser<'_> { self.bump(); // consume At if self.peek() != SyntaxKind::Id { - self.error("expected capture name after '@'"); + self.error( + DiagnosticKind::ExpectedCaptureName, + "expected capture name after '@'", + ); return; } @@ -524,7 +561,10 @@ impl Parser<'_> { self.bump(); self.validate_type_name(text, span); } else { - self.error("expected type name after '::' (e.g., ::MyType or ::string)"); + self.error( + DiagnosticKind::ExpectedTypeName, + "expected type name after '::' (e.g., ::MyType or ::string)", + ); } self.finish_node(); @@ -540,6 +580,7 @@ impl Parser<'_> { let span = self.current_span(); self.error_with_fix( + DiagnosticKind::InvalidTypeAnnotationSyntax, span, "single colon is not valid for type annotations", "use '::'", @@ -569,7 +610,10 @@ impl Parser<'_> { self.expect(SyntaxKind::Negation, "'!' for negated field"); if self.peek() != SyntaxKind::Id { - self.error("expected field name after '!' (e.g., !value)"); + self.error( + DiagnosticKind::ExpectedFieldName, + "expected field name after '!' (e.g., !value)", + ); self.finish_node(); return; } @@ -591,6 +635,7 @@ impl Parser<'_> { } else { // Bare identifiers are not valid expressions; trees require parentheses self.error_and_bump( + DiagnosticKind::BareIdentifier, "bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier)", ); } @@ -615,7 +660,10 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr_no_suffix(); } else { - self.error("expected expression after field name"); + self.error( + DiagnosticKind::ExpectedExpression, + "expected expression after field name", + ); } self.finish_node(); @@ -629,6 +677,7 @@ impl Parser<'_> { self.peek(); let span = self.current_span(); self.error_with_fix( + DiagnosticKind::InvalidFieldEquals, span, "'=' is not valid for field constraints", "use ':'", @@ -639,7 +688,10 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr(); } else { - self.error("expected expression after field name"); + self.error( + DiagnosticKind::ExpectedExpression, + "expected expression after field name", + ); } self.finish_node(); @@ -659,6 +711,7 @@ impl Parser<'_> { ), }; self.error_with_fix( + DiagnosticKind::InvalidSeparator, span, format!( "'{}' is not valid syntax; plotnik uses whitespace for separation", @@ -694,6 +747,7 @@ impl Parser<'_> { let suggested = name.replace(['.', '-'], "_"); let suggested = to_snake_case(&suggested); self.error_with_fix( + DiagnosticKind::CaptureNameHasDots, span, "capture names cannot contain dots", format!("captures become struct fields; use @{} instead", suggested), @@ -706,6 +760,7 @@ impl Parser<'_> { let suggested = name.replace('-', "_"); let suggested = to_snake_case(&suggested); self.error_with_fix( + DiagnosticKind::CaptureNameHasHyphens, span, "capture names cannot contain hyphens", format!("captures become struct fields; use @{} instead", suggested), @@ -717,6 +772,7 @@ impl Parser<'_> { if name.chars().next().is_some_and(|c| c.is_ascii_uppercase()) { let suggested = to_snake_case(name); self.error_with_fix( + DiagnosticKind::CaptureNameUppercase, span, "capture names must start with lowercase", format!( @@ -733,6 +789,7 @@ impl Parser<'_> { if !name.chars().next().is_some_and(|c| c.is_ascii_uppercase()) { let suggested = to_pascal_case(name); self.error_with_fix( + DiagnosticKind::DefNameLowercase, span, "definition names must start with uppercase", format!( @@ -747,6 +804,7 @@ impl Parser<'_> { if name.contains('_') || name.contains('-') || name.contains('.') { let suggested = to_pascal_case(name); self.error_with_fix( + DiagnosticKind::DefNameHasSeparators, span, "definition names cannot contain separators", format!( @@ -763,6 +821,7 @@ impl Parser<'_> { if name.contains('_') || name.contains('-') || name.contains('.') { let suggested = to_pascal_case(name); self.error_with_fix( + DiagnosticKind::BranchLabelHasSeparators, span, "branch labels cannot contain separators", format!( @@ -780,6 +839,7 @@ impl Parser<'_> { let suggested = name.replace(['.', '-'], "_"); let suggested = to_snake_case(&suggested); self.error_with_fix( + DiagnosticKind::FieldNameHasDots, span, "field names cannot contain dots", format!("field names must be snake_case; use {}: instead", suggested), @@ -792,6 +852,7 @@ impl Parser<'_> { let suggested = name.replace('-', "_"); let suggested = to_snake_case(&suggested); self.error_with_fix( + DiagnosticKind::FieldNameHasHyphens, span, "field names cannot contain hyphens", format!("field names must be snake_case; use {}: instead", suggested), @@ -803,6 +864,7 @@ impl Parser<'_> { if name.chars().next().is_some_and(|c| c.is_ascii_uppercase()) { let suggested = to_snake_case(name); self.error_with_fix( + DiagnosticKind::FieldNameUppercase, span, "field names must start with lowercase", format!("field names must be snake_case; use {}: instead", suggested), @@ -816,6 +878,7 @@ impl Parser<'_> { if name.contains('.') || name.contains('-') { let suggested = to_pascal_case(name); self.error_with_fix( + DiagnosticKind::TypeNameInvalidChars, span, "type names cannot contain dots or hyphens", format!( 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 f1418da7..29ea15b0 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs @@ -139,10 +139,6 @@ fn named_def_missing_equals_with_garbage() { | 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` "#); } diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs index a6fd39e0..87f55a95 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs @@ -194,10 +194,6 @@ fn predicate_match() { | 1 | (identifier) #match? @name "test" | ^^^^^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (identifier)` - error: unnamed definition must be last in file; add a name: `Name = name` - | - 1 | (identifier) #match? @name "test" - | ^^^^ unnamed definition must be last in file; add a name: `Name = name` "#); } 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 fd4a4361..899e5339 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs @@ -197,10 +197,6 @@ fn named_def_missing_equals() { | 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` "); } diff --git a/crates/plotnik-lib/src/query/alt_kinds.rs b/crates/plotnik-lib/src/query/alt_kinds.rs index af16df76..56ffa110 100644 --- a/crates/plotnik-lib/src/query/alt_kinds.rs +++ b/crates/plotnik-lib/src/query/alt_kinds.rs @@ -9,6 +9,7 @@ use super::Query; use super::invariants::{ assert_alt_no_bare_exprs, assert_root_no_bare_exprs, ensure_both_branch_kinds, }; +use crate::diagnostics::DiagnosticKind; use crate::parser::{AltExpr, AltKind, Branch, Expr}; impl Query<'_> { @@ -78,10 +79,8 @@ impl Query<'_> { let untagged_range = branch_range(untagged_branch); self.alt_kind_diagnostics - .error( - "mixed tagged and untagged branches in alternation", - untagged_range, - ) + .report(DiagnosticKind::MixedAltBranches, untagged_range) + .message("mixed tagged and untagged branches in alternation") .related_to("tagged branch here", tagged_range) .emit(); } diff --git a/crates/plotnik-lib/src/query/dump.rs b/crates/plotnik-lib/src/query/dump.rs index 1f7ed97b..9f2f7219 100644 --- a/crates/plotnik-lib/src/query/dump.rs +++ b/crates/plotnik-lib/src/query/dump.rs @@ -30,7 +30,11 @@ mod test_helpers { } pub fn dump_diagnostics(&self) -> String { - self.diagnostics().render(self.source) + self.diagnostics().render_filtered(self.source) + } + + pub fn dump_diagnostics_raw(&self) -> String { + self.diagnostics_raw().render(self.source) } } } diff --git a/crates/plotnik-lib/src/query/mod.rs b/crates/plotnik-lib/src/query/mod.rs index 6d4491ff..c768deac 100644 --- a/crates/plotnik-lib/src/query/mod.rs +++ b/crates/plotnik-lib/src/query/mod.rs @@ -188,8 +188,11 @@ impl<'a> Query<'a> { .unwrap_or(ShapeCardinality::One) } - /// All diagnostics combined from all passes. - pub fn diagnostics(&self) -> Diagnostics { + /// All diagnostics combined from all passes (unfiltered). + /// + /// Use this for debugging or when you need to see all diagnostics + /// including cascading errors. + pub fn diagnostics_raw(&self) -> Diagnostics { let mut all = Diagnostics::new(); all.extend(self.parse_diagnostics.clone()); all.extend(self.alt_kind_diagnostics.clone()); @@ -199,6 +202,14 @@ impl<'a> Query<'a> { all } + /// All diagnostics combined from all passes. + /// + /// Returns diagnostics with cascading errors suppressed. + /// For raw access, use [`diagnostics_raw`](Self::diagnostics_raw). + pub fn diagnostics(&self) -> Diagnostics { + self.diagnostics_raw() + } + /// Query is valid if there are no error-severity diagnostics (warnings are allowed). pub fn is_valid(&self) -> bool { !self.parse_diagnostics.has_errors() diff --git a/crates/plotnik-lib/src/query/recursion.rs b/crates/plotnik-lib/src/query/recursion.rs index 780ea9db..51386780 100644 --- a/crates/plotnik-lib/src/query/recursion.rs +++ b/crates/plotnik-lib/src/query/recursion.rs @@ -7,6 +7,7 @@ use indexmap::{IndexMap, IndexSet}; use rowan::TextRange; use super::Query; +use crate::diagnostics::DiagnosticKind; use crate::parser::{Def, Expr, SyntaxKind}; impl Query<'_> { @@ -207,13 +208,13 @@ impl Query<'_> { .map(|(r, _)| *r) .unwrap_or_else(|| TextRange::empty(0.into())); - let mut builder = self.recursion_diagnostics.error( - format!( + let mut builder = self + .recursion_diagnostics + .report(DiagnosticKind::RecursionNoEscape, range) + .message(format!( "recursive pattern can never match: cycle {} has no escape path", cycle_str - ), - range, - ); + )); for (rel_range, rel_msg) in related { builder = builder.related_to(rel_msg, rel_range); diff --git a/crates/plotnik-lib/src/query/shapes.rs b/crates/plotnik-lib/src/query/shapes.rs index 8916d131..3cbf8de8 100644 --- a/crates/plotnik-lib/src/query/shapes.rs +++ b/crates/plotnik-lib/src/query/shapes.rs @@ -11,6 +11,7 @@ use super::Query; use super::invariants::{ ensure_capture_has_inner, ensure_quantifier_has_inner, ensure_ref_has_name, }; +use crate::diagnostics::DiagnosticKind; use crate::parser::{Expr, FieldExpr, Ref, SeqExpr, SyntaxNode}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -116,13 +117,11 @@ impl Query<'_> { .unwrap_or_else(|| "field".to_string()); self.shapes_diagnostics - .error( - format!( - "field `{}` value must match a single node, not a sequence", - field_name - ), - value.text_range(), - ) + .report(DiagnosticKind::FieldSequenceValue, value.text_range()) + .message(format!( + "field `{}` value must match a single node, not a sequence", + field_name + )) .emit(); } diff --git a/crates/plotnik-lib/src/query/symbol_table.rs b/crates/plotnik-lib/src/query/symbol_table.rs index e1620e59..63abc0b0 100644 --- a/crates/plotnik-lib/src/query/symbol_table.rs +++ b/crates/plotnik-lib/src/query/symbol_table.rs @@ -6,6 +6,7 @@ use indexmap::IndexMap; +use crate::diagnostics::DiagnosticKind; use crate::parser::{Expr, Ref, ast}; use super::Query; @@ -25,7 +26,8 @@ impl<'a> Query<'a> { if self.symbol_table.contains_key(name) { self.resolve_diagnostics - .error(format!("duplicate definition: `{}`", name), range) + .report(DiagnosticKind::DuplicateDefinition, range) + .message(format!("duplicate definition: `{}`", name)) .emit(); continue; } @@ -101,10 +103,8 @@ impl<'a> Query<'a> { } self.resolve_diagnostics - .error( - format!("undefined reference: `{}`", name), - name_token.text_range(), - ) + .report(DiagnosticKind::UndefinedReference, name_token.text_range()) + .message(format!("undefined reference: `{}`", name)) .emit(); } } From e2282760062969c18a2b7baf6101fe0a29975b1a Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 6 Dec 2025 09:36:16 -0300 Subject: [PATCH 03/15] Refactor Diagnostics printer to simplify implementation --- crates/plotnik-lib/src/diagnostics/mod.rs | 76 ++----------------- crates/plotnik-lib/src/diagnostics/printer.rs | 45 +---------- 2 files changed, 10 insertions(+), 111 deletions(-) diff --git a/crates/plotnik-lib/src/diagnostics/mod.rs b/crates/plotnik-lib/src/diagnostics/mod.rs index aa8644fa..d344aebc 100644 --- a/crates/plotnik-lib/src/diagnostics/mod.rs +++ b/crates/plotnik-lib/src/diagnostics/mod.rs @@ -83,7 +83,7 @@ impl Diagnostics { /// /// Suppression rule: when a higher-priority diagnostic's span contains /// a lower-priority diagnostic's span, the lower-priority one is suppressed. - pub(crate) fn filtered(&self) -> Vec<&DiagnosticMessage> { + pub(crate) fn filtered(&self) -> Vec { if self.messages.is_empty() { return Vec::new(); } @@ -108,7 +108,7 @@ impl Diagnostics { .iter() .enumerate() .filter(|(i, _)| !suppressed[*i]) - .map(|(_, m)| m) + .map(|(_, m)| m.clone()) .collect() } @@ -118,16 +118,13 @@ impl Diagnostics { &self.messages } - pub fn printer<'a>(&'a self, source: &'a str) -> DiagnosticsPrinter<'a> { - DiagnosticsPrinter::new(&self.messages, source) + pub fn printer<'a>(&self, source: &'a str) -> DiagnosticsPrinter<'a> { + DiagnosticsPrinter::new(self.messages.clone(), source) } - /// Printer that uses filtered diagnostics. - pub fn filtered_printer<'a>(&'a self, source: &'a str) -> FilteredDiagnosticsPrinter<'a> { - FilteredDiagnosticsPrinter { - diagnostics: self, - source, - } + /// Printer that uses filtered diagnostics (cascading errors suppressed). + pub fn filtered_printer<'a>(&self, source: &'a str) -> DiagnosticsPrinter<'a> { + DiagnosticsPrinter::new(self.filtered(), source) } pub fn render(&self, source: &str) -> String { @@ -151,65 +148,6 @@ impl Diagnostics { } } -/// Printer wrapper that uses filtered diagnostics. -pub struct FilteredDiagnosticsPrinter<'a> { - diagnostics: &'a Diagnostics, - source: &'a str, -} - -impl<'a> FilteredDiagnosticsPrinter<'a> { - pub fn path(self, path: &'a str) -> FilteredDiagnosticsPrinterWithPath<'a> { - FilteredDiagnosticsPrinterWithPath { - diagnostics: self.diagnostics, - source: self.source, - path: Some(path), - colored: false, - } - } - - pub fn colored(self, colored: bool) -> FilteredDiagnosticsPrinterWithPath<'a> { - FilteredDiagnosticsPrinterWithPath { - diagnostics: self.diagnostics, - source: self.source, - path: None, - colored, - } - } - - pub fn render(&self) -> String { - let filtered = self.diagnostics.filtered(); - DiagnosticsPrinter::from_refs(&filtered, self.source).render() - } -} - -pub struct FilteredDiagnosticsPrinterWithPath<'a> { - diagnostics: &'a Diagnostics, - source: &'a str, - path: Option<&'a str>, - colored: bool, -} - -impl<'a> FilteredDiagnosticsPrinterWithPath<'a> { - pub fn path(mut self, path: &'a str) -> Self { - self.path = Some(path); - self - } - - pub fn colored(mut self, colored: bool) -> Self { - self.colored = colored; - self - } - - pub fn render(&self) -> String { - let filtered = self.diagnostics.filtered(); - let mut printer = DiagnosticsPrinter::from_refs(&filtered, self.source); - if let Some(p) = self.path { - printer = printer.path(p); - } - printer.colored(self.colored).render() - } -} - impl<'a> DiagnosticBuilder<'a> { /// Override the default message for this diagnostic kind. pub fn message(mut self, msg: impl Into) -> Self { diff --git a/crates/plotnik-lib/src/diagnostics/printer.rs b/crates/plotnik-lib/src/diagnostics/printer.rs index 3ef5608f..314c189a 100644 --- a/crates/plotnik-lib/src/diagnostics/printer.rs +++ b/crates/plotnik-lib/src/diagnostics/printer.rs @@ -8,55 +8,16 @@ use rowan::TextRange; use super::message::{DiagnosticMessage, Severity}; pub struct DiagnosticsPrinter<'a> { - diagnostics: DiagnosticsSlice<'a>, + diagnostics: Vec, source: &'a str, path: Option<&'a str>, colored: bool, } -enum DiagnosticsSlice<'a> { - Borrowed(&'a [DiagnosticMessage]), - Refs(Vec<&'a DiagnosticMessage>), -} - -impl<'a> DiagnosticsSlice<'a> { - fn iter(&self) -> impl Iterator { - match self { - DiagnosticsSlice::Borrowed(slice) => DiagnosticsIter::Borrowed(slice.iter()), - DiagnosticsSlice::Refs(vec) => DiagnosticsIter::Refs(vec.iter()), - } - } -} - -enum DiagnosticsIter<'a, 'b> { - Borrowed(std::slice::Iter<'a, DiagnosticMessage>), - Refs(std::slice::Iter<'b, &'a DiagnosticMessage>), -} - -impl<'a, 'b> Iterator for DiagnosticsIter<'a, 'b> { - type Item = &'a DiagnosticMessage; - - fn next(&mut self) -> Option { - match self { - DiagnosticsIter::Borrowed(iter) => iter.next(), - DiagnosticsIter::Refs(iter) => iter.next().copied(), - } - } -} - impl<'a> DiagnosticsPrinter<'a> { - pub(crate) fn new(diagnostics: &'a [DiagnosticMessage], source: &'a str) -> Self { - Self { - diagnostics: DiagnosticsSlice::Borrowed(diagnostics), - source, - path: None, - colored: false, - } - } - - pub(crate) fn from_refs(diagnostics: &[&'a DiagnosticMessage], source: &'a str) -> Self { + pub(crate) fn new(diagnostics: Vec, source: &'a str) -> Self { Self { - diagnostics: DiagnosticsSlice::Refs(diagnostics.to_vec()), + diagnostics, source, path: None, colored: false, From adc6cb9e0a0f65368d257781c1c0dffb8cec169f Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 6 Dec 2025 09:37:01 -0300 Subject: [PATCH 04/15] Remove commented section headers from DiagnosticKind --- crates/plotnik-lib/src/diagnostics/message.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/plotnik-lib/src/diagnostics/message.rs b/crates/plotnik-lib/src/diagnostics/message.rs index 7e9be201..b649f87b 100644 --- a/crates/plotnik-lib/src/diagnostics/message.rs +++ b/crates/plotnik-lib/src/diagnostics/message.rs @@ -14,13 +14,11 @@ use rowan::TextRange; /// - Structural observations are often consequences of earlier errors #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum DiagnosticKind { - // === Unclosed delimiters (highest priority) === // These cause cascading errors throughout the rest of the file UnclosedTree, UnclosedSequence, UnclosedAlternation, - // === Expected token errors === // User omitted something required - root cause errors ExpectedExpression, ExpectedTypeName, @@ -28,7 +26,6 @@ pub enum DiagnosticKind { ExpectedFieldName, ExpectedSubtype, - // === Invalid token/syntax usage === // User wrote something that doesn't belong EmptyTree, BareIdentifier, @@ -44,7 +41,6 @@ pub enum DiagnosticKind { CaptureWithoutTarget, LowercaseBranchLabel, - // === Naming validation === // Convention violations - fixable with suggestions CaptureNameHasDots, CaptureNameHasHyphens, @@ -57,7 +53,6 @@ pub enum DiagnosticKind { FieldNameUppercase, TypeNameInvalidChars, - // === Semantic errors === // Valid syntax, invalid semantics DuplicateDefinition, UndefinedReference, @@ -65,7 +60,6 @@ pub enum DiagnosticKind { RecursionNoEscape, FieldSequenceValue, - // === Structural observations (lowest priority) === // Often consequences of earlier errors UnnamedDefNotLast, } From 72f6dfbad0ecb71bc052d1db2138dd7ba9bb562e Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 6 Dec 2025 09:59:42 -0300 Subject: [PATCH 05/15] Refactor error handling methods in parser core --- crates/plotnik-lib/src/parser/core.rs | 22 +++++--- crates/plotnik-lib/src/parser/grammar.rs | 66 ++++++++---------------- 2 files changed, 37 insertions(+), 51 deletions(-) diff --git a/crates/plotnik-lib/src/parser/core.rs b/crates/plotnik-lib/src/parser/core.rs index 4bc2a296..d16a1dda 100644 --- a/crates/plotnik-lib/src/parser/core.rs +++ b/crates/plotnik-lib/src/parser/core.rs @@ -254,14 +254,20 @@ impl<'src> Parser<'src> { if self.eat(kind) { return true; } - self.error( + self.error_msg( DiagnosticKind::UnexpectedToken, format!("expected {}", what), ); false } - pub(super) fn error(&mut self, kind: DiagnosticKind, message: impl Into) { + /// Emit diagnostic with default message for the kind. + pub(super) fn error(&mut self, kind: DiagnosticKind) { + self.error_msg(kind, kind.default_message()); + } + + /// Emit diagnostic with custom message. + pub(super) fn error_msg(&mut self, kind: DiagnosticKind, message: impl Into) { let range = self.current_span(); let pos = range.start(); if self.last_diagnostic_pos == Some(pos) { @@ -271,8 +277,12 @@ impl<'src> Parser<'src> { self.diagnostics.report(kind, range).message(message).emit(); } - pub(super) fn error_and_bump(&mut self, kind: DiagnosticKind, message: &str) { - self.error(kind, message); + pub(super) fn error_and_bump(&mut self, kind: DiagnosticKind) { + self.error_and_bump_msg(kind, kind.default_message()); + } + + pub(super) fn error_and_bump_msg(&mut self, kind: DiagnosticKind, message: &str) { + self.error_msg(kind, message); if !self.eof() { self.start_node(SyntaxKind::Error); self.bump(); @@ -288,12 +298,12 @@ impl<'src> Parser<'src> { recovery: TokenSet, ) { if self.at_set(recovery) || self.should_stop() { - self.error(kind, message); + self.error_msg(kind, message); return; } self.start_node(SyntaxKind::Error); - self.error(kind, message); + self.error_msg(kind, message); while !self.at_set(recovery) && !self.should_stop() { self.bump(); } diff --git a/crates/plotnik-lib/src/parser/grammar.rs b/crates/plotnik-lib/src/parser/grammar.rs index f979c613..583432e2 100644 --- a/crates/plotnik-lib/src/parser/grammar.rs +++ b/crates/plotnik-lib/src/parser/grammar.rs @@ -73,7 +73,7 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr(); } else { - self.error( + self.error_msg( DiagnosticKind::ExpectedExpression, "expected expression after '=' in named definition", ); @@ -90,19 +90,13 @@ impl Parser<'_> { self.parse_expr(); true } else if kind == SyntaxKind::At { - self.error_and_bump( - DiagnosticKind::CaptureWithoutTarget, - "capture '@' must follow an expression to capture", - ); + self.error_and_bump(DiagnosticKind::CaptureWithoutTarget); false } else if kind == SyntaxKind::Predicate { - self.error_and_bump( - DiagnosticKind::UnsupportedPredicate, - "tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported", - ); + self.error_and_bump(DiagnosticKind::UnsupportedPredicate); false } else { - self.error_and_bump( + self.error_and_bump_msg( DiagnosticKind::UnexpectedToken, "unexpected token; expected an expression like (node), [choice], {sequence}, \"literal\", or _", ); @@ -143,13 +137,10 @@ impl Parser<'_> { SyntaxKind::Negation => self.parse_negated_field(), SyntaxKind::Id => self.parse_tree_or_field(), SyntaxKind::KwError | SyntaxKind::KwMissing => { - self.error_and_bump( - DiagnosticKind::ErrorMissingOutsideParens, - "ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...)", - ); + self.error_and_bump(DiagnosticKind::ErrorMissingOutsideParens); } _ => { - self.error_and_bump( + self.error_and_bump_msg( DiagnosticKind::UnexpectedToken, "unexpected token; expected an expression", ); @@ -177,10 +168,7 @@ impl Parser<'_> { match self.peek() { SyntaxKind::ParenClose => { self.start_node_at(checkpoint, SyntaxKind::Tree); - self.error( - DiagnosticKind::EmptyTree, - "empty tree expression - expected node type or children", - ); + self.error(DiagnosticKind::EmptyTree); self.pop_delimiter(); self.bump(); // consume ')' self.finish_node(); @@ -205,10 +193,7 @@ impl Parser<'_> { if self.peek() == SyntaxKind::Slash { if is_ref { self.start_node_at(checkpoint, SyntaxKind::Tree); - self.error( - DiagnosticKind::InvalidSupertypeSyntax, - "references cannot use supertype syntax (/)", - ); + self.error(DiagnosticKind::InvalidSupertypeSyntax); is_ref = false; } self.bump(); @@ -220,7 +205,7 @@ impl Parser<'_> { self.bump_string_tokens(); } _ => { - self.error( + self.error_msg( DiagnosticKind::ExpectedSubtype, "expected subtype after '/' (e.g., expression/binary_expression)", ); @@ -232,10 +217,7 @@ impl Parser<'_> { self.start_node_at(checkpoint, SyntaxKind::Tree); self.bump(); if self.peek() != SyntaxKind::ParenClose { - self.error( - DiagnosticKind::ErrorTakesNoArguments, - "(ERROR) takes no arguments", - ); + self.error(DiagnosticKind::ErrorTakesNoArguments); self.parse_children(SyntaxKind::ParenClose, TREE_RECOVERY); } self.pop_delimiter(); @@ -339,16 +321,13 @@ impl Parser<'_> { continue; } if kind == SyntaxKind::Predicate { - self.error_and_bump( - DiagnosticKind::UnsupportedPredicate, - "tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported", - ); + self.error_and_bump(DiagnosticKind::UnsupportedPredicate); continue; } if recovery.contains(kind) { break; } - self.error_and_bump( + self.error_and_bump_msg( DiagnosticKind::UnexpectedToken, "unexpected token; expected a child expression or closing delimiter", ); @@ -419,7 +398,7 @@ impl Parser<'_> { if ALT_RECOVERY.contains(kind) { break; } - self.error_and_bump( + self.error_and_bump_msg( DiagnosticKind::UnexpectedToken, "unexpected token; expected a child expression or closing delimiter", ); @@ -441,7 +420,7 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr(); } else { - self.error( + self.error_msg( DiagnosticKind::ExpectedExpression, "expected expression after branch label", ); @@ -473,7 +452,7 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr(); } else { - self.error( + self.error_msg( DiagnosticKind::ExpectedExpression, "expected expression after branch label", ); @@ -528,10 +507,7 @@ impl Parser<'_> { self.bump(); // consume At if self.peek() != SyntaxKind::Id { - self.error( - DiagnosticKind::ExpectedCaptureName, - "expected capture name after '@'", - ); + self.error(DiagnosticKind::ExpectedCaptureName); return; } @@ -561,7 +537,7 @@ impl Parser<'_> { self.bump(); self.validate_type_name(text, span); } else { - self.error( + self.error_msg( DiagnosticKind::ExpectedTypeName, "expected type name after '::' (e.g., ::MyType or ::string)", ); @@ -610,7 +586,7 @@ impl Parser<'_> { self.expect(SyntaxKind::Negation, "'!' for negated field"); if self.peek() != SyntaxKind::Id { - self.error( + self.error_msg( DiagnosticKind::ExpectedFieldName, "expected field name after '!' (e.g., !value)", ); @@ -634,7 +610,7 @@ impl Parser<'_> { self.parse_field_equals_typo(); } else { // Bare identifiers are not valid expressions; trees require parentheses - self.error_and_bump( + self.error_and_bump_msg( DiagnosticKind::BareIdentifier, "bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier)", ); @@ -660,7 +636,7 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr_no_suffix(); } else { - self.error( + self.error_msg( DiagnosticKind::ExpectedExpression, "expected expression after field name", ); @@ -688,7 +664,7 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr(); } else { - self.error( + self.error_msg( DiagnosticKind::ExpectedExpression, "expected expression after field name", ); From f8aefa9fae48e783ce5cb491c567e7694e1ce0e1 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 6 Dec 2025 10:29:21 -0300 Subject: [PATCH 06/15] Refactor diagnostic messages to use template system --- crates/plotnik-lib/src/diagnostics/message.rs | 137 ++++++--- crates/plotnik-lib/src/diagnostics/mod.rs | 5 +- crates/plotnik-lib/src/diagnostics/tests.rs | 75 +++-- crates/plotnik-lib/src/parser/core.rs | 4 +- crates/plotnik-lib/src/parser/grammar.rs | 7 +- .../parser/tests/recovery/coverage_tests.rs | 16 +- .../parser/tests/recovery/incomplete_tests.rs | 104 +++---- .../parser/tests/recovery/unclosed_tests.rs | 56 ++-- .../parser/tests/recovery/unexpected_tests.rs | 184 ++++++------ .../parser/tests/recovery/validation_tests.rs | 280 +++++++++--------- crates/plotnik-lib/src/query/alt_kinds.rs | 1 - crates/plotnik-lib/src/query/recursion.rs | 5 +- crates/plotnik-lib/src/query/shapes.rs | 5 +- crates/plotnik-lib/src/query/shapes_tests.rs | 8 +- crates/plotnik-lib/src/query/symbol_table.rs | 4 +- 15 files changed, 482 insertions(+), 409 deletions(-) diff --git a/crates/plotnik-lib/src/diagnostics/message.rs b/crates/plotnik-lib/src/diagnostics/message.rs index b649f87b..aedf03e8 100644 --- a/crates/plotnik-lib/src/diagnostics/message.rs +++ b/crates/plotnik-lib/src/diagnostics/message.rs @@ -78,67 +78,122 @@ impl DiagnosticKind { self < other } - /// Default message for this diagnostic kind. - /// - /// Provides a sensible fallback; callers can override with context-specific messages. - pub fn default_message(&self) -> &'static str { + /// Base message for this diagnostic kind, used when no custom message is provided. + pub fn fallback_message(&self) -> &'static str { match self { // Unclosed delimiters - Self::UnclosedTree => "unclosed tree; expected ')'", - Self::UnclosedSequence => "unclosed sequence; expected '}'", - Self::UnclosedAlternation => "unclosed alternation; expected ']'", + Self::UnclosedTree => "unclosed tree", + Self::UnclosedSequence => "unclosed sequence", + Self::UnclosedAlternation => "unclosed alternation", // Expected token errors Self::ExpectedExpression => "expected expression", - Self::ExpectedTypeName => "expected type name after '::'", - Self::ExpectedCaptureName => "expected capture name after '@'", + Self::ExpectedTypeName => "expected type name", + Self::ExpectedCaptureName => "expected capture name", Self::ExpectedFieldName => "expected field name", - Self::ExpectedSubtype => "expected subtype after '/'", + Self::ExpectedSubtype => "expected subtype", // Invalid token/syntax usage - Self::EmptyTree => "empty tree expression - expected node type or children", - Self::BareIdentifier => { - "bare identifier not allowed; nodes must be enclosed in parentheses" - } - Self::InvalidSeparator => "invalid separator; plotnik uses whitespace for separation", - Self::InvalidFieldEquals => "'=' is not valid for field constraints; use ':'", - Self::InvalidSupertypeSyntax => "references cannot use supertype syntax (/)", + Self::EmptyTree => "empty tree expression", + Self::BareIdentifier => "bare identifier not allowed", + Self::InvalidSeparator => "invalid separator", + Self::InvalidFieldEquals => "invalid field syntax", + Self::InvalidSupertypeSyntax => "invalid supertype syntax", Self::InvalidTypeAnnotationSyntax => "invalid type annotation syntax", Self::ErrorTakesNoArguments => "(ERROR) takes no arguments", Self::RefCannotHaveChildren => "reference cannot contain children", - Self::ErrorMissingOutsideParens => { - "ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...)" - } - Self::UnsupportedPredicate => { - "tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported" - } + Self::ErrorMissingOutsideParens => "ERROR/MISSING outside parentheses", + Self::UnsupportedPredicate => "unsupported predicate", Self::UnexpectedToken => "unexpected token", - Self::CaptureWithoutTarget => "capture '@' must follow an expression to capture", - Self::LowercaseBranchLabel => { - "tagged alternation labels must be Capitalized (they map to enum variants)" - } + Self::CaptureWithoutTarget => "capture without target", + Self::LowercaseBranchLabel => "lowercase branch label", // Naming validation - Self::CaptureNameHasDots => "capture names cannot contain dots", - Self::CaptureNameHasHyphens => "capture names cannot contain hyphens", - Self::CaptureNameUppercase => "capture names must start with lowercase", - Self::DefNameLowercase => "definition names must start with uppercase", - Self::DefNameHasSeparators => "definition names cannot contain separators", - Self::BranchLabelHasSeparators => "branch labels cannot contain separators", - Self::FieldNameHasDots => "field names cannot contain dots", - Self::FieldNameHasHyphens => "field names cannot contain hyphens", - Self::FieldNameUppercase => "field names must start with lowercase", - Self::TypeNameInvalidChars => "type names cannot contain dots or hyphens", + Self::CaptureNameHasDots => "capture name contains dots", + Self::CaptureNameHasHyphens => "capture name contains hyphens", + Self::CaptureNameUppercase => "capture name starts with uppercase", + Self::DefNameLowercase => "definition name starts with lowercase", + Self::DefNameHasSeparators => "definition name contains separators", + Self::BranchLabelHasSeparators => "branch label contains separators", + Self::FieldNameHasDots => "field name contains dots", + Self::FieldNameHasHyphens => "field name contains hyphens", + Self::FieldNameUppercase => "field name starts with uppercase", + Self::TypeNameInvalidChars => "type name contains invalid characters", // Semantic errors Self::DuplicateDefinition => "duplicate definition", Self::UndefinedReference => "undefined reference", Self::MixedAltBranches => "mixed tagged and untagged branches in alternation", - Self::RecursionNoEscape => "recursive pattern can never match: no escape path", - Self::FieldSequenceValue => "field value must match a single node, not a sequence", + Self::RecursionNoEscape => "recursive pattern can never match", + Self::FieldSequenceValue => "field value must be a single node", + + // Structural observations + Self::UnnamedDefNotLast => "unnamed definition must be last", + } + } + + /// Template for custom messages. Contains `{}` placeholder for caller-provided detail. + pub fn custom_message(&self) -> &'static str { + match self { + // Unclosed delimiters + Self::UnclosedTree => "unclosed tree: {}", + Self::UnclosedSequence => "unclosed sequence: {}", + Self::UnclosedAlternation => "unclosed alternation: {}", + + // Expected token errors + Self::ExpectedExpression => "expected expression: {}", + Self::ExpectedTypeName => "expected type name: {}", + Self::ExpectedCaptureName => "expected capture name: {}", + Self::ExpectedFieldName => "expected field name: {}", + Self::ExpectedSubtype => "expected subtype: {}", + + // Invalid token/syntax usage + Self::EmptyTree => "empty tree expression: {}", + Self::BareIdentifier => "bare identifier not allowed: {}", + Self::InvalidSeparator => "invalid separator: {}", + Self::InvalidFieldEquals => "invalid field syntax: {}", + Self::InvalidSupertypeSyntax => "invalid supertype syntax: {}", + Self::InvalidTypeAnnotationSyntax => "invalid type annotation: {}", + Self::ErrorTakesNoArguments => "(ERROR) takes no arguments: {}", + Self::RefCannotHaveChildren => "reference `{}` cannot contain children", + Self::ErrorMissingOutsideParens => "ERROR/MISSING outside parentheses: {}", + Self::UnsupportedPredicate => "unsupported predicate: {}", + Self::UnexpectedToken => "unexpected token: {}", + Self::CaptureWithoutTarget => "capture without target: {}", + Self::LowercaseBranchLabel => "lowercase branch label: {}", + + // Naming validation + Self::CaptureNameHasDots => "capture name contains dots: {}", + Self::CaptureNameHasHyphens => "capture name contains hyphens: {}", + Self::CaptureNameUppercase => "capture name starts with uppercase: {}", + Self::DefNameLowercase => "definition name starts with lowercase: {}", + Self::DefNameHasSeparators => "definition name contains separators: {}", + Self::BranchLabelHasSeparators => "branch label contains separators: {}", + Self::FieldNameHasDots => "field name contains dots: {}", + Self::FieldNameHasHyphens => "field name contains hyphens: {}", + Self::FieldNameUppercase => "field name starts with uppercase: {}", + Self::TypeNameInvalidChars => "type name contains invalid characters: {}", + + // Semantic errors + Self::DuplicateDefinition => "duplicate definition: `{}`", + Self::UndefinedReference => "undefined reference: `{}`", + Self::MixedAltBranches => "mixed alternation: {}", + Self::RecursionNoEscape => "recursive pattern can never match: {}", + Self::FieldSequenceValue => "field `{}` value must be a single node", // Structural observations - Self::UnnamedDefNotLast => "unnamed definition must be last in file", + Self::UnnamedDefNotLast => "unnamed definition must be last: {}", + } + } + + /// Render the final message. + /// + /// - `None` → returns `fallback_message()` + /// - `Some(detail)` → returns `custom_message()` with `{}` replaced by detail + pub fn message(&self, msg: Option<&str>) -> String { + match msg { + None => self.fallback_message().to_string(), + Some(detail) => self.custom_message().replace("{}", detail), } } } @@ -210,7 +265,7 @@ impl DiagnosticMessage { } pub(crate) fn with_default_message(kind: DiagnosticKind, range: TextRange) -> Self { - Self::new(kind, range, kind.default_message()) + Self::new(kind, range, kind.fallback_message()) } pub(crate) fn severity(&self) -> Severity { diff --git a/crates/plotnik-lib/src/diagnostics/mod.rs b/crates/plotnik-lib/src/diagnostics/mod.rs index d344aebc..afdb358e 100644 --- a/crates/plotnik-lib/src/diagnostics/mod.rs +++ b/crates/plotnik-lib/src/diagnostics/mod.rs @@ -149,9 +149,10 @@ impl Diagnostics { } impl<'a> DiagnosticBuilder<'a> { - /// Override the default message for this diagnostic kind. + /// Provide custom detail for this diagnostic, rendered using the kind's template. pub fn message(mut self, msg: impl Into) -> Self { - self.message.message = msg.into(); + let detail = msg.into(); + self.message.message = self.message.kind.message(Some(&detail)); self } diff --git a/crates/plotnik-lib/src/diagnostics/tests.rs b/crates/plotnik-lib/src/diagnostics/tests.rs index 5d1f6ebc..d2e3d5a7 100644 --- a/crates/plotnik-lib/src/diagnostics/tests.rs +++ b/crates/plotnik-lib/src/diagnostics/tests.rs @@ -63,12 +63,12 @@ fn builder_with_related() { assert_eq!(diagnostics.len(), 1); let result = diagnostics.printer("hello world!").render(); insta::assert_snapshot!(result, @r" - error: primary + error: unclosed tree: primary | 1 | hello world! | ^^^^^ ---- related info | | - | primary + | unclosed tree: primary "); } @@ -86,10 +86,10 @@ fn builder_with_fix() { let result = diagnostics.printer("hello world").render(); insta::assert_snapshot!(result, @r" - error: fixable + error: invalid field syntax: fixable | 1 | hello world - | ^^^^^ fixable + | ^^^^^ invalid field syntax: fixable | help: apply this fix | @@ -115,13 +115,13 @@ fn builder_with_all_options() { let result = diagnostics.printer("hello world stuff!").render(); insta::assert_snapshot!(result, @r" - error: main error + error: unclosed tree: main error | 1 | hello world stuff! | ^^^^^ ----- ----- and here | | | | | see also - | main error + | unclosed tree: main error | help: try this | @@ -167,11 +167,11 @@ fn printer_with_path() { let result = diagnostics.printer("hello world").path("test.pql").render(); insta::assert_snapshot!(result, @r" - error: test error + error: undefined reference: `test error` --> test.pql:1:1 | 1 | hello world - | ^^^^^ test error + | ^^^^^ undefined reference: `test error` "); } @@ -188,10 +188,10 @@ fn printer_zero_width_span() { let result = diagnostics.printer("hello").render(); insta::assert_snapshot!(result, @r" - error: zero width error + error: expected expression: zero width error | 1 | hello - | ^ zero width error + | ^ expected expression: zero width error "); } @@ -209,12 +209,12 @@ fn printer_related_zero_width() { let result = diagnostics.printer("hello world!").render(); insta::assert_snapshot!(result, @r" - error: primary + error: unclosed tree: primary | 1 | hello world! | ^^^^^ - zero width related | | - | primary + | unclosed tree: primary "); } @@ -238,14 +238,14 @@ fn printer_multiple_diagnostics() { let result = diagnostics.printer("hello world!").render(); insta::assert_snapshot!(result, @r" - error: first error + error: unclosed tree: first error | 1 | hello world! - | ^^^^^ first error - error: second error + | ^^^^^ unclosed tree: first error + error: undefined reference: `second error` | 1 | hello world! - | ^^^^ second error + | ^^^^ undefined reference: `second error` "); } @@ -295,25 +295,52 @@ fn diagnostic_kind_suppression_order() { } #[test] -fn diagnostic_kind_default_messages() { +fn diagnostic_kind_fallback_messages() { assert_eq!( - DiagnosticKind::UnclosedTree.default_message(), - "unclosed tree; expected ')'" + DiagnosticKind::UnclosedTree.fallback_message(), + "unclosed tree" ); assert_eq!( - DiagnosticKind::UnclosedSequence.default_message(), - "unclosed sequence; expected '}'" + DiagnosticKind::UnclosedSequence.fallback_message(), + "unclosed sequence" ); assert_eq!( - DiagnosticKind::UnclosedAlternation.default_message(), - "unclosed alternation; expected ']'" + DiagnosticKind::UnclosedAlternation.fallback_message(), + "unclosed alternation" ); assert_eq!( - DiagnosticKind::ExpectedExpression.default_message(), + DiagnosticKind::ExpectedExpression.fallback_message(), "expected expression" ); } +#[test] +fn diagnostic_kind_custom_messages() { + assert_eq!( + DiagnosticKind::UnclosedTree.custom_message(), + "unclosed tree: {}" + ); + assert_eq!( + DiagnosticKind::UndefinedReference.custom_message(), + "undefined reference: `{}`" + ); +} + +#[test] +fn diagnostic_kind_message_rendering() { + // No custom message → fallback + assert_eq!(DiagnosticKind::UnclosedTree.message(None), "unclosed tree"); + // With custom message → template applied + assert_eq!( + DiagnosticKind::UnclosedTree.message(Some("expected ')'")), + "unclosed tree: expected ')'" + ); + assert_eq!( + DiagnosticKind::UndefinedReference.message(Some("Foo")), + "undefined reference: `Foo`" + ); +} + // === Filtering/suppression tests === #[test] diff --git a/crates/plotnik-lib/src/parser/core.rs b/crates/plotnik-lib/src/parser/core.rs index d16a1dda..c7f4b71d 100644 --- a/crates/plotnik-lib/src/parser/core.rs +++ b/crates/plotnik-lib/src/parser/core.rs @@ -263,7 +263,7 @@ impl<'src> Parser<'src> { /// Emit diagnostic with default message for the kind. pub(super) fn error(&mut self, kind: DiagnosticKind) { - self.error_msg(kind, kind.default_message()); + self.error_msg(kind, kind.fallback_message()); } /// Emit diagnostic with custom message. @@ -278,7 +278,7 @@ impl<'src> Parser<'src> { } pub(super) fn error_and_bump(&mut self, kind: DiagnosticKind) { - self.error_and_bump_msg(kind, kind.default_message()); + self.error_and_bump_msg(kind, kind.fallback_message()); } pub(super) fn error_and_bump_msg(&mut self, kind: DiagnosticKind, message: &str) { diff --git a/crates/plotnik-lib/src/parser/grammar.rs b/crates/plotnik-lib/src/parser/grammar.rs index 583432e2..23eff67b 100644 --- a/crates/plotnik-lib/src/parser/grammar.rs +++ b/crates/plotnik-lib/src/parser/grammar.rs @@ -45,10 +45,7 @@ impl Parser<'_> { let def_text = &self.source[usize::from(span.start())..usize::from(span.end())]; self.diagnostics .report(DiagnosticKind::UnnamedDefNotLast, *span) - .message(format!( - "unnamed definition must be last in file; add a name: `Name = {}`", - def_text.trim() - )) + .message(format!("add a name: `Name = {}`", def_text.trim())) .emit(); } } @@ -262,7 +259,7 @@ impl Parser<'_> { if let Some(name) = &ref_name { self.diagnostics .report(DiagnosticKind::RefCannotHaveChildren, children_span) - .message(format!("reference `{}` cannot contain children", name)) + .message(name) .emit(); } } else if is_ref { 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 29ea15b0..8ed5cf2f 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs @@ -131,14 +131,14 @@ fn named_def_missing_equals_with_garbage() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed: 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 _ + | ^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: unexpected token: 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 _ + | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ "#); } @@ -152,14 +152,14 @@ fn named_def_missing_equals_recovers_to_next_def() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed: 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 _ + | ^^^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | Broken ^^^ - | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ "#); } 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 ea5c05fe..964d8bd0 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs @@ -10,10 +10,10 @@ fn missing_capture_name() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected capture name after '@' + error: expected capture name: expected capture name | 1 | (identifier) @ - | ^ expected capture name after '@' + | ^ expected capture name: expected capture name "); } @@ -26,10 +26,10 @@ fn missing_field_value() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression after field name + error: expected expression: expected expression after field name | 1 | (call name:) - | ^ expected expression after field name + | ^ expected expression: expected expression after field name "); } @@ -40,10 +40,10 @@ fn named_def_eof_after_equals() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression after '=' in named definition + error: expected expression: expected expression after '=' in named definition | 1 | Expr = - | ^ expected expression after '=' in named definition + | ^ expected expression: expected expression after '=' in named definition "); } @@ -56,10 +56,10 @@ fn missing_type_name() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected type name after '::' (e.g., ::MyType or ::string) + error: expected type name: expected type name after '::' (e.g., ::MyType or ::string) | 1 | (identifier) @name :: - | ^ expected type name after '::' (e.g., ::MyType or ::string) + | ^ expected type name: expected type name after '::' (e.g., ::MyType or ::string) "); } @@ -72,10 +72,10 @@ fn missing_negated_field_name() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected field name after '!' (e.g., !value) + error: expected field name: expected field name after '!' (e.g., !value) | 1 | (call !) - | ^ expected field name after '!' (e.g., !value) + | ^ expected field name: expected field name after '!' (e.g., !value) "); } @@ -88,10 +88,10 @@ fn missing_subtype() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected subtype after '/' (e.g., expression/binary_expression) + error: expected subtype: expected subtype after '/' (e.g., expression/binary_expression) | 1 | (expression/) - | ^ expected subtype after '/' (e.g., expression/binary_expression) + | ^ expected subtype: expected subtype after '/' (e.g., expression/binary_expression) "); } @@ -104,10 +104,10 @@ fn tagged_branch_missing_expression() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression after branch label + error: expected expression: expected expression after branch label | 1 | [Label:] - | ^ expected expression after branch label + | ^ expected expression: expected expression after branch label "); } @@ -118,10 +118,10 @@ fn type_annotation_missing_name_at_eof() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected type name after '::' (e.g., ::MyType or ::string) + error: expected type name: expected type name after '::' (e.g., ::MyType or ::string) | 1 | (a) @x :: - | ^ expected type name after '::' (e.g., ::MyType or ::string) + | ^ expected type name: expected type name after '::' (e.g., ::MyType or ::string) "); } @@ -132,10 +132,10 @@ fn type_annotation_missing_name_with_bracket() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected type name after '::' (e.g., ::MyType or ::string) + error: expected type name: expected type name after '::' (e.g., ::MyType or ::string) | 1 | [(a) @x :: ] - | ^ expected type name after '::' (e.g., ::MyType or ::string) + | ^ expected type name: expected type name after '::' (e.g., ::MyType or ::string) "); } @@ -148,20 +148,20 @@ fn type_annotation_invalid_token_after() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected type name after '::' (e.g., ::MyType or ::string) + error: expected type name: expected type name after '::' (e.g., ::MyType or ::string) | 1 | (identifier) @name :: ( - | ^ expected type name after '::' (e.g., ::MyType or ::string) - error: unclosed tree; expected ')' + | ^ expected type name: expected type name after '::' (e.g., ::MyType or ::string) + error: unclosed tree: unclosed tree; expected ')' | 1 | (identifier) @name :: ( - | -^ unclosed tree; expected ')' + | -^ unclosed tree: unclosed tree; expected ')' | | | tree started here - error: unnamed definition must be last in file; add a name: `Name = (identifier) @name ::` + error: unnamed definition must be last: add a name: `Name = (identifier) @name ::` | 1 | (identifier) @name :: ( - | ^^^^^^^^^^^^^^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (identifier) @name ::` + | ^^^^^^^^^^^^^^^^^^^^^ unnamed definition must be last: add a name: `Name = (identifier) @name ::` "); } @@ -174,10 +174,10 @@ fn field_value_is_garbage() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression after field name + error: expected expression: expected expression after field name | 1 | (call name: %%%) - | ^^^ expected expression after field name + | ^^^ expected expression: expected expression after field name "); } @@ -190,10 +190,10 @@ fn capture_with_invalid_char() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected capture name after '@' + error: expected capture name: expected capture name | 1 | (identifier) @123 - | ^^^ expected capture name after '@' + | ^^^ expected capture name: expected capture name "); } @@ -204,10 +204,10 @@ fn bare_capture_at_eof_triggers_sync() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture '@' must follow an expression to capture + error: capture without target: capture without target | 1 | @ - | ^ capture '@' must follow an expression to capture + | ^ capture without target: capture without target "); } @@ -220,14 +220,14 @@ fn bare_capture_at_root() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture '@' must follow an expression to capture + error: capture without target: capture without target | 1 | @name - | ^ capture '@' must follow an expression to capture - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^ capture without target: capture without target + error: bare identifier not allowed: 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) + | ^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) "); } @@ -240,14 +240,14 @@ fn capture_at_start_of_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token; expected a child expression or closing delimiter + error: unexpected token: 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) + | ^ unexpected token: unexpected token; expected a child expression or closing delimiter + error: bare identifier not allowed: 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) + | ^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) "); } @@ -260,18 +260,18 @@ fn mixed_valid_invalid_captures() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture '@' must follow an expression to capture + error: capture without target: capture without target | 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) + | ^ capture without target: capture without target + error: bare identifier not allowed: 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` + | ^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: unnamed definition must be last: add a name: `Name = (a) @ok` | 1 | (a) @ok @ @name - | ^^^^^^^ unnamed definition must be last in file; add a name: `Name = (a) @ok` + | ^^^^^^^ unnamed definition must be last: add a name: `Name = (a) @ok` "); } @@ -284,20 +284,20 @@ fn field_equals_typo_missing_value() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: '=' is not valid for field constraints + error: invalid field syntax: '=' is not valid for field constraints | 1 | (call name = ) - | ^ '=' is not valid for field constraints + | ^ invalid field syntax: '=' is not valid for field constraints | help: use ':' | 1 - (call name = ) 1 + (call name : ) | - error: expected expression after field name + error: expected expression: expected expression after field name | 1 | (call name = ) - | ^ expected expression after field name + | ^ expected expression: expected expression after field name "); } @@ -308,19 +308,19 @@ fn lowercase_branch_label_missing_expression() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: tagged alternation labels must be Capitalized (they map to enum variants) + error: lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) | 1 | [label:] - | ^^^^^ tagged alternation labels must be Capitalized (they map to enum variants) + | ^^^^^ lowercase branch 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 + error: expected expression: expected expression after branch label | 1 | [label:] - | ^ expected expression after branch label + | ^ expected expression: expected expression after branch label "); } diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs index 792e7ebb..c6f2dee8 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs @@ -10,10 +10,10 @@ fn missing_paren() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree; expected ')' + error: unclosed tree: unclosed tree; expected ')' | 1 | (identifier - | - ^ unclosed tree; expected ')' + | - ^ unclosed tree: unclosed tree; expected ')' | | | tree started here "); @@ -28,10 +28,10 @@ fn missing_bracket() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed alternation; expected ']' + error: unclosed alternation: unclosed alternation; expected ']' | 1 | [(identifier) (string) - | - ^ unclosed alternation; expected ']' + | - ^ unclosed alternation: unclosed alternation; expected ']' | | | alternation started here "); @@ -46,10 +46,10 @@ fn missing_brace() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed sequence; expected '}' + error: unclosed sequence: unclosed sequence; expected '}' | 1 | {(a) (b) - | - ^ unclosed sequence; expected '}' + | - ^ unclosed sequence: unclosed sequence; expected '}' | | | sequence started here "); @@ -64,10 +64,10 @@ fn nested_unclosed() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree; expected ')' + error: unclosed tree: unclosed tree; expected ')' | 1 | (a (b (c) - | - ^ unclosed tree; expected ')' + | - ^ unclosed tree: unclosed tree; expected ')' | | | tree started here "); @@ -82,10 +82,10 @@ fn deeply_nested_unclosed() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree; expected ')' + error: unclosed tree: unclosed tree; expected ')' | 1 | (a (b (c (d - | - ^ unclosed tree; expected ')' + | - ^ unclosed tree: unclosed tree; expected ')' | | | tree started here "); @@ -100,10 +100,10 @@ fn unclosed_alternation_nested() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree; expected ')' + error: unclosed tree: unclosed tree; expected ')' | 1 | [(a) (b - | - ^ unclosed tree; expected ')' + | - ^ unclosed tree: unclosed tree; expected ')' | | | tree started here "); @@ -118,10 +118,10 @@ fn empty_parens() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: empty tree expression - expected node type or children + error: empty tree expression: empty tree expression | 1 | () - | ^ empty tree expression - expected node type or children + | ^ empty tree expression: empty tree expression "); } @@ -135,12 +135,12 @@ fn unclosed_tree_shows_open_location() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree; expected ')' + error: unclosed tree: unclosed tree; expected ')' | 1 | (call | - tree started here 2 | (identifier) - | ^ unclosed tree; expected ')' + | ^ unclosed tree: unclosed tree; expected ')' "); } @@ -155,13 +155,13 @@ fn unclosed_alternation_shows_open_location() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed alternation; expected ']' + error: unclosed alternation: unclosed alternation; expected ']' | 1 | [ | - alternation started here 2 | (a) 3 | (b) - | ^ unclosed alternation; expected ']' + | ^ unclosed alternation: unclosed alternation; expected ']' "); } @@ -176,13 +176,13 @@ fn unclosed_sequence_shows_open_location() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed sequence; expected '}' + error: unclosed sequence: unclosed sequence; expected '}' | 1 | { | - sequence started here 2 | (a) 3 | (b) - | ^ unclosed sequence; expected '}' + | ^ unclosed sequence: unclosed sequence; expected '}' "); } @@ -193,14 +193,14 @@ fn unclosed_double_quote_string() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected a child expression or closing delimiter + error: unexpected token: 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 ')' + | ^^^^^ unexpected token: unexpected token; expected a child expression or closing delimiter + error: unclosed tree: unclosed tree; expected ')' | 1 | (call "foo) - | - ^ unclosed tree; expected ')' + | - ^ unclosed tree: unclosed tree; expected ')' | | | tree started here "#); @@ -213,14 +213,14 @@ fn unclosed_single_quote_string() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token; expected a child expression or closing delimiter + error: unexpected token: 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 ')' + | ^^^^^ unexpected token: unexpected token; expected a child expression or closing delimiter + error: unclosed tree: unclosed tree; expected ')' | 1 | (call 'foo) - | - ^ unclosed tree; expected ')' + | - ^ unclosed tree: unclosed tree; expected ')' | | | tree started here "); diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs index 87f55a95..f6208138 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs @@ -10,14 +10,14 @@ fn unexpected_token() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | (identifier) ^^^ (string) - | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - error: unnamed definition must be last in file; add a name: `Name = (identifier)` + | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unnamed definition must be last: add a name: `Name = (identifier)` | 1 | (identifier) ^^^ (string) - | ^^^^^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (identifier)` + | ^^^^^^^^^^^^ unnamed definition must be last: add a name: `Name = (identifier)` "#); } @@ -30,10 +30,10 @@ fn multiple_consecutive_garbage() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | ^^^ $$$ %%% (ok) - | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ "#); } @@ -46,10 +46,10 @@ fn garbage_at_start() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | ^^^ (a) - | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ "#); } @@ -62,10 +62,10 @@ fn only_garbage() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | ^^^ $$$ - | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ "#); } @@ -78,10 +78,10 @@ fn garbage_inside_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token; expected a child expression or closing delimiter + error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | [(a) ^^^ (b)] - | ^^^ unexpected token; expected a child expression or closing delimiter + | ^^^ unexpected token: unexpected token; expected a child expression or closing delimiter "); } @@ -94,18 +94,18 @@ fn garbage_inside_node() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected capture name after '@' + error: expected capture name: expected capture name | 1 | (a (b) @@@ (c)) (d) - | ^ expected capture name after '@' - error: unexpected token; expected a child expression or closing delimiter + | ^ expected capture name: expected capture name + error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | (a (b) @@@ (c)) (d) - | ^ unexpected token; expected a child expression or closing delimiter - error: unnamed definition must be last in file; add a name: `Name = (a (b) @@@ (c))` + | ^ unexpected token: unexpected token; expected a child expression or closing delimiter + error: unnamed definition must be last: add a name: `Name = (a (b) @@@ (c))` | 1 | (a (b) @@@ (c)) (d) - | ^^^^^^^^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (a (b) @@@ (c))` + | ^^^^^^^^^^^^^^^ unnamed definition must be last: add a name: `Name = (a (b) @@@ (c))` "); } @@ -118,14 +118,14 @@ fn xml_tag_garbage() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 |
(identifier)
- | ^^^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 |
(identifier)
- | ^^^^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ "#); } @@ -138,10 +138,10 @@ fn xml_self_closing() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 |
(a) - | ^^^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ "#); } @@ -154,22 +154,22 @@ fn predicate_unsupported() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported + error: unsupported predicate: unsupported predicate | 1 | (a (#eq? @x "foo") b) - | ^^^^ tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported - error: unexpected token; expected a child expression or closing delimiter + | ^^^^ unsupported predicate: unsupported predicate + error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | (a (#eq? @x "foo") b) - | ^ unexpected token; expected a child expression or closing delimiter - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^ unexpected token: unexpected token; expected a child expression or closing delimiter + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (a (#eq? @x "foo") b) - | ^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (a (#eq? @x "foo") b) - | ^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) "#); } @@ -182,18 +182,18 @@ fn predicate_match() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported + error: unsupported predicate: unsupported predicate | 1 | (identifier) #match? @name "test" - | ^^^^^^^ tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^^^^^^^ unsupported predicate: unsupported predicate + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (identifier) #match? @name "test" - | ^^^^ 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)` + | ^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: unnamed definition must be last: add a name: `Name = (identifier)` | 1 | (identifier) #match? @name "test" - | ^^^^^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (identifier)` + | ^^^^^^^^^^^^ unnamed definition must be last: add a name: `Name = (identifier)` "#); } @@ -204,18 +204,18 @@ fn predicate_in_tree() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported + error: unsupported predicate: unsupported predicate | 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 + | ^^^^ unsupported predicate: unsupported predicate + error: unexpected token: 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) + | ^ unexpected token: unexpected token; expected a child expression or closing delimiter + error: bare identifier not allowed: 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) + | ^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) "#); } @@ -228,10 +228,10 @@ fn predicate_in_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token; expected a child expression or closing delimiter + error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | [(a) #eq? (b)] - | ^^^^ unexpected token; expected a child expression or closing delimiter + | ^^^^ unexpected token: unexpected token; expected a child expression or closing delimiter "); } @@ -244,10 +244,10 @@ fn predicate_in_sequence() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported + error: unsupported predicate: unsupported predicate | 1 | {(a) #set! (b)} - | ^^^^^ tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported + | ^^^^^ unsupported predicate: unsupported predicate "); } @@ -262,14 +262,14 @@ fn multiline_garbage_recovery() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token; expected a child expression or closing delimiter + error: unexpected token: unexpected token; expected a child expression or closing delimiter | 2 | ^^^ - | ^^^ unexpected token; expected a child expression or closing delimiter - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^^^ unexpected token: unexpected token; expected a child expression or closing delimiter + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 3 | b) - | ^ bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) "); } @@ -282,10 +282,10 @@ fn top_level_garbage_recovery() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token: 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 _ + | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ "#); } @@ -302,14 +302,14 @@ fn multiple_definitions_with_garbage_between() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 2 | ^^^ - | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 4 | $$$ - | ^^^ unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ "#); } @@ -322,18 +322,18 @@ fn alternation_recovery_to_capture() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token; expected a child expression or closing delimiter + error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | [^^^ @name] - | ^^^ unexpected token; expected a child expression or closing delimiter - error: unexpected token; expected a child expression or closing delimiter + | ^^^ unexpected token: unexpected token; expected a child expression or closing delimiter + error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | [^^^ @name] - | ^ unexpected token; expected a child expression or closing delimiter - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^ unexpected token: unexpected token; expected a child expression or closing delimiter + error: bare identifier not allowed: 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) + | ^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) "); } @@ -346,10 +346,10 @@ fn comma_between_defs() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token: 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 _ + | ^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ "#); } @@ -360,10 +360,10 @@ fn bare_colon_in_tree() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token; expected a child expression or closing delimiter + error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | (a : (b)) - | ^ unexpected token; expected a child expression or closing delimiter + | ^ unexpected token: unexpected token; expected a child expression or closing delimiter "); } @@ -374,18 +374,18 @@ fn paren_close_inside_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: expected closing ']' for alternation + error: unexpected token: expected closing ']' for alternation | 1 | [(a) ) (b)] - | ^ expected closing ']' for alternation - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^ unexpected token: expected closing ']' for alternation + error: unexpected token: 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)` + | ^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unnamed definition must be last: add a name: `Name = [(a)` | 1 | [(a) ) (b)] - | ^^^^ unnamed definition must be last in file; add a name: `Name = [(a)` + | ^^^^ unnamed definition must be last: add a name: `Name = [(a)` "#); } @@ -396,18 +396,18 @@ fn bracket_close_inside_sequence() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: expected closing '}' for sequence + error: unexpected token: expected closing '}' for sequence | 1 | {(a) ] (b)} - | ^ expected closing '}' for sequence - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^ unexpected token: expected closing '}' for sequence + error: unexpected token: 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)` + | ^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unnamed definition must be last: add a name: `Name = {(a)` | 1 | {(a) ] (b)} - | ^^^^ unnamed definition must be last in file; add a name: `Name = {(a)` + | ^^^^ unnamed definition must be last: add a name: `Name = {(a)` "#); } @@ -418,18 +418,18 @@ fn paren_close_inside_sequence() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: expected closing '}' for sequence + error: unexpected token: expected closing '}' for sequence | 1 | {(a) ) (b)} - | ^ expected closing '}' for sequence - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^ unexpected token: expected closing '}' for sequence + error: unexpected token: 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)` + | ^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unnamed definition must be last: add a name: `Name = {(a)` | 1 | {(a) ) (b)} - | ^^^^ unnamed definition must be last in file; add a name: `Name = {(a)` + | ^^^^ unnamed definition must be last: add a name: `Name = {(a)` "#); } @@ -440,14 +440,14 @@ fn single_colon_type_annotation_followed_by_non_id() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token: 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` + | ^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unnamed definition must be last: add a name: `Name = (a) @x` | 1 | (a) @x : (b) - | ^^^^^^ unnamed definition must be last in file; add a name: `Name = (a) @x` + | ^^^^^^ unnamed definition must be last: add a name: `Name = (a) @x` "#); } @@ -458,9 +458,9 @@ fn single_colon_type_annotation_at_eof() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token: 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 _ + | ^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ "#); } 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 899e5339..47e6a9b7 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs @@ -59,10 +59,10 @@ fn reference_with_supertype_syntax_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: references cannot use supertype syntax (/) + error: invalid supertype syntax: invalid supertype syntax | 1 | (RefName/subtype) - | ^ references cannot use supertype syntax (/) + | ^ invalid supertype syntax: invalid supertype syntax "); } @@ -93,10 +93,10 @@ fn error_with_unexpected_content() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: (ERROR) takes no arguments + error: (ERROR) takes no arguments: (ERROR) takes no arguments | 1 | (ERROR (something)) - | ^ (ERROR) takes no arguments + | ^ (ERROR) takes no arguments: (ERROR) takes no arguments "); } @@ -109,10 +109,10 @@ fn bare_error_keyword() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...) + error: ERROR/MISSING outside parentheses: ERROR/MISSING outside parentheses | 1 | ERROR - | ^^^^^ ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...) + | ^^^^^ ERROR/MISSING outside parentheses: ERROR/MISSING outside parentheses "); } @@ -125,10 +125,10 @@ fn bare_missing_keyword() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...) + error: ERROR/MISSING outside parentheses: ERROR/MISSING outside parentheses | 1 | MISSING - | ^^^^^^^ ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...) + | ^^^^^^^ ERROR/MISSING outside parentheses: ERROR/MISSING outside parentheses "); } @@ -177,10 +177,10 @@ fn bare_upper_ident_not_followed_by_equals_is_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed: 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) + | ^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) "); } @@ -193,10 +193,10 @@ fn named_def_missing_equals() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed: 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) + | ^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) "); } @@ -211,10 +211,10 @@ fn unnamed_def_not_allowed_in_middle() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unnamed definition must be last in file; add a name: `Name = (first)` + error: unnamed definition must be last: add a name: `Name = (first)` | 1 | (first) - | ^^^^^^^ unnamed definition must be last in file; add a name: `Name = (first)` + | ^^^^^^^ unnamed definition must be last: add a name: `Name = (first)` "); } @@ -229,14 +229,14 @@ fn multiple_unnamed_defs_errors_for_all_but_last() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unnamed definition must be last in file; add a name: `Name = (first)` + error: unnamed definition must be last: 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)` + | ^^^^^^^ unnamed definition must be last: add a name: `Name = (first)` + error: unnamed definition must be last: add a name: `Name = (second)` | 2 | (second) - | ^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (second)` + | ^^^^^^^^ unnamed definition must be last: add a name: `Name = (second)` "); } @@ -249,14 +249,14 @@ fn capture_space_after_dot_is_anchor() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unnamed definition must be last in file; add a name: `Name = (identifier) @foo` + error: unnamed definition must be last: 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 = .` + | ^^^^^^^^^^^^^^^^^ unnamed definition must be last: add a name: `Name = (identifier) @foo` + error: unnamed definition must be last: add a name: `Name = .` | 1 | (identifier) @foo . (other) - | ^ unnamed definition must be last in file; add a name: `Name = .` + | ^ unnamed definition must be last: add a name: `Name = .` "); } @@ -267,10 +267,10 @@ fn def_name_lowercase_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition names must start with uppercase + error: definition name starts with lowercase: definition names must start with uppercase | 1 | lowercase = (x) - | ^^^^^^^^^ definition names must start with uppercase + | ^^^^^^^^^ definition name starts with lowercase: definition names must start with uppercase | help: definition names must be PascalCase; use Lowercase instead | @@ -289,10 +289,10 @@ fn def_name_snake_case_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition names must start with uppercase + error: definition name starts with lowercase: definition names must start with uppercase | 1 | my_expr = (identifier) - | ^^^^^^^ definition names must start with uppercase + | ^^^^^^^ definition name starts with lowercase: definition names must start with uppercase | help: definition names must be PascalCase; use MyExpr instead | @@ -311,10 +311,10 @@ fn def_name_kebab_case_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition names must start with uppercase + error: definition name starts with lowercase: definition names must start with uppercase | 1 | my-expr = (identifier) - | ^^^^^^^ definition names must start with uppercase + | ^^^^^^^ definition name starts with lowercase: definition names must start with uppercase | help: definition names must be PascalCase; use MyExpr instead | @@ -333,10 +333,10 @@ fn def_name_dotted_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition names must start with uppercase + error: definition name starts with lowercase: definition names must start with uppercase | 1 | my.expr = (identifier) - | ^^^^^^^ definition names must start with uppercase + | ^^^^^^^ definition name starts with lowercase: definition names must start with uppercase | help: definition names must be PascalCase; use MyExpr instead | @@ -353,10 +353,10 @@ fn def_name_with_underscores_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition names cannot contain separators + error: definition name contains separators: definition names cannot contain separators | 1 | Some_Thing = (x) - | ^^^^^^^^^^ definition names cannot contain separators + | ^^^^^^^^^^ definition name contains separators: definition names cannot contain separators | help: definition names must be PascalCase; use SomeThing instead | @@ -373,10 +373,10 @@ fn def_name_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition names cannot contain separators + error: definition name contains separators: definition names cannot contain separators | 1 | Some-Thing = (x) - | ^^^^^^^^^^ definition names cannot contain separators + | ^^^^^^^^^^ definition name contains separators: definition names cannot contain separators | help: definition names must be PascalCase; use SomeThing instead | @@ -395,10 +395,10 @@ fn capture_name_pascal_case_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture names must start with lowercase + error: capture name starts with uppercase: capture names must start with lowercase | 1 | (a) @Name - | ^^^^ capture names must start with lowercase + | ^^^^ capture name starts with uppercase: capture names must start with lowercase | help: capture names must be snake_case; use @name instead | @@ -417,10 +417,10 @@ fn capture_name_pascal_case_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture names cannot contain hyphens + error: capture name contains hyphens: capture names cannot contain hyphens | 1 | (a) @My-Name - | ^^^^^^^ capture names cannot contain hyphens + | ^^^^^^^ capture name contains hyphens: capture names cannot contain hyphens | help: captures become struct fields; use @my_name instead | @@ -439,10 +439,10 @@ fn capture_name_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture names cannot contain hyphens + error: capture name contains hyphens: capture names cannot contain hyphens | 1 | (a) @my-name - | ^^^^^^^ capture names cannot contain hyphens + | ^^^^^^^ capture name contains hyphens: capture names cannot contain hyphens | help: captures become struct fields; use @my_name instead | @@ -461,10 +461,10 @@ fn capture_dotted_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture names cannot contain dots + error: capture name contains dots: capture names cannot contain dots | 1 | (identifier) @foo.bar - | ^^^^^^^ capture names cannot contain dots + | ^^^^^^^ capture name contains dots: capture names cannot contain dots | help: captures become struct fields; use @foo_bar instead | @@ -483,10 +483,10 @@ fn capture_dotted_multiple_parts() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture names cannot contain dots + error: capture name contains dots: capture names cannot contain dots | 1 | (identifier) @foo.bar.baz - | ^^^^^^^^^^^ capture names cannot contain dots + | ^^^^^^^^^^^ capture name contains dots: capture names cannot contain dots | help: captures become struct fields; use @foo_bar_baz instead | @@ -505,20 +505,20 @@ fn capture_dotted_followed_by_field() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture names cannot contain dots + error: capture name contains dots: capture names cannot contain dots | 1 | (node) @foo.bar name: (other) - | ^^^^^^^ capture names cannot contain dots + | ^^^^^^^ capture name contains dots: 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` + error: unnamed definition must be last: 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` + | ^^^^^^^^^^^^^^^ unnamed definition must be last: add a name: `Name = (node) @foo.bar` "); } @@ -531,24 +531,24 @@ fn capture_space_after_dot_breaks_chain() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture names cannot contain dots + error: capture name contains dots: capture names cannot contain dots | 1 | (identifier) @foo. bar - | ^^^^ capture names cannot contain dots + | ^^^^ capture name contains dots: 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) + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 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.` + | ^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: unnamed definition must be last: add a name: `Name = (identifier) @foo.` | 1 | (identifier) @foo. bar - | ^^^^^^^^^^^^^^^^^^ unnamed definition must be last in file; add a name: `Name = (identifier) @foo.` + | ^^^^^^^^^^^^^^^^^^ unnamed definition must be last: add a name: `Name = (identifier) @foo.` "); } @@ -561,10 +561,10 @@ fn capture_hyphenated_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture names cannot contain hyphens + error: capture name contains hyphens: capture names cannot contain hyphens | 1 | (identifier) @foo-bar - | ^^^^^^^ capture names cannot contain hyphens + | ^^^^^^^ capture name contains hyphens: capture names cannot contain hyphens | help: captures become struct fields; use @foo_bar instead | @@ -583,10 +583,10 @@ fn capture_hyphenated_multiple() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture names cannot contain hyphens + error: capture name contains hyphens: capture names cannot contain hyphens | 1 | (identifier) @foo-bar-baz - | ^^^^^^^^^^^ capture names cannot contain hyphens + | ^^^^^^^^^^^ capture name contains hyphens: capture names cannot contain hyphens | help: captures become struct fields; use @foo_bar_baz instead | @@ -605,10 +605,10 @@ fn capture_mixed_dots_and_hyphens() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture names cannot contain dots + error: capture name contains dots: capture names cannot contain dots | 1 | (identifier) @foo.bar-baz - | ^^^^^^^^^^^ capture names cannot contain dots + | ^^^^^^^^^^^ capture name contains dots: capture names cannot contain dots | help: captures become struct fields; use @foo_bar_baz instead | @@ -627,10 +627,10 @@ fn field_name_pascal_case_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: field names must start with lowercase + error: field name starts with uppercase: field names must start with lowercase | 1 | (call Name: (a)) - | ^^^^ field names must start with lowercase + | ^^^^ field name starts with uppercase: field names must start with lowercase | help: field names must be snake_case; use name: instead | @@ -647,10 +647,10 @@ fn field_name_with_dots_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: field names cannot contain dots + error: field name contains dots: field names cannot contain dots | 1 | (call foo.bar: (x)) - | ^^^^^^^ field names cannot contain dots + | ^^^^^^^ field name contains dots: field names cannot contain dots | help: field names must be snake_case; use foo_bar: instead | @@ -667,10 +667,10 @@ fn field_name_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: field names cannot contain hyphens + error: field name contains hyphens: field names cannot contain hyphens | 1 | (call foo-bar: (x)) - | ^^^^^^^ field names cannot contain hyphens + | ^^^^^^^ field name contains hyphens: field names cannot contain hyphens | help: field names must be snake_case; use foo_bar: instead | @@ -689,10 +689,10 @@ fn negated_field_with_upper_ident_parses() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: field names must start with lowercase + error: field name starts with uppercase: field names must start with lowercase | 1 | (call !Arguments) - | ^^^^^^^^^ field names must start with lowercase + | ^^^^^^^^^ field name starts with uppercase: field names must start with lowercase | help: field names must be snake_case; use arguments: instead | @@ -711,10 +711,10 @@ fn branch_label_snake_case_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: branch labels cannot contain separators + error: branch label contains separators: branch labels cannot contain separators | 1 | [My_branch: (a) Other: (b)] - | ^^^^^^^^^ branch labels cannot contain separators + | ^^^^^^^^^ branch label contains separators: branch labels cannot contain separators | help: branch labels must be PascalCase; use MyBranch: instead | @@ -733,10 +733,10 @@ fn branch_label_kebab_case_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: branch labels cannot contain separators + error: branch label contains separators: branch labels cannot contain separators | 1 | [My-branch: (a) Other: (b)] - | ^^^^^^^^^ branch labels cannot contain separators + | ^^^^^^^^^ branch label contains separators: branch labels cannot contain separators | help: branch labels must be PascalCase; use MyBranch: instead | @@ -755,10 +755,10 @@ fn branch_label_dotted_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: branch labels cannot contain separators + error: branch label contains separators: branch labels cannot contain separators | 1 | [My.branch: (a) Other: (b)] - | ^^^^^^^^^ branch labels cannot contain separators + | ^^^^^^^^^ branch label contains separators: branch labels cannot contain separators | help: branch labels must be PascalCase; use MyBranch: instead | @@ -775,10 +775,10 @@ fn branch_label_with_underscores_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: branch labels cannot contain separators + error: branch label contains separators: branch labels cannot contain separators | 1 | [Some_Label: (x)] - | ^^^^^^^^^^ branch labels cannot contain separators + | ^^^^^^^^^^ branch label contains separators: branch labels cannot contain separators | help: branch labels must be PascalCase; use SomeLabel: instead | @@ -795,10 +795,10 @@ fn branch_label_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: branch labels cannot contain separators + error: branch label contains separators: branch labels cannot contain separators | 1 | [Some-Label: (x)] - | ^^^^^^^^^^ branch labels cannot contain separators + | ^^^^^^^^^^ branch label contains separators: branch labels cannot contain separators | help: branch labels must be PascalCase; use SomeLabel: instead | @@ -820,20 +820,20 @@ fn lowercase_branch_label() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: tagged alternation labels must be Capitalized (they map to enum variants) + error: lowercase branch label: 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) + | ^^^^ lowercase branch label: 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) + error: lowercase branch label: 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) + | ^^^^^ lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) | help: capitalize as `Right` | @@ -852,10 +852,10 @@ fn lowercase_branch_label_suggests_capitalized() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: tagged alternation labels must be Capitalized (they map to enum variants) + error: lowercase branch label: 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) + | ^^^^^ lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) | help: capitalize as `First` | @@ -872,10 +872,10 @@ fn mixed_case_branch_labels() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: tagged alternation labels must be Capitalized (they map to enum variants) + error: lowercase branch label: 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) + | ^^^ lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) | help: capitalize as `Foo` | @@ -894,10 +894,10 @@ fn type_annotation_dotted_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: type names cannot contain dots or hyphens + error: type name contains invalid characters: type names cannot contain dots or hyphens | 1 | (a) @x :: My.Type - | ^^^^^^^ type names cannot contain dots or hyphens + | ^^^^^^^ type name contains invalid characters: type names cannot contain dots or hyphens | help: type names cannot contain separators; use ::MyType instead | @@ -916,10 +916,10 @@ fn type_annotation_kebab_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: type names cannot contain dots or hyphens + error: type name contains invalid characters: type names cannot contain dots or hyphens | 1 | (a) @x :: My-Type - | ^^^^^^^ type names cannot contain dots or hyphens + | ^^^^^^^ type name contains invalid characters: type names cannot contain dots or hyphens | help: type names cannot contain separators; use ::MyType instead | @@ -936,10 +936,10 @@ fn type_name_with_dots_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: type names cannot contain dots or hyphens + error: type name contains invalid characters: type names cannot contain dots or hyphens | 1 | (x) @name :: Some.Type - | ^^^^^^^^^ type names cannot contain dots or hyphens + | ^^^^^^^^^ type name contains invalid characters: type names cannot contain dots or hyphens | help: type names cannot contain separators; use ::SomeType instead | @@ -956,10 +956,10 @@ fn type_name_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: type names cannot contain dots or hyphens + error: type name contains invalid characters: type names cannot contain dots or hyphens | 1 | (x) @name :: Some-Type - | ^^^^^^^^^ type names cannot contain dots or hyphens + | ^^^^^^^^^ type name contains invalid characters: type names cannot contain dots or hyphens | help: type names cannot contain separators; use ::SomeType instead | @@ -976,10 +976,10 @@ fn comma_in_node_children() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: ',' is not valid syntax; plotnik uses whitespace for separation + error: invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation | 1 | (node (a), (b)) - | ^ ',' is not valid syntax; plotnik uses whitespace for separation + | ^ invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation | help: remove separator | @@ -996,20 +996,20 @@ fn comma_in_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: ',' is not valid syntax; plotnik uses whitespace for separation + error: invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a), (b), (c)] - | ^ ',' is not valid syntax; plotnik uses whitespace for separation + | ^ invalid separator: ',' 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 + error: invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a), (b), (c)] - | ^ ',' is not valid syntax; plotnik uses whitespace for separation + | ^ invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation | help: remove separator | @@ -1026,10 +1026,10 @@ fn comma_in_sequence() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: ',' is not valid syntax; plotnik uses whitespace for separation + error: invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation | 1 | {(a), (b)} - | ^ ',' is not valid syntax; plotnik uses whitespace for separation + | ^ invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation | help: remove separator | @@ -1046,20 +1046,20 @@ fn pipe_in_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: '|' is not valid syntax; plotnik uses whitespace for separation + error: invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a) | (b) | (c)] - | ^ '|' is not valid syntax; plotnik uses whitespace for separation + | ^ invalid separator: '|' 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 + error: invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a) | (b) | (c)] - | ^ '|' is not valid syntax; plotnik uses whitespace for separation + | ^ invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation | help: remove separator | @@ -1078,10 +1078,10 @@ fn pipe_between_branches() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: '|' is not valid syntax; plotnik uses whitespace for separation + error: invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a) | (b)] - | ^ '|' is not valid syntax; plotnik uses whitespace for separation + | ^ invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation | help: remove separator | @@ -1098,20 +1098,20 @@ fn pipe_in_tree() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: '|' is not valid syntax; plotnik uses whitespace for separation + error: invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation | 1 | (a | b) - | ^ '|' is not valid syntax; plotnik uses whitespace for separation + | ^ invalid separator: '|' 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) + error: bare identifier not allowed: 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) + | ^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) "); } @@ -1122,10 +1122,10 @@ fn pipe_in_sequence() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: '|' is not valid syntax; plotnik uses whitespace for separation + error: invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation | 1 | {(a) | (b)} - | ^ '|' is not valid syntax; plotnik uses whitespace for separation + | ^ invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation | help: remove separator | @@ -1142,10 +1142,10 @@ fn field_equals_typo() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: '=' is not valid for field constraints + error: invalid field syntax: '=' is not valid for field constraints | 1 | (node name = (identifier)) - | ^ '=' is not valid for field constraints + | ^ invalid field syntax: '=' is not valid for field constraints | help: use ':' | @@ -1162,10 +1162,10 @@ fn field_equals_typo_no_space() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: '=' is not valid for field constraints + error: invalid field syntax: '=' is not valid for field constraints | 1 | (node name=(identifier)) - | ^ '=' is not valid for field constraints + | ^ invalid field syntax: '=' is not valid for field constraints | help: use ':' | @@ -1182,20 +1182,20 @@ fn field_equals_typo_no_expression() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: '=' is not valid for field constraints + error: invalid field syntax: '=' is not valid for field constraints | 1 | (call name=) - | ^ '=' is not valid for field constraints + | ^ invalid field syntax: '=' is not valid for field constraints | help: use ':' | 1 - (call name=) 1 + (call name:) | - error: expected expression after field name + error: expected expression: expected expression after field name | 1 | (call name=) - | ^ expected expression after field name + | ^ expected expression: expected expression after field name "); } @@ -1208,10 +1208,10 @@ fn field_equals_typo_in_tree() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: '=' is not valid for field constraints + error: invalid field syntax: '=' is not valid for field constraints | 1 | (call name = (identifier)) - | ^ '=' is not valid for field constraints + | ^ invalid field syntax: '=' is not valid for field constraints | help: use ':' | @@ -1228,10 +1228,10 @@ fn single_colon_type_annotation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: single colon is not valid for type annotations + error: invalid type annotation: single colon is not valid for type annotations | 1 | (identifier) @name : Type - | ^ single colon is not valid for type annotations + | ^ invalid type annotation: single colon is not valid for type annotations | help: use '::' | @@ -1247,10 +1247,10 @@ fn single_colon_type_annotation_no_space() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: single colon is not valid for type annotations + error: invalid type annotation: single colon is not valid for type annotations | 1 | (identifier) @name:Type - | ^ single colon is not valid for type annotations + | ^ invalid type annotation: single colon is not valid for type annotations | help: use '::' | @@ -1268,10 +1268,10 @@ fn single_colon_type_annotation_with_space() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: single colon is not valid for type annotations + error: invalid type annotation: single colon is not valid for type annotations | 1 | (a) @x : Type - | ^ single colon is not valid for type annotations + | ^ invalid type annotation: single colon is not valid for type annotations | help: use '::' | @@ -1287,25 +1287,25 @@ fn single_colon_primitive_type() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture '@' must follow an expression to capture + error: capture without target: capture without target | 1 | @val : string - | ^ capture '@' must follow an expression to capture - error: expected ':' to separate field name from its value + | ^ capture without target: capture without target + error: unexpected token: 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 + | ^ unexpected token: expected ':' to separate field name from its value + error: expected expression: 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) + | ^ expected expression: expected expression after field name + error: bare identifier not allowed: 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` + | ^^^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: unnamed definition must be last: add a name: `Name = val` | 1 | @val : string - | ^^^ unnamed definition must be last in file; add a name: `Name = val` + | ^^^ unnamed definition must be last: add a name: `Name = val` "); } diff --git a/crates/plotnik-lib/src/query/alt_kinds.rs b/crates/plotnik-lib/src/query/alt_kinds.rs index 56ffa110..3c6a002c 100644 --- a/crates/plotnik-lib/src/query/alt_kinds.rs +++ b/crates/plotnik-lib/src/query/alt_kinds.rs @@ -80,7 +80,6 @@ impl Query<'_> { self.alt_kind_diagnostics .report(DiagnosticKind::MixedAltBranches, untagged_range) - .message("mixed tagged and untagged branches in alternation") .related_to("tagged branch here", tagged_range) .emit(); } diff --git a/crates/plotnik-lib/src/query/recursion.rs b/crates/plotnik-lib/src/query/recursion.rs index 51386780..e23267e8 100644 --- a/crates/plotnik-lib/src/query/recursion.rs +++ b/crates/plotnik-lib/src/query/recursion.rs @@ -211,10 +211,7 @@ impl Query<'_> { let mut builder = self .recursion_diagnostics .report(DiagnosticKind::RecursionNoEscape, range) - .message(format!( - "recursive pattern can never match: cycle {} has no escape path", - cycle_str - )); + .message(format!("cycle {} has no escape path", cycle_str)); for (rel_range, rel_msg) in related { builder = builder.related_to(rel_msg, rel_range); diff --git a/crates/plotnik-lib/src/query/shapes.rs b/crates/plotnik-lib/src/query/shapes.rs index 3cbf8de8..5a3fe840 100644 --- a/crates/plotnik-lib/src/query/shapes.rs +++ b/crates/plotnik-lib/src/query/shapes.rs @@ -118,10 +118,7 @@ impl Query<'_> { self.shapes_diagnostics .report(DiagnosticKind::FieldSequenceValue, value.text_range()) - .message(format!( - "field `{}` value must match a single node, not a sequence", - field_name - )) + .message(field_name) .emit(); } diff --git a/crates/plotnik-lib/src/query/shapes_tests.rs b/crates/plotnik-lib/src/query/shapes_tests.rs index 46f5964f..d76a469d 100644 --- a/crates/plotnik-lib/src/query/shapes_tests.rs +++ b/crates/plotnik-lib/src/query/shapes_tests.rs @@ -168,10 +168,10 @@ fn field_with_seq_error() { NamedNode¹ b "); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: field `name` value must match a single node, not a sequence + error: field `name` value must be a single node | 1 | (call name: {(a) (b)}) - | ^^^^^^^^^ field `name` value must match a single node, not a sequence + | ^^^^^^^^^ field `name` value must be a single node "); } @@ -195,10 +195,10 @@ fn field_with_ref_to_seq_error() { Ref⁺ X "); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: field `name` value must match a single node, not a sequence + error: field `name` value must be a single node | 2 | (call name: (X)) - | ^^^ field `name` value must match a single node, not a sequence + | ^^^ field `name` value must be a single node "); } diff --git a/crates/plotnik-lib/src/query/symbol_table.rs b/crates/plotnik-lib/src/query/symbol_table.rs index 63abc0b0..73ebb029 100644 --- a/crates/plotnik-lib/src/query/symbol_table.rs +++ b/crates/plotnik-lib/src/query/symbol_table.rs @@ -27,7 +27,7 @@ impl<'a> Query<'a> { if self.symbol_table.contains_key(name) { self.resolve_diagnostics .report(DiagnosticKind::DuplicateDefinition, range) - .message(format!("duplicate definition: `{}`", name)) + .message(name) .emit(); continue; } @@ -104,7 +104,7 @@ impl<'a> Query<'a> { self.resolve_diagnostics .report(DiagnosticKind::UndefinedReference, name_token.text_range()) - .message(format!("undefined reference: `{}`", name)) + .message(name) .emit(); } } From 93598c93825fe3e013b3a52f31cdf18cea29bbfe Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 6 Dec 2025 10:34:29 -0300 Subject: [PATCH 07/15] Modify `custom_message()` to dynamically generate message templates --- crates/plotnik-lib/src/diagnostics/message.rs | 59 ++++--------------- 1 file changed, 13 insertions(+), 46 deletions(-) diff --git a/crates/plotnik-lib/src/diagnostics/message.rs b/crates/plotnik-lib/src/diagnostics/message.rs index aedf03e8..4c96daae 100644 --- a/crates/plotnik-lib/src/diagnostics/message.rs +++ b/crates/plotnik-lib/src/diagnostics/message.rs @@ -133,56 +133,23 @@ impl DiagnosticKind { } /// Template for custom messages. Contains `{}` placeholder for caller-provided detail. - pub fn custom_message(&self) -> &'static str { + pub fn custom_message(&self) -> String { match self { - // Unclosed delimiters - Self::UnclosedTree => "unclosed tree: {}", - Self::UnclosedSequence => "unclosed sequence: {}", - Self::UnclosedAlternation => "unclosed alternation: {}", - - // Expected token errors - Self::ExpectedExpression => "expected expression: {}", - Self::ExpectedTypeName => "expected type name: {}", - Self::ExpectedCaptureName => "expected capture name: {}", - Self::ExpectedFieldName => "expected field name: {}", - Self::ExpectedSubtype => "expected subtype: {}", - - // Invalid token/syntax usage - Self::EmptyTree => "empty tree expression: {}", - Self::BareIdentifier => "bare identifier not allowed: {}", - Self::InvalidSeparator => "invalid separator: {}", - Self::InvalidFieldEquals => "invalid field syntax: {}", - Self::InvalidSupertypeSyntax => "invalid supertype syntax: {}", - Self::InvalidTypeAnnotationSyntax => "invalid type annotation: {}", - Self::ErrorTakesNoArguments => "(ERROR) takes no arguments: {}", - Self::RefCannotHaveChildren => "reference `{}` cannot contain children", - Self::ErrorMissingOutsideParens => "ERROR/MISSING outside parentheses: {}", - Self::UnsupportedPredicate => "unsupported predicate: {}", - Self::UnexpectedToken => "unexpected token: {}", - Self::CaptureWithoutTarget => "capture without target: {}", - Self::LowercaseBranchLabel => "lowercase branch label: {}", + // Special cases: placeholder embedded in message + Self::RefCannotHaveChildren => "reference `{}` cannot contain children".to_string(), + Self::FieldSequenceValue => "field `{}` value must be a single node".to_string(), - // Naming validation - Self::CaptureNameHasDots => "capture name contains dots: {}", - Self::CaptureNameHasHyphens => "capture name contains hyphens: {}", - Self::CaptureNameUppercase => "capture name starts with uppercase: {}", - Self::DefNameLowercase => "definition name starts with lowercase: {}", - Self::DefNameHasSeparators => "definition name contains separators: {}", - Self::BranchLabelHasSeparators => "branch label contains separators: {}", - Self::FieldNameHasDots => "field name contains dots: {}", - Self::FieldNameHasHyphens => "field name contains hyphens: {}", - Self::FieldNameUppercase => "field name starts with uppercase: {}", - Self::TypeNameInvalidChars => "type name contains invalid characters: {}", + // Cases with backtick-wrapped placeholders + Self::DuplicateDefinition | Self::UndefinedReference => { + format!("{}: `{{}}`", self.fallback_message()) + } - // Semantic errors - Self::DuplicateDefinition => "duplicate definition: `{}`", - Self::UndefinedReference => "undefined reference: `{}`", - Self::MixedAltBranches => "mixed alternation: {}", - Self::RecursionNoEscape => "recursive pattern can never match: {}", - Self::FieldSequenceValue => "field `{}` value must be a single node", + // Cases where custom text differs from fallback + Self::InvalidTypeAnnotationSyntax => "invalid type annotation: {}".to_string(), + Self::MixedAltBranches => "mixed alternation: {}".to_string(), - // Structural observations - Self::UnnamedDefNotLast => "unnamed definition must be last: {}", + // Standard pattern: fallback + ": {}" + _ => format!("{}: {{}}", self.fallback_message()), } } From 221cfdac468b9791129e602f0470a542209f2828 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 6 Dec 2025 10:43:51 -0300 Subject: [PATCH 08/15] Reduce diagnostics verbosity --- crates/plotnik-lib/src/diagnostics/printer.rs | 8 +- crates/plotnik-lib/src/diagnostics/tests.rs | 19 +-- crates/plotnik-lib/src/parser/ast_tests.rs | 6 +- .../parser/tests/recovery/coverage_tests.rs | 8 +- .../parser/tests/recovery/incomplete_tests.rs | 52 +++--- .../parser/tests/recovery/unclosed_tests.rs | 28 ++-- .../parser/tests/recovery/unexpected_tests.rs | 92 +++++------ .../parser/tests/recovery/validation_tests.rs | 154 +++++++++--------- .../plotnik-lib/src/query/alt_kinds_tests.rs | 10 +- .../plotnik-lib/src/query/recursion_tests.rs | 6 - crates/plotnik-lib/src/query/shapes_tests.rs | 4 +- .../src/query/symbol_table_tests.rs | 13 +- 12 files changed, 193 insertions(+), 207 deletions(-) diff --git a/crates/plotnik-lib/src/diagnostics/printer.rs b/crates/plotnik-lib/src/diagnostics/printer.rs index 314c189a..45814829 100644 --- a/crates/plotnik-lib/src/diagnostics/printer.rs +++ b/crates/plotnik-lib/src/diagnostics/printer.rs @@ -50,11 +50,9 @@ impl<'a> DiagnosticsPrinter<'a> { for (i, diag) in self.diagnostics.iter().enumerate() { let range = adjust_range(diag.range, self.source.len()); - let mut snippet = Snippet::source(self.source).line_start(1).annotation( - AnnotationKind::Primary - .span(range.clone()) - .label(&diag.message), - ); + let mut snippet = Snippet::source(self.source) + .line_start(1) + .annotation(AnnotationKind::Primary.span(range.clone())); if let Some(p) = self.path { snippet = snippet.path(p); diff --git a/crates/plotnik-lib/src/diagnostics/tests.rs b/crates/plotnik-lib/src/diagnostics/tests.rs index d2e3d5a7..f6b24149 100644 --- a/crates/plotnik-lib/src/diagnostics/tests.rs +++ b/crates/plotnik-lib/src/diagnostics/tests.rs @@ -67,8 +67,6 @@ fn builder_with_related() { | 1 | hello world! | ^^^^^ ---- related info - | | - | unclosed tree: primary "); } @@ -89,7 +87,7 @@ fn builder_with_fix() { error: invalid field syntax: fixable | 1 | hello world - | ^^^^^ invalid field syntax: fixable + | ^^^^^ | help: apply this fix | @@ -119,9 +117,8 @@ fn builder_with_all_options() { | 1 | hello world stuff! | ^^^^^ ----- ----- and here - | | | - | | see also - | unclosed tree: main error + | | + | see also | help: try this | @@ -171,7 +168,7 @@ fn printer_with_path() { --> test.pql:1:1 | 1 | hello world - | ^^^^^ undefined reference: `test error` + | ^^^^^ "); } @@ -191,7 +188,7 @@ fn printer_zero_width_span() { error: expected expression: zero width error | 1 | hello - | ^ expected expression: zero width error + | ^ "); } @@ -213,8 +210,6 @@ fn printer_related_zero_width() { | 1 | hello world! | ^^^^^ - zero width related - | | - | unclosed tree: primary "); } @@ -241,11 +236,11 @@ fn printer_multiple_diagnostics() { error: unclosed tree: first error | 1 | hello world! - | ^^^^^ unclosed tree: first error + | ^^^^^ error: undefined reference: `second error` | 1 | hello world! - | ^^^^ undefined reference: `second error` + | ^^^^ "); } diff --git a/crates/plotnik-lib/src/parser/ast_tests.rs b/crates/plotnik-lib/src/parser/ast_tests.rs index 40c5bc7f..543a5c6d 100644 --- a/crates/plotnik-lib/src/parser/ast_tests.rs +++ b/crates/plotnik-lib/src/parser/ast_tests.rs @@ -261,12 +261,12 @@ fn complex_example() { fn ast_with_errors() { let query = Query::try_from("(call (Undefined))").unwrap(); assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_diagnostics(), @r#" + insta::assert_snapshot!(query.dump_diagnostics(), @r" error: undefined reference: `Undefined` | 1 | (call (Undefined)) - | ^^^^^^^^^ undefined reference: `Undefined` - "#); + | ^^^^^^^^^ + "); } #[test] 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 8ed5cf2f..eabe89ad 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs @@ -134,11 +134,11 @@ fn named_def_missing_equals_with_garbage() { error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | Expr ^^^ (identifier) - | ^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^^^^ error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | Expr ^^^ (identifier) - | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^ "#); } @@ -155,11 +155,11 @@ fn named_def_missing_equals_recovers_to_next_def() { error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | Broken ^^^ - | ^^^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^^^^^^ error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | Broken ^^^ - | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^ "#); } 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 964d8bd0..1284a3c7 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs @@ -13,7 +13,7 @@ fn missing_capture_name() { error: expected capture name: expected capture name | 1 | (identifier) @ - | ^ expected capture name: expected capture name + | ^ "); } @@ -29,7 +29,7 @@ fn missing_field_value() { error: expected expression: expected expression after field name | 1 | (call name:) - | ^ expected expression: expected expression after field name + | ^ "); } @@ -43,7 +43,7 @@ fn named_def_eof_after_equals() { error: expected expression: expected expression after '=' in named definition | 1 | Expr = - | ^ expected expression: expected expression after '=' in named definition + | ^ "); } @@ -59,7 +59,7 @@ fn missing_type_name() { error: expected type name: expected type name after '::' (e.g., ::MyType or ::string) | 1 | (identifier) @name :: - | ^ expected type name: expected type name after '::' (e.g., ::MyType or ::string) + | ^ "); } @@ -75,7 +75,7 @@ fn missing_negated_field_name() { error: expected field name: expected field name after '!' (e.g., !value) | 1 | (call !) - | ^ expected field name: expected field name after '!' (e.g., !value) + | ^ "); } @@ -91,7 +91,7 @@ fn missing_subtype() { error: expected subtype: expected subtype after '/' (e.g., expression/binary_expression) | 1 | (expression/) - | ^ expected subtype: expected subtype after '/' (e.g., expression/binary_expression) + | ^ "); } @@ -107,7 +107,7 @@ fn tagged_branch_missing_expression() { error: expected expression: expected expression after branch label | 1 | [Label:] - | ^ expected expression: expected expression after branch label + | ^ "); } @@ -121,7 +121,7 @@ fn type_annotation_missing_name_at_eof() { error: expected type name: expected type name after '::' (e.g., ::MyType or ::string) | 1 | (a) @x :: - | ^ expected type name: expected type name after '::' (e.g., ::MyType or ::string) + | ^ "); } @@ -135,7 +135,7 @@ fn type_annotation_missing_name_with_bracket() { error: expected type name: expected type name after '::' (e.g., ::MyType or ::string) | 1 | [(a) @x :: ] - | ^ expected type name: expected type name after '::' (e.g., ::MyType or ::string) + | ^ "); } @@ -151,17 +151,17 @@ fn type_annotation_invalid_token_after() { error: expected type name: expected type name after '::' (e.g., ::MyType or ::string) | 1 | (identifier) @name :: ( - | ^ expected type name: expected type name after '::' (e.g., ::MyType or ::string) + | ^ error: unclosed tree: unclosed tree; expected ')' | 1 | (identifier) @name :: ( - | -^ unclosed tree: unclosed tree; expected ')' + | -^ | | | tree started here error: unnamed definition must be last: add a name: `Name = (identifier) @name ::` | 1 | (identifier) @name :: ( - | ^^^^^^^^^^^^^^^^^^^^^ unnamed definition must be last: add a name: `Name = (identifier) @name ::` + | ^^^^^^^^^^^^^^^^^^^^^ "); } @@ -177,7 +177,7 @@ fn field_value_is_garbage() { error: expected expression: expected expression after field name | 1 | (call name: %%%) - | ^^^ expected expression: expected expression after field name + | ^^^ "); } @@ -193,7 +193,7 @@ fn capture_with_invalid_char() { error: expected capture name: expected capture name | 1 | (identifier) @123 - | ^^^ expected capture name: expected capture name + | ^^^ "); } @@ -207,7 +207,7 @@ fn bare_capture_at_eof_triggers_sync() { error: capture without target: capture without target | 1 | @ - | ^ capture without target: capture without target + | ^ "); } @@ -223,11 +223,11 @@ fn bare_capture_at_root() { error: capture without target: capture without target | 1 | @name - | ^ capture without target: capture without target + | ^ error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | @name - | ^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^^^^ "); } @@ -243,11 +243,11 @@ fn capture_at_start_of_alternation() { error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | [@x (a)] - | ^ unexpected token: unexpected token; expected a child expression or closing delimiter + | ^ error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | [@x (a)] - | ^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^ "); } @@ -263,15 +263,15 @@ fn mixed_valid_invalid_captures() { error: capture without target: capture without target | 1 | (a) @ok @ @name - | ^ capture without target: capture without target + | ^ error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (a) @ok @ @name - | ^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^^^^ error: unnamed definition must be last: add a name: `Name = (a) @ok` | 1 | (a) @ok @ @name - | ^^^^^^^ unnamed definition must be last: add a name: `Name = (a) @ok` + | ^^^^^^^ "); } @@ -287,7 +287,7 @@ fn field_equals_typo_missing_value() { error: invalid field syntax: '=' is not valid for field constraints | 1 | (call name = ) - | ^ invalid field syntax: '=' is not valid for field constraints + | ^ | help: use ':' | @@ -297,7 +297,7 @@ fn field_equals_typo_missing_value() { error: expected expression: expected expression after field name | 1 | (call name = ) - | ^ expected expression: expected expression after field name + | ^ "); } @@ -311,7 +311,7 @@ fn lowercase_branch_label_missing_expression() { error: lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) | 1 | [label:] - | ^^^^^ lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) + | ^^^^^ | help: capitalize as `Label` | @@ -321,6 +321,6 @@ fn lowercase_branch_label_missing_expression() { error: expected expression: expected expression after branch label | 1 | [label:] - | ^ expected expression: expected expression after branch label + | ^ "); } diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs index c6f2dee8..8a313c2c 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs @@ -13,7 +13,7 @@ fn missing_paren() { error: unclosed tree: unclosed tree; expected ')' | 1 | (identifier - | - ^ unclosed tree: unclosed tree; expected ')' + | - ^ | | | tree started here "); @@ -31,7 +31,7 @@ fn missing_bracket() { error: unclosed alternation: unclosed alternation; expected ']' | 1 | [(identifier) (string) - | - ^ unclosed alternation: unclosed alternation; expected ']' + | - ^ | | | alternation started here "); @@ -49,7 +49,7 @@ fn missing_brace() { error: unclosed sequence: unclosed sequence; expected '}' | 1 | {(a) (b) - | - ^ unclosed sequence: unclosed sequence; expected '}' + | - ^ | | | sequence started here "); @@ -67,7 +67,7 @@ fn nested_unclosed() { error: unclosed tree: unclosed tree; expected ')' | 1 | (a (b (c) - | - ^ unclosed tree: unclosed tree; expected ')' + | - ^ | | | tree started here "); @@ -85,7 +85,7 @@ fn deeply_nested_unclosed() { error: unclosed tree: unclosed tree; expected ')' | 1 | (a (b (c (d - | - ^ unclosed tree: unclosed tree; expected ')' + | - ^ | | | tree started here "); @@ -103,7 +103,7 @@ fn unclosed_alternation_nested() { error: unclosed tree: unclosed tree; expected ')' | 1 | [(a) (b - | - ^ unclosed tree: unclosed tree; expected ')' + | - ^ | | | tree started here "); @@ -121,7 +121,7 @@ fn empty_parens() { error: empty tree expression: empty tree expression | 1 | () - | ^ empty tree expression: empty tree expression + | ^ "); } @@ -140,7 +140,7 @@ fn unclosed_tree_shows_open_location() { 1 | (call | - tree started here 2 | (identifier) - | ^ unclosed tree: unclosed tree; expected ')' + | ^ "); } @@ -161,7 +161,7 @@ fn unclosed_alternation_shows_open_location() { | - alternation started here 2 | (a) 3 | (b) - | ^ unclosed alternation: unclosed alternation; expected ']' + | ^ "); } @@ -182,7 +182,7 @@ fn unclosed_sequence_shows_open_location() { | - sequence started here 2 | (a) 3 | (b) - | ^ unclosed sequence: unclosed sequence; expected '}' + | ^ "); } @@ -196,11 +196,11 @@ fn unclosed_double_quote_string() { error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | (call "foo) - | ^^^^^ unexpected token: unexpected token; expected a child expression or closing delimiter + | ^^^^^ error: unclosed tree: unclosed tree; expected ')' | 1 | (call "foo) - | - ^ unclosed tree: unclosed tree; expected ')' + | - ^ | | | tree started here "#); @@ -216,11 +216,11 @@ fn unclosed_single_quote_string() { error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | (call 'foo) - | ^^^^^ unexpected token: unexpected token; expected a child expression or closing delimiter + | ^^^^^ error: unclosed tree: unclosed tree; expected ')' | 1 | (call 'foo) - | - ^ unclosed tree: unclosed tree; expected ')' + | - ^ | | | tree started here "); diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs index f6208138..742f27a5 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs @@ -13,11 +13,11 @@ fn unexpected_token() { error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | (identifier) ^^^ (string) - | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^ error: unnamed definition must be last: add a name: `Name = (identifier)` | 1 | (identifier) ^^^ (string) - | ^^^^^^^^^^^^ unnamed definition must be last: add a name: `Name = (identifier)` + | ^^^^^^^^^^^^ "#); } @@ -33,7 +33,7 @@ fn multiple_consecutive_garbage() { error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | ^^^ $$$ %%% (ok) - | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^ "#); } @@ -49,7 +49,7 @@ fn garbage_at_start() { error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | ^^^ (a) - | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^ "#); } @@ -65,7 +65,7 @@ fn only_garbage() { error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | ^^^ $$$ - | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^ "#); } @@ -81,7 +81,7 @@ fn garbage_inside_alternation() { error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | [(a) ^^^ (b)] - | ^^^ unexpected token: unexpected token; expected a child expression or closing delimiter + | ^^^ "); } @@ -97,15 +97,15 @@ fn garbage_inside_node() { error: expected capture name: expected capture name | 1 | (a (b) @@@ (c)) (d) - | ^ expected capture name: expected capture name + | ^ error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | (a (b) @@@ (c)) (d) - | ^ unexpected token: unexpected token; expected a child expression or closing delimiter + | ^ error: unnamed definition must be last: add a name: `Name = (a (b) @@@ (c))` | 1 | (a (b) @@@ (c)) (d) - | ^^^^^^^^^^^^^^^ unnamed definition must be last: add a name: `Name = (a (b) @@@ (c))` + | ^^^^^^^^^^^^^^^ "); } @@ -121,11 +121,11 @@ fn xml_tag_garbage() { error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 |
(identifier)
- | ^^^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^^^ error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 |
(identifier)
- | ^^^^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^^^^ "#); } @@ -141,7 +141,7 @@ fn xml_self_closing() { error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 |
(a) - | ^^^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^^^ "#); } @@ -157,19 +157,19 @@ fn predicate_unsupported() { error: unsupported predicate: unsupported predicate | 1 | (a (#eq? @x "foo") b) - | ^^^^ unsupported predicate: unsupported predicate + | ^^^^ error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | (a (#eq? @x "foo") b) - | ^ unexpected token: unexpected token; expected a child expression or closing delimiter + | ^ error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (a (#eq? @x "foo") b) - | ^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^ error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (a (#eq? @x "foo") b) - | ^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^ "#); } @@ -185,15 +185,15 @@ fn predicate_match() { error: unsupported predicate: unsupported predicate | 1 | (identifier) #match? @name "test" - | ^^^^^^^ unsupported predicate: unsupported predicate + | ^^^^^^^ error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (identifier) #match? @name "test" - | ^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^^^^ error: unnamed definition must be last: add a name: `Name = (identifier)` | 1 | (identifier) #match? @name "test" - | ^^^^^^^^^^^^ unnamed definition must be last: add a name: `Name = (identifier)` + | ^^^^^^^^^^^^ "#); } @@ -207,15 +207,15 @@ fn predicate_in_tree() { error: unsupported predicate: unsupported predicate | 1 | (function #eq? @name "test") - | ^^^^ unsupported predicate: unsupported predicate + | ^^^^ error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | (function #eq? @name "test") - | ^ unexpected token: unexpected token; expected a child expression or closing delimiter + | ^ error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (function #eq? @name "test") - | ^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^^^^ "#); } @@ -231,7 +231,7 @@ fn predicate_in_alternation() { error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | [(a) #eq? (b)] - | ^^^^ unexpected token: unexpected token; expected a child expression or closing delimiter + | ^^^^ "); } @@ -247,7 +247,7 @@ fn predicate_in_sequence() { error: unsupported predicate: unsupported predicate | 1 | {(a) #set! (b)} - | ^^^^^ unsupported predicate: unsupported predicate + | ^^^^^ "); } @@ -265,11 +265,11 @@ fn multiline_garbage_recovery() { error: unexpected token: unexpected token; expected a child expression or closing delimiter | 2 | ^^^ - | ^^^ unexpected token: unexpected token; expected a child expression or closing delimiter + | ^^^ error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 3 | b) - | ^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^ "); } @@ -285,7 +285,7 @@ fn top_level_garbage_recovery() { error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | Expr = (a) ^^^ Expr2 = (b) - | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^ "#); } @@ -305,11 +305,11 @@ fn multiple_definitions_with_garbage_between() { error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 2 | ^^^ - | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^ error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 4 | $$$ - | ^^^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^^^ "#); } @@ -325,15 +325,15 @@ fn alternation_recovery_to_capture() { error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | [^^^ @name] - | ^^^ unexpected token: unexpected token; expected a child expression or closing delimiter + | ^^^ error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | [^^^ @name] - | ^ unexpected token: unexpected token; expected a child expression or closing delimiter + | ^ error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | [^^^ @name] - | ^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^^^^ "); } @@ -349,7 +349,7 @@ fn comma_between_defs() { error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | A = (a), B = (b) - | ^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^ "#); } @@ -363,7 +363,7 @@ fn bare_colon_in_tree() { error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | (a : (b)) - | ^ unexpected token: unexpected token; expected a child expression or closing delimiter + | ^ "); } @@ -377,15 +377,15 @@ fn paren_close_inside_alternation() { error: unexpected token: expected closing ']' for alternation | 1 | [(a) ) (b)] - | ^ unexpected token: expected closing ']' for alternation + | ^ error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | [(a) ) (b)] - | ^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^ error: unnamed definition must be last: add a name: `Name = [(a)` | 1 | [(a) ) (b)] - | ^^^^ unnamed definition must be last: add a name: `Name = [(a)` + | ^^^^ "#); } @@ -399,15 +399,15 @@ fn bracket_close_inside_sequence() { error: unexpected token: expected closing '}' for sequence | 1 | {(a) ] (b)} - | ^ unexpected token: expected closing '}' for sequence + | ^ error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | {(a) ] (b)} - | ^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^ error: unnamed definition must be last: add a name: `Name = {(a)` | 1 | {(a) ] (b)} - | ^^^^ unnamed definition must be last: add a name: `Name = {(a)` + | ^^^^ "#); } @@ -421,15 +421,15 @@ fn paren_close_inside_sequence() { error: unexpected token: expected closing '}' for sequence | 1 | {(a) ) (b)} - | ^ unexpected token: expected closing '}' for sequence + | ^ error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | {(a) ) (b)} - | ^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^ error: unnamed definition must be last: add a name: `Name = {(a)` | 1 | {(a) ) (b)} - | ^^^^ unnamed definition must be last: add a name: `Name = {(a)` + | ^^^^ "#); } @@ -443,11 +443,11 @@ fn single_colon_type_annotation_followed_by_non_id() { error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | (a) @x : (b) - | ^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^ error: unnamed definition must be last: add a name: `Name = (a) @x` | 1 | (a) @x : (b) - | ^^^^^^ unnamed definition must be last: add a name: `Name = (a) @x` + | ^^^^^^ "#); } @@ -461,6 +461,6 @@ fn single_colon_type_annotation_at_eof() { error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | (a) @x : - | ^ unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + | ^ "#); } 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 47e6a9b7..0dbf6bad 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs @@ -14,7 +14,7 @@ fn ref_with_children_error() { error: reference `Expr` cannot contain children | 2 | (Expr (child)) - | ^^^^^^^ reference `Expr` cannot contain children + | ^^^^^^^ "); } @@ -31,7 +31,7 @@ fn ref_with_multiple_children_error() { error: reference `Expr` cannot contain children | 2 | (Expr (a) (b) @cap) - | ^^^^^^^^^^^^ reference `Expr` cannot contain children + | ^^^^^^^^^^^^ "); } @@ -48,7 +48,7 @@ fn ref_with_field_children_error() { error: reference `Expr` cannot contain children | 2 | (Expr name: (identifier)) - | ^^^^^^^^^^^^^^^^^^ reference `Expr` cannot contain children + | ^^^^^^^^^^^^^^^^^^ "); } @@ -62,7 +62,7 @@ fn reference_with_supertype_syntax_error() { error: invalid supertype syntax: invalid supertype syntax | 1 | (RefName/subtype) - | ^ invalid supertype syntax: invalid supertype syntax + | ^ "); } @@ -78,7 +78,7 @@ fn mixed_tagged_and_untagged() { error: mixed tagged and untagged branches in alternation | 1 | [Tagged: (a) (b) Another: (c)] - | ------ ^^^ mixed tagged and untagged branches in alternation + | ------ ^^^ | | | tagged branch here "); @@ -96,7 +96,7 @@ fn error_with_unexpected_content() { error: (ERROR) takes no arguments: (ERROR) takes no arguments | 1 | (ERROR (something)) - | ^ (ERROR) takes no arguments: (ERROR) takes no arguments + | ^ "); } @@ -112,7 +112,7 @@ fn bare_error_keyword() { error: ERROR/MISSING outside parentheses: ERROR/MISSING outside parentheses | 1 | ERROR - | ^^^^^ ERROR/MISSING outside parentheses: ERROR/MISSING outside parentheses + | ^^^^^ "); } @@ -128,7 +128,7 @@ fn bare_missing_keyword() { error: ERROR/MISSING outside parentheses: ERROR/MISSING outside parentheses | 1 | MISSING - | ^^^^^^^ ERROR/MISSING outside parentheses: ERROR/MISSING outside parentheses + | ^^^^^^^ "); } @@ -144,11 +144,11 @@ fn upper_ident_in_alternation_not_followed_by_colon() { error: undefined reference: `Expr` | 1 | [(Expr) (Statement)] - | ^^^^ undefined reference: `Expr` + | ^^^^ error: undefined reference: `Statement` | 1 | [(Expr) (Statement)] - | ^^^^^^^^^ undefined reference: `Statement` + | ^^^^^^^^^ "); } @@ -164,7 +164,7 @@ fn upper_ident_not_followed_by_equals_is_expression() { error: undefined reference: `Expr` | 1 | (Expr) - | ^^^^ undefined reference: `Expr` + | ^^^^ "); } @@ -180,7 +180,7 @@ fn bare_upper_ident_not_followed_by_equals_is_error() { error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | Expr - | ^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^^^^ "); } @@ -196,7 +196,7 @@ fn named_def_missing_equals() { error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | Expr (identifier) - | ^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^^^^ "); } @@ -214,7 +214,7 @@ fn unnamed_def_not_allowed_in_middle() { error: unnamed definition must be last: add a name: `Name = (first)` | 1 | (first) - | ^^^^^^^ unnamed definition must be last: add a name: `Name = (first)` + | ^^^^^^^ "); } @@ -232,11 +232,11 @@ fn multiple_unnamed_defs_errors_for_all_but_last() { error: unnamed definition must be last: add a name: `Name = (first)` | 1 | (first) - | ^^^^^^^ unnamed definition must be last: add a name: `Name = (first)` + | ^^^^^^^ error: unnamed definition must be last: add a name: `Name = (second)` | 2 | (second) - | ^^^^^^^^ unnamed definition must be last: add a name: `Name = (second)` + | ^^^^^^^^ "); } @@ -252,11 +252,11 @@ fn capture_space_after_dot_is_anchor() { error: unnamed definition must be last: add a name: `Name = (identifier) @foo` | 1 | (identifier) @foo . (other) - | ^^^^^^^^^^^^^^^^^ unnamed definition must be last: add a name: `Name = (identifier) @foo` + | ^^^^^^^^^^^^^^^^^ error: unnamed definition must be last: add a name: `Name = .` | 1 | (identifier) @foo . (other) - | ^ unnamed definition must be last: add a name: `Name = .` + | ^ "); } @@ -270,7 +270,7 @@ fn def_name_lowercase_error() { error: definition name starts with lowercase: definition names must start with uppercase | 1 | lowercase = (x) - | ^^^^^^^^^ definition name starts with lowercase: definition names must start with uppercase + | ^^^^^^^^^ | help: definition names must be PascalCase; use Lowercase instead | @@ -292,7 +292,7 @@ fn def_name_snake_case_suggests_pascal() { error: definition name starts with lowercase: definition names must start with uppercase | 1 | my_expr = (identifier) - | ^^^^^^^ definition name starts with lowercase: definition names must start with uppercase + | ^^^^^^^ | help: definition names must be PascalCase; use MyExpr instead | @@ -314,7 +314,7 @@ fn def_name_kebab_case_suggests_pascal() { error: definition name starts with lowercase: definition names must start with uppercase | 1 | my-expr = (identifier) - | ^^^^^^^ definition name starts with lowercase: definition names must start with uppercase + | ^^^^^^^ | help: definition names must be PascalCase; use MyExpr instead | @@ -336,7 +336,7 @@ fn def_name_dotted_suggests_pascal() { error: definition name starts with lowercase: definition names must start with uppercase | 1 | my.expr = (identifier) - | ^^^^^^^ definition name starts with lowercase: definition names must start with uppercase + | ^^^^^^^ | help: definition names must be PascalCase; use MyExpr instead | @@ -356,7 +356,7 @@ fn def_name_with_underscores_error() { error: definition name contains separators: definition names cannot contain separators | 1 | Some_Thing = (x) - | ^^^^^^^^^^ definition name contains separators: definition names cannot contain separators + | ^^^^^^^^^^ | help: definition names must be PascalCase; use SomeThing instead | @@ -376,7 +376,7 @@ fn def_name_with_hyphens_error() { error: definition name contains separators: definition names cannot contain separators | 1 | Some-Thing = (x) - | ^^^^^^^^^^ definition name contains separators: definition names cannot contain separators + | ^^^^^^^^^^ | help: definition names must be PascalCase; use SomeThing instead | @@ -398,7 +398,7 @@ fn capture_name_pascal_case_error() { error: capture name starts with uppercase: capture names must start with lowercase | 1 | (a) @Name - | ^^^^ capture name starts with uppercase: capture names must start with lowercase + | ^^^^ | help: capture names must be snake_case; use @name instead | @@ -420,7 +420,7 @@ fn capture_name_pascal_case_with_hyphens_error() { error: capture name contains hyphens: capture names cannot contain hyphens | 1 | (a) @My-Name - | ^^^^^^^ capture name contains hyphens: capture names cannot contain hyphens + | ^^^^^^^ | help: captures become struct fields; use @my_name instead | @@ -442,7 +442,7 @@ fn capture_name_with_hyphens_error() { error: capture name contains hyphens: capture names cannot contain hyphens | 1 | (a) @my-name - | ^^^^^^^ capture name contains hyphens: capture names cannot contain hyphens + | ^^^^^^^ | help: captures become struct fields; use @my_name instead | @@ -464,7 +464,7 @@ fn capture_dotted_error() { error: capture name contains dots: capture names cannot contain dots | 1 | (identifier) @foo.bar - | ^^^^^^^ capture name contains dots: capture names cannot contain dots + | ^^^^^^^ | help: captures become struct fields; use @foo_bar instead | @@ -486,7 +486,7 @@ fn capture_dotted_multiple_parts() { error: capture name contains dots: capture names cannot contain dots | 1 | (identifier) @foo.bar.baz - | ^^^^^^^^^^^ capture name contains dots: capture names cannot contain dots + | ^^^^^^^^^^^ | help: captures become struct fields; use @foo_bar_baz instead | @@ -508,7 +508,7 @@ fn capture_dotted_followed_by_field() { error: capture name contains dots: capture names cannot contain dots | 1 | (node) @foo.bar name: (other) - | ^^^^^^^ capture name contains dots: capture names cannot contain dots + | ^^^^^^^ | help: captures become struct fields; use @foo_bar instead | @@ -518,7 +518,7 @@ fn capture_dotted_followed_by_field() { error: unnamed definition must be last: add a name: `Name = (node) @foo.bar` | 1 | (node) @foo.bar name: (other) - | ^^^^^^^^^^^^^^^ unnamed definition must be last: add a name: `Name = (node) @foo.bar` + | ^^^^^^^^^^^^^^^ "); } @@ -534,7 +534,7 @@ fn capture_space_after_dot_breaks_chain() { error: capture name contains dots: capture names cannot contain dots | 1 | (identifier) @foo. bar - | ^^^^ capture name contains dots: capture names cannot contain dots + | ^^^^ | help: captures become struct fields; use @foo_ instead | @@ -544,11 +544,11 @@ fn capture_space_after_dot_breaks_chain() { error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (identifier) @foo. bar - | ^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^^^ error: unnamed definition must be last: add a name: `Name = (identifier) @foo.` | 1 | (identifier) @foo. bar - | ^^^^^^^^^^^^^^^^^^ unnamed definition must be last: add a name: `Name = (identifier) @foo.` + | ^^^^^^^^^^^^^^^^^^ "); } @@ -564,7 +564,7 @@ fn capture_hyphenated_error() { error: capture name contains hyphens: capture names cannot contain hyphens | 1 | (identifier) @foo-bar - | ^^^^^^^ capture name contains hyphens: capture names cannot contain hyphens + | ^^^^^^^ | help: captures become struct fields; use @foo_bar instead | @@ -586,7 +586,7 @@ fn capture_hyphenated_multiple() { error: capture name contains hyphens: capture names cannot contain hyphens | 1 | (identifier) @foo-bar-baz - | ^^^^^^^^^^^ capture name contains hyphens: capture names cannot contain hyphens + | ^^^^^^^^^^^ | help: captures become struct fields; use @foo_bar_baz instead | @@ -608,7 +608,7 @@ fn capture_mixed_dots_and_hyphens() { error: capture name contains dots: capture names cannot contain dots | 1 | (identifier) @foo.bar-baz - | ^^^^^^^^^^^ capture name contains dots: capture names cannot contain dots + | ^^^^^^^^^^^ | help: captures become struct fields; use @foo_bar_baz instead | @@ -630,7 +630,7 @@ fn field_name_pascal_case_error() { error: field name starts with uppercase: field names must start with lowercase | 1 | (call Name: (a)) - | ^^^^ field name starts with uppercase: field names must start with lowercase + | ^^^^ | help: field names must be snake_case; use name: instead | @@ -650,7 +650,7 @@ fn field_name_with_dots_error() { error: field name contains dots: field names cannot contain dots | 1 | (call foo.bar: (x)) - | ^^^^^^^ field name contains dots: field names cannot contain dots + | ^^^^^^^ | help: field names must be snake_case; use foo_bar: instead | @@ -670,7 +670,7 @@ fn field_name_with_hyphens_error() { error: field name contains hyphens: field names cannot contain hyphens | 1 | (call foo-bar: (x)) - | ^^^^^^^ field name contains hyphens: field names cannot contain hyphens + | ^^^^^^^ | help: field names must be snake_case; use foo_bar: instead | @@ -692,7 +692,7 @@ fn negated_field_with_upper_ident_parses() { error: field name starts with uppercase: field names must start with lowercase | 1 | (call !Arguments) - | ^^^^^^^^^ field name starts with uppercase: field names must start with lowercase + | ^^^^^^^^^ | help: field names must be snake_case; use arguments: instead | @@ -714,7 +714,7 @@ fn branch_label_snake_case_suggests_pascal() { error: branch label contains separators: branch labels cannot contain separators | 1 | [My_branch: (a) Other: (b)] - | ^^^^^^^^^ branch label contains separators: branch labels cannot contain separators + | ^^^^^^^^^ | help: branch labels must be PascalCase; use MyBranch: instead | @@ -736,7 +736,7 @@ fn branch_label_kebab_case_suggests_pascal() { error: branch label contains separators: branch labels cannot contain separators | 1 | [My-branch: (a) Other: (b)] - | ^^^^^^^^^ branch label contains separators: branch labels cannot contain separators + | ^^^^^^^^^ | help: branch labels must be PascalCase; use MyBranch: instead | @@ -758,7 +758,7 @@ fn branch_label_dotted_suggests_pascal() { error: branch label contains separators: branch labels cannot contain separators | 1 | [My.branch: (a) Other: (b)] - | ^^^^^^^^^ branch label contains separators: branch labels cannot contain separators + | ^^^^^^^^^ | help: branch labels must be PascalCase; use MyBranch: instead | @@ -778,7 +778,7 @@ fn branch_label_with_underscores_error() { error: branch label contains separators: branch labels cannot contain separators | 1 | [Some_Label: (x)] - | ^^^^^^^^^^ branch label contains separators: branch labels cannot contain separators + | ^^^^^^^^^^ | help: branch labels must be PascalCase; use SomeLabel: instead | @@ -798,7 +798,7 @@ fn branch_label_with_hyphens_error() { error: branch label contains separators: branch labels cannot contain separators | 1 | [Some-Label: (x)] - | ^^^^^^^^^^ branch label contains separators: branch labels cannot contain separators + | ^^^^^^^^^^ | help: branch labels must be PascalCase; use SomeLabel: instead | @@ -823,7 +823,7 @@ fn lowercase_branch_label() { error: lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) | 2 | left: (a) - | ^^^^ lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) + | ^^^^ | help: capitalize as `Left` | @@ -833,7 +833,7 @@ fn lowercase_branch_label() { error: lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) | 3 | right: (b) - | ^^^^^ lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) + | ^^^^^ | help: capitalize as `Right` | @@ -855,7 +855,7 @@ fn lowercase_branch_label_suggests_capitalized() { error: lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) | 1 | [first: (a) Second: (b)] - | ^^^^^ lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) + | ^^^^^ | help: capitalize as `First` | @@ -875,7 +875,7 @@ fn mixed_case_branch_labels() { error: lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) | 1 | [foo: (a) Bar: (b)] - | ^^^ lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) + | ^^^ | help: capitalize as `Foo` | @@ -897,7 +897,7 @@ fn type_annotation_dotted_suggests_pascal() { error: type name contains invalid characters: type names cannot contain dots or hyphens | 1 | (a) @x :: My.Type - | ^^^^^^^ type name contains invalid characters: type names cannot contain dots or hyphens + | ^^^^^^^ | help: type names cannot contain separators; use ::MyType instead | @@ -919,7 +919,7 @@ fn type_annotation_kebab_suggests_pascal() { error: type name contains invalid characters: type names cannot contain dots or hyphens | 1 | (a) @x :: My-Type - | ^^^^^^^ type name contains invalid characters: type names cannot contain dots or hyphens + | ^^^^^^^ | help: type names cannot contain separators; use ::MyType instead | @@ -939,7 +939,7 @@ fn type_name_with_dots_error() { error: type name contains invalid characters: type names cannot contain dots or hyphens | 1 | (x) @name :: Some.Type - | ^^^^^^^^^ type name contains invalid characters: type names cannot contain dots or hyphens + | ^^^^^^^^^ | help: type names cannot contain separators; use ::SomeType instead | @@ -959,7 +959,7 @@ fn type_name_with_hyphens_error() { error: type name contains invalid characters: type names cannot contain dots or hyphens | 1 | (x) @name :: Some-Type - | ^^^^^^^^^ type name contains invalid characters: type names cannot contain dots or hyphens + | ^^^^^^^^^ | help: type names cannot contain separators; use ::SomeType instead | @@ -979,7 +979,7 @@ fn comma_in_node_children() { error: invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation | 1 | (node (a), (b)) - | ^ invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation + | ^ | help: remove separator | @@ -999,7 +999,7 @@ fn comma_in_alternation() { error: invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a), (b), (c)] - | ^ invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation + | ^ | help: remove separator | @@ -1009,7 +1009,7 @@ fn comma_in_alternation() { error: invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a), (b), (c)] - | ^ invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation + | ^ | help: remove separator | @@ -1029,7 +1029,7 @@ fn comma_in_sequence() { error: invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation | 1 | {(a), (b)} - | ^ invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation + | ^ | help: remove separator | @@ -1049,7 +1049,7 @@ fn pipe_in_alternation() { error: invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a) | (b) | (c)] - | ^ invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation + | ^ | help: remove separator | @@ -1059,7 +1059,7 @@ fn pipe_in_alternation() { error: invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a) | (b) | (c)] - | ^ invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation + | ^ | help: remove separator | @@ -1081,7 +1081,7 @@ fn pipe_between_branches() { error: invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a) | (b)] - | ^ invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation + | ^ | help: remove separator | @@ -1101,7 +1101,7 @@ fn pipe_in_tree() { error: invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation | 1 | (a | b) - | ^ invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation + | ^ | help: remove separator | @@ -1111,7 +1111,7 @@ fn pipe_in_tree() { error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (a | b) - | ^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^ "); } @@ -1125,7 +1125,7 @@ fn pipe_in_sequence() { error: invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation | 1 | {(a) | (b)} - | ^ invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation + | ^ | help: remove separator | @@ -1145,7 +1145,7 @@ fn field_equals_typo() { error: invalid field syntax: '=' is not valid for field constraints | 1 | (node name = (identifier)) - | ^ invalid field syntax: '=' is not valid for field constraints + | ^ | help: use ':' | @@ -1165,7 +1165,7 @@ fn field_equals_typo_no_space() { error: invalid field syntax: '=' is not valid for field constraints | 1 | (node name=(identifier)) - | ^ invalid field syntax: '=' is not valid for field constraints + | ^ | help: use ':' | @@ -1185,7 +1185,7 @@ fn field_equals_typo_no_expression() { error: invalid field syntax: '=' is not valid for field constraints | 1 | (call name=) - | ^ invalid field syntax: '=' is not valid for field constraints + | ^ | help: use ':' | @@ -1195,7 +1195,7 @@ fn field_equals_typo_no_expression() { error: expected expression: expected expression after field name | 1 | (call name=) - | ^ expected expression: expected expression after field name + | ^ "); } @@ -1211,7 +1211,7 @@ fn field_equals_typo_in_tree() { error: invalid field syntax: '=' is not valid for field constraints | 1 | (call name = (identifier)) - | ^ invalid field syntax: '=' is not valid for field constraints + | ^ | help: use ':' | @@ -1231,7 +1231,7 @@ fn single_colon_type_annotation() { error: invalid type annotation: single colon is not valid for type annotations | 1 | (identifier) @name : Type - | ^ invalid type annotation: single colon is not valid for type annotations + | ^ | help: use '::' | @@ -1250,7 +1250,7 @@ fn single_colon_type_annotation_no_space() { error: invalid type annotation: single colon is not valid for type annotations | 1 | (identifier) @name:Type - | ^ invalid type annotation: single colon is not valid for type annotations + | ^ | help: use '::' | @@ -1271,7 +1271,7 @@ fn single_colon_type_annotation_with_space() { error: invalid type annotation: single colon is not valid for type annotations | 1 | (a) @x : Type - | ^ invalid type annotation: single colon is not valid for type annotations + | ^ | help: use '::' | @@ -1290,22 +1290,22 @@ fn single_colon_primitive_type() { error: capture without target: capture without target | 1 | @val : string - | ^ capture without target: capture without target + | ^ error: unexpected token: expected ':' to separate field name from its value | 1 | @val : string - | ^ unexpected token: expected ':' to separate field name from its value + | ^ error: expected expression: expected expression after field name | 1 | @val : string - | ^ expected expression: expected expression after field name + | ^ error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | @val : string - | ^^^^^^ bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + | ^^^^^^ error: unnamed definition must be last: add a name: `Name = val` | 1 | @val : string - | ^^^ unnamed definition must be last: add a name: `Name = val` + | ^^^ "); } diff --git a/crates/plotnik-lib/src/query/alt_kinds_tests.rs b/crates/plotnik-lib/src/query/alt_kinds_tests.rs index 4c1ee966..1a36a34f 100644 --- a/crates/plotnik-lib/src/query/alt_kinds_tests.rs +++ b/crates/plotnik-lib/src/query/alt_kinds_tests.rs @@ -38,7 +38,7 @@ fn mixed_alternation_tagged_first() { error: mixed tagged and untagged branches in alternation | 1 | [A: (a) (b)] - | - ^^^ mixed tagged and untagged branches in alternation + | - ^^^ | | | tagged branch here "); @@ -60,7 +60,7 @@ fn mixed_alternation_untagged_first() { error: mixed tagged and untagged branches in alternation | 3 | (a) - | ^^^ mixed tagged and untagged branches in alternation + | ^^^ 4 | B: (b) | - tagged branch here "); @@ -74,7 +74,7 @@ fn nested_mixed_alternation() { error: mixed tagged and untagged branches in alternation | 1 | (call [A: (a) (b)]) - | - ^^^ mixed tagged and untagged branches in alternation + | - ^^^ | | | tagged branch here "); @@ -88,13 +88,13 @@ fn multiple_mixed_alternations() { error: mixed tagged and untagged branches in alternation | 1 | (foo [A: (a) (b)] [C: (c) (d)]) - | - ^^^ mixed tagged and untagged branches in alternation + | - ^^^ | | | tagged branch here error: mixed tagged and untagged branches in alternation | 1 | (foo [A: (a) (b)] [C: (c) (d)]) - | - ^^^ mixed tagged and untagged branches in alternation + | - ^^^ | | | tagged branch here "); diff --git a/crates/plotnik-lib/src/query/recursion_tests.rs b/crates/plotnik-lib/src/query/recursion_tests.rs index 59b14b5b..24089a66 100644 --- a/crates/plotnik-lib/src/query/recursion_tests.rs +++ b/crates/plotnik-lib/src/query/recursion_tests.rs @@ -29,7 +29,6 @@ fn no_escape_via_plus() { 1 | E = (call (E)+) | ^ | | - | recursive pattern can never match: cycle `E` → `E` has no escape path | `E` references itself "); } @@ -57,7 +56,6 @@ fn recursion_in_tree_child() { 1 | E = (call (E)) | ^ | | - | recursive pattern can never match: cycle `E` → `E` has no escape path | `E` references itself "); } @@ -105,7 +103,6 @@ fn mutual_recursion_no_escape() { 2 | B = (bar (A)) | ^ | | - | recursive pattern can never match: cycle `B` → `A` → `B` has no escape path | `B` references `A` "); } @@ -172,7 +169,6 @@ fn cycle_ref_in_field() { 2 | B = (bar (A)) | ^ | | - | recursive pattern can never match: cycle `B` → `A` → `B` has no escape path | `B` references `A` "); } @@ -193,7 +189,6 @@ fn cycle_ref_in_capture() { 2 | B = (bar (A)) | ^ | | - | recursive pattern can never match: cycle `B` → `A` → `B` has no escape path | `B` references `A` "); } @@ -214,7 +209,6 @@ fn cycle_ref_in_sequence() { 2 | B = (bar (A)) | ^ | | - | recursive pattern can never match: cycle `B` → `A` → `B` has no escape path | `B` references `A` "); } diff --git a/crates/plotnik-lib/src/query/shapes_tests.rs b/crates/plotnik-lib/src/query/shapes_tests.rs index d76a469d..36357dc4 100644 --- a/crates/plotnik-lib/src/query/shapes_tests.rs +++ b/crates/plotnik-lib/src/query/shapes_tests.rs @@ -171,7 +171,7 @@ fn field_with_seq_error() { error: field `name` value must be a single node | 1 | (call name: {(a) (b)}) - | ^^^^^^^^^ field `name` value must be a single node + | ^^^^^^^^^ "); } @@ -198,7 +198,7 @@ fn field_with_ref_to_seq_error() { error: field `name` value must be a single node | 2 | (call name: (X)) - | ^^^ field `name` value must be a single node + | ^^^ "); } diff --git a/crates/plotnik-lib/src/query/symbol_table_tests.rs b/crates/plotnik-lib/src/query/symbol_table_tests.rs index e512f798..0ebb00a4 100644 --- a/crates/plotnik-lib/src/query/symbol_table_tests.rs +++ b/crates/plotnik-lib/src/query/symbol_table_tests.rs @@ -52,7 +52,7 @@ fn undefined_reference() { error: undefined reference: `Undefined` | 1 | Call = (call_expression function: (Undefined)) - | ^^^^^^^^^ undefined reference: `Undefined` + | ^^^^^^^^^ "); } @@ -85,7 +85,6 @@ fn mutual_recursion() { 2 | B = (bar (A)) | ^ | | - | recursive pattern can never match: cycle `B` → `A` → `B` has no escape path | `B` references `A` "); } @@ -103,7 +102,7 @@ fn duplicate_definition() { error: duplicate definition: `Expr` | 2 | Expr = (other) - | ^^^^ duplicate definition: `Expr` + | ^^^^ "); } @@ -193,7 +192,7 @@ fn entry_point_undefined_reference() { error: undefined reference: `Unknown` | 1 | (call function: (Unknown)) - | ^^^^^^^ undefined reference: `Unknown` + | ^^^^^^^ "); } @@ -241,15 +240,15 @@ fn multiple_undefined() { error: undefined reference: `X` | 1 | (foo (X) (Y) (Z)) - | ^ undefined reference: `X` + | ^ error: undefined reference: `Y` | 1 | (foo (X) (Y) (Z)) - | ^ undefined reference: `Y` + | ^ error: undefined reference: `Z` | 1 | (foo (X) (Y) (Z)) - | ^ undefined reference: `Z` + | ^ "); } From d618d6f959da8e84f5e2c663747cde9472e270c9 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 6 Dec 2025 10:50:29 -0300 Subject: [PATCH 09/15] Newline in-between two messages --- crates/plotnik-lib/src/diagnostics/printer.rs | 2 +- crates/plotnik-lib/src/diagnostics/tests.rs | 1 + .../parser/tests/recovery/coverage_tests.rs | 2 ++ .../parser/tests/recovery/incomplete_tests.rs | 8 +++++++ .../parser/tests/recovery/unclosed_tests.rs | 2 ++ .../parser/tests/recovery/unexpected_tests.rs | 22 +++++++++++++++++++ .../parser/tests/recovery/validation_tests.rs | 15 +++++++++++++ .../plotnik-lib/src/query/alt_kinds_tests.rs | 1 + .../src/query/symbol_table_tests.rs | 2 ++ 9 files changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/plotnik-lib/src/diagnostics/printer.rs b/crates/plotnik-lib/src/diagnostics/printer.rs index 45814829..4c86a424 100644 --- a/crates/plotnik-lib/src/diagnostics/printer.rs +++ b/crates/plotnik-lib/src/diagnostics/printer.rs @@ -82,7 +82,7 @@ impl<'a> DiagnosticsPrinter<'a> { } if i > 0 { - w.write_char('\n')?; + w.write_str("\n\n")?; } write!(w, "{}", renderer.render(&report))?; } diff --git a/crates/plotnik-lib/src/diagnostics/tests.rs b/crates/plotnik-lib/src/diagnostics/tests.rs index f6b24149..a2943cd9 100644 --- a/crates/plotnik-lib/src/diagnostics/tests.rs +++ b/crates/plotnik-lib/src/diagnostics/tests.rs @@ -237,6 +237,7 @@ fn printer_multiple_diagnostics() { | 1 | hello world! | ^^^^^ + error: undefined reference: `second error` | 1 | hello world! 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 eabe89ad..4f93b76d 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs @@ -135,6 +135,7 @@ fn named_def_missing_equals_with_garbage() { | 1 | Expr ^^^ (identifier) | ^^^^ + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | Expr ^^^ (identifier) @@ -156,6 +157,7 @@ fn named_def_missing_equals_recovers_to_next_def() { | 1 | Broken ^^^ | ^^^^^^ + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | Broken ^^^ 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 1284a3c7..b52df9de 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs @@ -152,12 +152,14 @@ fn type_annotation_invalid_token_after() { | 1 | (identifier) @name :: ( | ^ + error: unclosed tree: unclosed tree; expected ')' | 1 | (identifier) @name :: ( | -^ | | | tree started here + error: unnamed definition must be last: add a name: `Name = (identifier) @name ::` | 1 | (identifier) @name :: ( @@ -224,6 +226,7 @@ fn bare_capture_at_root() { | 1 | @name | ^ + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | @name @@ -244,6 +247,7 @@ fn capture_at_start_of_alternation() { | 1 | [@x (a)] | ^ + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | [@x (a)] @@ -264,10 +268,12 @@ fn mixed_valid_invalid_captures() { | 1 | (a) @ok @ @name | ^ + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (a) @ok @ @name | ^^^^ + error: unnamed definition must be last: add a name: `Name = (a) @ok` | 1 | (a) @ok @ @name @@ -294,6 +300,7 @@ fn field_equals_typo_missing_value() { 1 - (call name = ) 1 + (call name : ) | + error: expected expression: expected expression after field name | 1 | (call name = ) @@ -318,6 +325,7 @@ fn lowercase_branch_label_missing_expression() { 1 - [label:] 1 + [Label:] | + error: expected expression: expected expression after branch label | 1 | [label:] diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs index 8a313c2c..2e917a43 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs @@ -197,6 +197,7 @@ fn unclosed_double_quote_string() { | 1 | (call "foo) | ^^^^^ + error: unclosed tree: unclosed tree; expected ')' | 1 | (call "foo) @@ -217,6 +218,7 @@ fn unclosed_single_quote_string() { | 1 | (call 'foo) | ^^^^^ + error: unclosed tree: unclosed tree; expected ')' | 1 | (call 'foo) diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs index 742f27a5..95404e07 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs @@ -14,6 +14,7 @@ fn unexpected_token() { | 1 | (identifier) ^^^ (string) | ^^^ + error: unnamed definition must be last: add a name: `Name = (identifier)` | 1 | (identifier) ^^^ (string) @@ -98,10 +99,12 @@ fn garbage_inside_node() { | 1 | (a (b) @@@ (c)) (d) | ^ + error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | (a (b) @@@ (c)) (d) | ^ + error: unnamed definition must be last: add a name: `Name = (a (b) @@@ (c))` | 1 | (a (b) @@@ (c)) (d) @@ -122,6 +125,7 @@ fn xml_tag_garbage() { | 1 |
(identifier)
| ^^^^^ + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 |
(identifier)
@@ -158,14 +162,17 @@ fn predicate_unsupported() { | 1 | (a (#eq? @x "foo") b) | ^^^^ + error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | (a (#eq? @x "foo") b) | ^ + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (a (#eq? @x "foo") b) | ^ + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (a (#eq? @x "foo") b) @@ -186,10 +193,12 @@ fn predicate_match() { | 1 | (identifier) #match? @name "test" | ^^^^^^^ + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (identifier) #match? @name "test" | ^^^^ + error: unnamed definition must be last: add a name: `Name = (identifier)` | 1 | (identifier) #match? @name "test" @@ -208,10 +217,12 @@ fn predicate_in_tree() { | 1 | (function #eq? @name "test") | ^^^^ + error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | (function #eq? @name "test") | ^ + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (function #eq? @name "test") @@ -266,6 +277,7 @@ fn multiline_garbage_recovery() { | 2 | ^^^ | ^^^ + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 3 | b) @@ -306,6 +318,7 @@ fn multiple_definitions_with_garbage_between() { | 2 | ^^^ | ^^^ + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 4 | $$$ @@ -326,10 +339,12 @@ fn alternation_recovery_to_capture() { | 1 | [^^^ @name] | ^^^ + error: unexpected token: unexpected token; expected a child expression or closing delimiter | 1 | [^^^ @name] | ^ + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | [^^^ @name] @@ -378,10 +393,12 @@ fn paren_close_inside_alternation() { | 1 | [(a) ) (b)] | ^ + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | [(a) ) (b)] | ^ + error: unnamed definition must be last: add a name: `Name = [(a)` | 1 | [(a) ) (b)] @@ -400,10 +417,12 @@ fn bracket_close_inside_sequence() { | 1 | {(a) ] (b)} | ^ + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | {(a) ] (b)} | ^ + error: unnamed definition must be last: add a name: `Name = {(a)` | 1 | {(a) ] (b)} @@ -422,10 +441,12 @@ fn paren_close_inside_sequence() { | 1 | {(a) ) (b)} | ^ + error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | {(a) ) (b)} | ^ + error: unnamed definition must be last: add a name: `Name = {(a)` | 1 | {(a) ) (b)} @@ -444,6 +465,7 @@ fn single_colon_type_annotation_followed_by_non_id() { | 1 | (a) @x : (b) | ^ + error: unnamed definition must be last: add a name: `Name = (a) @x` | 1 | (a) @x : (b) 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 0dbf6bad..c11b82e3 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs @@ -145,6 +145,7 @@ fn upper_ident_in_alternation_not_followed_by_colon() { | 1 | [(Expr) (Statement)] | ^^^^ + error: undefined reference: `Statement` | 1 | [(Expr) (Statement)] @@ -233,6 +234,7 @@ fn multiple_unnamed_defs_errors_for_all_but_last() { | 1 | (first) | ^^^^^^^ + error: unnamed definition must be last: add a name: `Name = (second)` | 2 | (second) @@ -253,6 +255,7 @@ fn capture_space_after_dot_is_anchor() { | 1 | (identifier) @foo . (other) | ^^^^^^^^^^^^^^^^^ + error: unnamed definition must be last: add a name: `Name = .` | 1 | (identifier) @foo . (other) @@ -515,6 +518,7 @@ fn capture_dotted_followed_by_field() { 1 - (node) @foo.bar name: (other) 1 + (node) @foo_bar name: (other) | + error: unnamed definition must be last: add a name: `Name = (node) @foo.bar` | 1 | (node) @foo.bar name: (other) @@ -541,10 +545,12 @@ fn capture_space_after_dot_breaks_chain() { 1 - (identifier) @foo. bar 1 + (identifier) @foo_ bar | + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (identifier) @foo. bar | ^^^ + error: unnamed definition must be last: add a name: `Name = (identifier) @foo.` | 1 | (identifier) @foo. bar @@ -830,6 +836,7 @@ fn lowercase_branch_label() { 2 - left: (a) 2 + Left: (a) | + error: lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) | 3 | right: (b) @@ -1006,6 +1013,7 @@ fn comma_in_alternation() { 1 - [(a), (b), (c)] 1 + [(a) (b), (c)] | + error: invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a), (b), (c)] @@ -1056,6 +1064,7 @@ fn pipe_in_alternation() { 1 - [(a) | (b) | (c)] 1 + [(a) (b) | (c)] | + error: invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a) | (b) | (c)] @@ -1108,6 +1117,7 @@ fn pipe_in_tree() { 1 - (a | b) 1 + (a b) | + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (a | b) @@ -1192,6 +1202,7 @@ fn field_equals_typo_no_expression() { 1 - (call name=) 1 + (call name:) | + error: expected expression: expected expression after field name | 1 | (call name=) @@ -1291,18 +1302,22 @@ fn single_colon_primitive_type() { | 1 | @val : string | ^ + error: unexpected token: expected ':' to separate field name from its value | 1 | @val : string | ^ + error: expected expression: expected expression after field name | 1 | @val : string | ^ + error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | @val : string | ^^^^^^ + error: unnamed definition must be last: add a name: `Name = val` | 1 | @val : string diff --git a/crates/plotnik-lib/src/query/alt_kinds_tests.rs b/crates/plotnik-lib/src/query/alt_kinds_tests.rs index 1a36a34f..a1844921 100644 --- a/crates/plotnik-lib/src/query/alt_kinds_tests.rs +++ b/crates/plotnik-lib/src/query/alt_kinds_tests.rs @@ -91,6 +91,7 @@ fn multiple_mixed_alternations() { | - ^^^ | | | tagged branch here + error: mixed tagged and untagged branches in alternation | 1 | (foo [A: (a) (b)] [C: (c) (d)]) diff --git a/crates/plotnik-lib/src/query/symbol_table_tests.rs b/crates/plotnik-lib/src/query/symbol_table_tests.rs index 0ebb00a4..631f9ffc 100644 --- a/crates/plotnik-lib/src/query/symbol_table_tests.rs +++ b/crates/plotnik-lib/src/query/symbol_table_tests.rs @@ -241,10 +241,12 @@ fn multiple_undefined() { | 1 | (foo (X) (Y) (Z)) | ^ + error: undefined reference: `Y` | 1 | (foo (X) (Y) (Z)) | ^ + error: undefined reference: `Z` | 1 | (foo (X) (Y) (Z)) From 38aec04b6349ec07b5680d57b591ec80ed493e80 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 6 Dec 2025 11:12:22 -0300 Subject: [PATCH 10/15] Better error suppression --- crates/plotnik-lib/src/diagnostics/message.rs | 28 +++++++++ crates/plotnik-lib/src/diagnostics/mod.rs | 50 ++++++++++++---- crates/plotnik-lib/src/diagnostics/tests.rs | 9 +-- crates/plotnik-lib/src/parser/core.rs | 16 +++-- crates/plotnik-lib/src/parser/grammar.rs | 9 ++- .../parser/tests/recovery/incomplete_tests.rs | 12 ---- .../parser/tests/recovery/unclosed_tests.rs | 60 +++++++++---------- .../parser/tests/recovery/unexpected_tests.rs | 5 -- .../parser/tests/recovery/validation_tests.rs | 5 -- 9 files changed, 118 insertions(+), 76 deletions(-) diff --git a/crates/plotnik-lib/src/diagnostics/message.rs b/crates/plotnik-lib/src/diagnostics/message.rs index 4c96daae..528f4e68 100644 --- a/crates/plotnik-lib/src/diagnostics/message.rs +++ b/crates/plotnik-lib/src/diagnostics/message.rs @@ -78,6 +78,34 @@ impl DiagnosticKind { self < other } + /// Structural errors are Unclosed* - they cause cascading errors but + /// should be suppressed by root-cause errors at the same position. + pub fn is_structural_error(&self) -> bool { + matches!( + self, + Self::UnclosedTree | Self::UnclosedSequence | Self::UnclosedAlternation + ) + } + + /// Root cause errors - user omitted something required. + /// These suppress structural errors at the same position. + pub fn is_root_cause_error(&self) -> bool { + matches!( + self, + Self::ExpectedExpression + | Self::ExpectedTypeName + | Self::ExpectedCaptureName + | Self::ExpectedFieldName + | Self::ExpectedSubtype + ) + } + + /// Consequence errors - often caused by earlier parse errors. + /// These get suppressed when any root-cause or structural error exists. + pub fn is_consequence_error(&self) -> bool { + matches!(self, Self::UnnamedDefNotLast) + } + /// Base message for this diagnostic kind, used when no custom message is provided. pub fn fallback_message(&self) -> &'static str { match self { diff --git a/crates/plotnik-lib/src/diagnostics/mod.rs b/crates/plotnik-lib/src/diagnostics/mod.rs index afdb358e..984612ef 100644 --- a/crates/plotnik-lib/src/diagnostics/mod.rs +++ b/crates/plotnik-lib/src/diagnostics/mod.rs @@ -81,8 +81,10 @@ impl Diagnostics { /// Returns diagnostics with cascading errors suppressed. /// - /// Suppression rule: when a higher-priority diagnostic's span contains - /// a lower-priority diagnostic's span, the lower-priority one is suppressed. + /// Suppression rules: + /// 1. Containment: when a higher-priority span strictly contains another, suppress the inner + /// 2. Same position: when spans start at the same position, root-cause errors suppress structural ones + /// 3. Consequence errors (UnnamedDefNotLast) suppressed when any primary error exists pub(crate) fn filtered(&self) -> Vec { if self.messages.is_empty() { return Vec::new(); @@ -90,16 +92,44 @@ impl Diagnostics { let mut suppressed = vec![false; self.messages.len()]; + // Rule 3: Suppress consequence errors if any primary error exists + let has_primary_error = self + .messages + .iter() + .any(|m| m.kind.is_root_cause_error() || m.kind.is_structural_error()); + if has_primary_error { + for (i, msg) in self.messages.iter().enumerate() { + if msg.kind.is_consequence_error() { + suppressed[i] = true; + } + } + } + // O(n²) but n is typically small (< 100 diagnostics) - for (i, outer) in self.messages.iter().enumerate() { - for (j, inner) in self.messages.iter().enumerate() { - if i == j || suppressed[j] { + for (i, a) in self.messages.iter().enumerate() { + for (j, b) in self.messages.iter().enumerate() { + if i == j || suppressed[i] || suppressed[j] { continue; } - // Check if outer contains inner and has higher priority - if span_contains(outer.range, inner.range) && outer.kind.suppresses(&inner.kind) { + // Rule 1: Strict containment (different start positions) + // The containing span suppresses the contained span + if span_strictly_contains(a.range, b.range) && a.kind.suppresses(&b.kind) { suppressed[j] = true; + continue; + } + + // Rule 2: Same start position + if a.range.start() == b.range.start() { + // Root cause errors (Expected*) suppress structural errors (Unclosed*) + if a.kind.is_root_cause_error() && b.kind.is_structural_error() { + suppressed[j] = true; + continue; + } + // Otherwise, fall back to normal priority (lower discriminant wins) + if a.kind.suppresses(&b.kind) { + suppressed[j] = true; + } } } } @@ -171,7 +201,7 @@ impl<'a> DiagnosticBuilder<'a> { } } -/// Check if outer span fully contains inner span. -fn span_contains(outer: TextRange, inner: TextRange) -> bool { - outer.start() <= inner.start() && inner.end() <= outer.end() +/// Check if outer span strictly contains inner span (different start positions). +fn span_strictly_contains(outer: TextRange, inner: TextRange) -> bool { + outer.start() < inner.start() && inner.end() <= outer.end() } diff --git a/crates/plotnik-lib/src/diagnostics/tests.rs b/crates/plotnik-lib/src/diagnostics/tests.rs index a2943cd9..367649ab 100644 --- a/crates/plotnik-lib/src/diagnostics/tests.rs +++ b/crates/plotnik-lib/src/diagnostics/tests.rs @@ -383,9 +383,9 @@ fn filtered_suppresses_lower_priority_contained() { } #[test] -fn filtered_does_not_suppress_higher_priority() { +fn filtered_consequence_suppressed_by_structural() { let mut diagnostics = Diagnostics::new(); - // Lower priority error (UnnamedDefNotLast) cannot suppress higher priority (UnclosedTree) + // Consequence error (UnnamedDefNotLast) suppressed when structural error (UnclosedTree) exists diagnostics .report( DiagnosticKind::UnnamedDefNotLast, @@ -400,8 +400,9 @@ fn filtered_does_not_suppress_higher_priority() { .emit(); let filtered = diagnostics.filtered(); - // Both should remain - lower priority cannot suppress higher - assert_eq!(filtered.len(), 2); + // Only UnclosedTree remains - consequence errors suppressed when primary errors exist + assert_eq!(filtered.len(), 1); + assert_eq!(filtered[0].kind, DiagnosticKind::UnclosedTree); } #[test] diff --git a/crates/plotnik-lib/src/parser/core.rs b/crates/plotnik-lib/src/parser/core.rs index c7f4b71d..3ecf9a39 100644 --- a/crates/plotnik-lib/src/parser/core.rs +++ b/crates/plotnik-lib/src/parser/core.rs @@ -370,23 +370,27 @@ impl<'src> Parser<'src> { self.delimiter_stack.pop() } - pub(super) fn error_with_related( + /// Error for unclosed delimiters - uses full span from opening to current position. + /// This enables proper cascading error suppression. + pub(super) fn error_unclosed_delimiter( &mut self, kind: DiagnosticKind, message: impl Into, related_msg: impl Into, - related_range: TextRange, + open_range: TextRange, ) { - let range = self.current_span(); - let pos = range.start(); + let current = self.current_span(); + let pos = current.start(); if self.last_diagnostic_pos == Some(pos) { return; } self.last_diagnostic_pos = Some(pos); + // Full span from opening delimiter to current position for suppression + let full_range = TextRange::new(open_range.start(), current.end()); self.diagnostics - .report(kind, range) + .report(kind, full_range) .message(message) - .related_to(related_msg, related_range) + .related_to(related_msg, open_range) .emit(); } diff --git a/crates/plotnik-lib/src/parser/grammar.rs b/crates/plotnik-lib/src/parser/grammar.rs index 23eff67b..b0cc3ef4 100644 --- a/crates/plotnik-lib/src/parser/grammar.rs +++ b/crates/plotnik-lib/src/parser/grammar.rs @@ -299,7 +299,12 @@ impl Parser<'_> { (caller must push delimiter before calling)" ) }); - self.error_with_related(kind, msg, format!("{construct} started here"), open.span); + self.error_unclosed_delimiter( + kind, + msg, + format!("{construct} started here"), + open.span, + ); break; } if self.has_fatal_error() { @@ -355,7 +360,7 @@ impl Parser<'_> { (caller must push delimiter before calling)" ) }); - self.error_with_related( + self.error_unclosed_delimiter( DiagnosticKind::UnclosedAlternation, msg, "alternation started here", 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 b52df9de..b694dc01 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs @@ -152,18 +152,6 @@ fn type_annotation_invalid_token_after() { | 1 | (identifier) @name :: ( | ^ - - error: unclosed tree: unclosed tree; expected ')' - | - 1 | (identifier) @name :: ( - | -^ - | | - | tree started here - - error: unnamed definition must be last: add a name: `Name = (identifier) @name ::` - | - 1 | (identifier) @name :: ( - | ^^^^^^^^^^^^^^^^^^^^^ "); } diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs index 2e917a43..54a5690c 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs @@ -13,7 +13,7 @@ fn missing_paren() { error: unclosed tree: unclosed tree; expected ')' | 1 | (identifier - | - ^ + | -^^^^^^^^^^ | | | tree started here "); @@ -31,7 +31,7 @@ fn missing_bracket() { error: unclosed alternation: unclosed alternation; expected ']' | 1 | [(identifier) (string) - | - ^ + | -^^^^^^^^^^^^^^^^^^^^^ | | | alternation started here "); @@ -49,7 +49,7 @@ fn missing_brace() { error: unclosed sequence: unclosed sequence; expected '}' | 1 | {(a) (b) - | - ^ + | -^^^^^^^ | | | sequence started here "); @@ -67,7 +67,7 @@ fn nested_unclosed() { error: unclosed tree: unclosed tree; expected ')' | 1 | (a (b (c) - | - ^ + | -^^^^^ | | | tree started here "); @@ -85,7 +85,7 @@ fn deeply_nested_unclosed() { error: unclosed tree: unclosed tree; expected ')' | 1 | (a (b (c (d - | - ^ + | -^ | | | tree started here "); @@ -103,7 +103,7 @@ fn unclosed_alternation_nested() { error: unclosed tree: unclosed tree; expected ')' | 1 | [(a) (b - | - ^ + | -^ | | | tree started here "); @@ -137,10 +137,12 @@ fn unclosed_tree_shows_open_location() { insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unclosed tree: unclosed tree; expected ')' | - 1 | (call - | - tree started here - 2 | (identifier) - | ^ + 1 | (call + | ^ tree started here + | _| + | | + 2 | | (identifier) + | |_________________^ "); } @@ -157,11 +159,13 @@ fn unclosed_alternation_shows_open_location() { insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unclosed alternation: unclosed alternation; expected ']' | - 1 | [ - | - alternation started here - 2 | (a) - 3 | (b) - | ^ + 1 | [ + | ^ alternation started here + | _| + | | + 2 | | (a) + 3 | | (b) + | |________^ "); } @@ -178,11 +182,13 @@ fn unclosed_sequence_shows_open_location() { insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unclosed sequence: unclosed sequence; expected '}' | - 1 | { - | - sequence started here - 2 | (a) - 3 | (b) - | ^ + 1 | { + | ^ sequence started here + | _| + | | + 2 | | (a) + 3 | | (b) + | |________^ "); } @@ -193,15 +199,10 @@ fn unclosed_double_quote_string() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token: unexpected token; expected a child expression or closing delimiter - | - 1 | (call "foo) - | ^^^^^ - error: unclosed tree: unclosed tree; expected ')' | 1 | (call "foo) - | - ^ + | -^^^^^^^^^^ | | | tree started here "#); @@ -214,15 +215,10 @@ fn unclosed_single_quote_string() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token: unexpected token; expected a child expression or closing delimiter - | - 1 | (call 'foo) - | ^^^^^ - error: unclosed tree: unclosed tree; expected ')' | 1 | (call 'foo) - | - ^ + | -^^^^^^^^^^ | | | tree started here "); diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs index 95404e07..7238fc5d 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs @@ -104,11 +104,6 @@ fn garbage_inside_node() { | 1 | (a (b) @@@ (c)) (d) | ^ - - error: unnamed definition must be last: add a name: `Name = (a (b) @@@ (c))` - | - 1 | (a (b) @@@ (c)) (d) - | ^^^^^^^^^^^^^^^ "); } 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 c11b82e3..11b282f2 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs @@ -1317,10 +1317,5 @@ fn single_colon_primitive_type() { | 1 | @val : string | ^^^^^^ - - error: unnamed definition must be last: add a name: `Name = val` - | - 1 | @val : string - | ^^^ "); } From 6f1442f851041ebe8e944c842cb2543526610e65 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 6 Dec 2025 11:32:17 -0300 Subject: [PATCH 11/15] Improve suppression --- crates/plotnik-lib/src/diagnostics/message.rs | 7 ++ crates/plotnik-lib/src/diagnostics/mod.rs | 43 ++++++++---- crates/plotnik-lib/src/parser/core.rs | 24 ++++++- .../parser/tests/recovery/incomplete_tests.rs | 32 --------- .../parser/tests/recovery/unexpected_tests.rs | 70 ------------------- .../parser/tests/recovery/validation_tests.rs | 32 --------- 6 files changed, 60 insertions(+), 148 deletions(-) diff --git a/crates/plotnik-lib/src/diagnostics/message.rs b/crates/plotnik-lib/src/diagnostics/message.rs index 528f4e68..27477bd7 100644 --- a/crates/plotnik-lib/src/diagnostics/message.rs +++ b/crates/plotnik-lib/src/diagnostics/message.rs @@ -242,7 +242,13 @@ impl RelatedInfo { #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct DiagnosticMessage { pub(crate) kind: DiagnosticKind, + /// The range shown to the user (underlined in output). pub(crate) range: TextRange, + /// The range used for suppression logic. Errors within another error's + /// suppression_range may be suppressed. Defaults to `range` but can be + /// set to a parent context (e.g., enclosing tree span) for better cascading + /// error suppression. + pub(crate) suppression_range: TextRange, pub(crate) message: String, pub(crate) fix: Option, pub(crate) related: Vec, @@ -253,6 +259,7 @@ impl DiagnosticMessage { Self { kind, range, + suppression_range: range, message: message.into(), fix: None, related: Vec::new(), diff --git a/crates/plotnik-lib/src/diagnostics/mod.rs b/crates/plotnik-lib/src/diagnostics/mod.rs index 984612ef..5365d755 100644 --- a/crates/plotnik-lib/src/diagnostics/mod.rs +++ b/crates/plotnik-lib/src/diagnostics/mod.rs @@ -82,9 +82,10 @@ impl Diagnostics { /// Returns diagnostics with cascading errors suppressed. /// /// Suppression rules: - /// 1. Containment: when a higher-priority span strictly contains another, suppress the inner + /// 1. Containment: when error A's suppression_range contains error B's display range, + /// and A has higher priority, suppress B /// 2. Same position: when spans start at the same position, root-cause errors suppress structural ones - /// 3. Consequence errors (UnnamedDefNotLast) suppressed when any primary error exists + /// 3. Consequence errors (UnnamedDefNotLast) suppressed when any other error exists pub(crate) fn filtered(&self) -> Vec { if self.messages.is_empty() { return Vec::new(); @@ -92,11 +93,8 @@ impl Diagnostics { let mut suppressed = vec![false; self.messages.len()]; - // Rule 3: Suppress consequence errors if any primary error exists - let has_primary_error = self - .messages - .iter() - .any(|m| m.kind.is_root_cause_error() || m.kind.is_structural_error()); + // Rule 3: Suppress consequence errors if any non-consequence error exists + let has_primary_error = self.messages.iter().any(|m| !m.kind.is_consequence_error()); if has_primary_error { for (i, msg) in self.messages.iter().enumerate() { if msg.kind.is_consequence_error() { @@ -112,9 +110,11 @@ impl Diagnostics { continue; } - // Rule 1: Strict containment (different start positions) - // The containing span suppresses the contained span - if span_strictly_contains(a.range, b.range) && a.kind.suppresses(&b.kind) { + // Rule 1: Suppression range containment + // If A's suppression_range contains B's display range, A can suppress B + if suppression_range_contains(a.suppression_range, b.range) + && a.kind.suppresses(&b.kind) + { suppressed[j] = true; continue; } @@ -191,6 +191,18 @@ impl<'a> DiagnosticBuilder<'a> { self } + /// Set the suppression range for this diagnostic. + /// + /// The suppression range is used to suppress cascading errors. Errors whose + /// display range falls within another error's suppression range may be + /// suppressed if the containing error has higher priority. + /// + /// Typically set to the parent context span (e.g., enclosing tree). + pub fn suppression_range(mut self, range: TextRange) -> Self { + self.message.suppression_range = range; + self + } + pub fn fix(mut self, description: impl Into, replacement: impl Into) -> Self { self.message.fix = Some(Fix::new(replacement, description)); self @@ -201,7 +213,12 @@ impl<'a> DiagnosticBuilder<'a> { } } -/// Check if outer span strictly contains inner span (different start positions). -fn span_strictly_contains(outer: TextRange, inner: TextRange) -> bool { - outer.start() < inner.start() && inner.end() <= outer.end() +/// Check if a suppression range contains a display range. +/// +/// For suppression purposes, we use non-strict containment: the inner range +/// can start at the same position as the outer range. This allows errors +/// reported at the same position but with different suppression contexts +/// to properly suppress each other. +fn suppression_range_contains(suppression: TextRange, display: TextRange) -> bool { + suppression.start() <= display.start() && display.end() <= suppression.end() } diff --git a/crates/plotnik-lib/src/parser/core.rs b/crates/plotnik-lib/src/parser/core.rs index 3ecf9a39..dcbc1c42 100644 --- a/crates/plotnik-lib/src/parser/core.rs +++ b/crates/plotnik-lib/src/parser/core.rs @@ -261,6 +261,22 @@ impl<'src> Parser<'src> { false } + /// Returns the suppression span for the current context. + /// + /// If inside a delimiter (tree/seq/alt), returns a span from the delimiter's + /// start to the end of source. This ensures all errors within the same + /// delimiter context can suppress each other based on priority. + /// At root level, returns the current token's span. + pub(super) fn current_suppression_span(&self) -> TextRange { + self.delimiter_stack + .last() + .map(|d| { + let source_end = TextSize::from(self.source.len() as u32); + TextRange::new(d.span.start(), source_end) + }) + .unwrap_or_else(|| self.current_span()) + } + /// Emit diagnostic with default message for the kind. pub(super) fn error(&mut self, kind: DiagnosticKind) { self.error_msg(kind, kind.fallback_message()); @@ -274,7 +290,13 @@ impl<'src> Parser<'src> { return; } self.last_diagnostic_pos = Some(pos); - self.diagnostics.report(kind, range).message(message).emit(); + + let suppression = self.current_suppression_span(); + self.diagnostics + .report(kind, range) + .message(message) + .suppression_range(suppression) + .emit(); } pub(super) fn error_and_bump(&mut self, kind: DiagnosticKind) { 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 b694dc01..1938c69a 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs @@ -231,11 +231,6 @@ fn capture_at_start_of_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token: unexpected token; expected a child expression or closing delimiter - | - 1 | [@x (a)] - | ^ - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | [@x (a)] @@ -261,11 +256,6 @@ fn mixed_valid_invalid_captures() { | 1 | (a) @ok @ @name | ^^^^ - - error: unnamed definition must be last: add a name: `Name = (a) @ok` - | - 1 | (a) @ok @ @name - | ^^^^^^^ "); } @@ -278,17 +268,6 @@ fn field_equals_typo_missing_value() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid field syntax: '=' is not valid for field constraints - | - 1 | (call name = ) - | ^ - | - help: use ':' - | - 1 - (call name = ) - 1 + (call name : ) - | - error: expected expression: expected expression after field name | 1 | (call name = ) @@ -303,17 +282,6 @@ fn lowercase_branch_label_missing_expression() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) - | - 1 | [label:] - | ^^^^^ - | - help: capitalize as `Label` - | - 1 - [label:] - 1 + [Label:] - | - error: expected expression: expected expression after branch label | 1 | [label:] diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs index 7238fc5d..a5e84ec1 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs @@ -14,11 +14,6 @@ fn unexpected_token() { | 1 | (identifier) ^^^ (string) | ^^^ - - error: unnamed definition must be last: add a name: `Name = (identifier)` - | - 1 | (identifier) ^^^ (string) - | ^^^^^^^^^^^^ "#); } @@ -99,11 +94,6 @@ fn garbage_inside_node() { | 1 | (a (b) @@@ (c)) (d) | ^ - - error: unexpected token: unexpected token; expected a child expression or closing delimiter - | - 1 | (a (b) @@@ (c)) (d) - | ^ "); } @@ -153,16 +143,6 @@ fn predicate_unsupported() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unsupported predicate: unsupported predicate - | - 1 | (a (#eq? @x "foo") b) - | ^^^^ - - error: unexpected token: unexpected token; expected a child expression or closing delimiter - | - 1 | (a (#eq? @x "foo") b) - | ^ - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (a (#eq? @x "foo") b) @@ -193,11 +173,6 @@ fn predicate_match() { | 1 | (identifier) #match? @name "test" | ^^^^ - - error: unnamed definition must be last: add a name: `Name = (identifier)` - | - 1 | (identifier) #match? @name "test" - | ^^^^^^^^^^^^ "#); } @@ -208,16 +183,6 @@ fn predicate_in_tree() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unsupported predicate: unsupported predicate - | - 1 | (function #eq? @name "test") - | ^^^^ - - error: unexpected token: unexpected token; expected a child expression or closing delimiter - | - 1 | (function #eq? @name "test") - | ^ - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (function #eq? @name "test") @@ -268,11 +233,6 @@ fn multiline_garbage_recovery() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token: unexpected token; expected a child expression or closing delimiter - | - 2 | ^^^ - | ^^^ - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 3 | b) @@ -330,16 +290,6 @@ fn alternation_recovery_to_capture() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token: unexpected token; expected a child expression or closing delimiter - | - 1 | [^^^ @name] - | ^^^ - - error: unexpected token: unexpected token; expected a child expression or closing delimiter - | - 1 | [^^^ @name] - | ^ - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | [^^^ @name] @@ -393,11 +343,6 @@ fn paren_close_inside_alternation() { | 1 | [(a) ) (b)] | ^ - - error: unnamed definition must be last: add a name: `Name = [(a)` - | - 1 | [(a) ) (b)] - | ^^^^ "#); } @@ -417,11 +362,6 @@ fn bracket_close_inside_sequence() { | 1 | {(a) ] (b)} | ^ - - error: unnamed definition must be last: add a name: `Name = {(a)` - | - 1 | {(a) ] (b)} - | ^^^^ "#); } @@ -441,11 +381,6 @@ fn paren_close_inside_sequence() { | 1 | {(a) ) (b)} | ^ - - error: unnamed definition must be last: add a name: `Name = {(a)` - | - 1 | {(a) ) (b)} - | ^^^^ "#); } @@ -460,11 +395,6 @@ fn single_colon_type_annotation_followed_by_non_id() { | 1 | (a) @x : (b) | ^ - - error: unnamed definition must be last: add a name: `Name = (a) @x` - | - 1 | (a) @x : (b) - | ^^^^^^ "#); } 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 11b282f2..66bbf409 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs @@ -518,11 +518,6 @@ fn capture_dotted_followed_by_field() { 1 - (node) @foo.bar name: (other) 1 + (node) @foo_bar name: (other) | - - error: unnamed definition must be last: add a name: `Name = (node) @foo.bar` - | - 1 | (node) @foo.bar name: (other) - | ^^^^^^^^^^^^^^^ "); } @@ -550,11 +545,6 @@ fn capture_space_after_dot_breaks_chain() { | 1 | (identifier) @foo. bar | ^^^ - - error: unnamed definition must be last: add a name: `Name = (identifier) @foo.` - | - 1 | (identifier) @foo. bar - | ^^^^^^^^^^^^^^^^^^ "); } @@ -1107,17 +1097,6 @@ fn pipe_in_tree() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation - | - 1 | (a | b) - | ^ - | - help: remove separator - | - 1 - (a | b) - 1 + (a b) - | - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (a | b) @@ -1192,17 +1171,6 @@ fn field_equals_typo_no_expression() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid field syntax: '=' is not valid for field constraints - | - 1 | (call name=) - | ^ - | - help: use ':' - | - 1 - (call name=) - 1 + (call name:) - | - error: expected expression: expected expression after field name | 1 | (call name=) From 451c7b53d9adc9d19971592a02e9ad66f975e93b Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 6 Dec 2025 11:52:44 -0300 Subject: [PATCH 12/15] Fix messages --- crates/plotnik-lib/src/diagnostics/message.rs | 8 +- crates/plotnik-lib/src/diagnostics/tests.rs | 16 +-- crates/plotnik-lib/src/parser/ast_tests.rs | 2 +- crates/plotnik-lib/src/parser/core.rs | 52 ++++--- crates/plotnik-lib/src/parser/grammar.rs | 41 ++---- .../parser/tests/recovery/coverage_tests.rs | 8 +- .../parser/tests/recovery/incomplete_tests.rs | 40 +++--- .../parser/tests/recovery/unclosed_tests.rs | 24 ++-- .../parser/tests/recovery/unexpected_tests.rs | 62 ++++---- .../parser/tests/recovery/validation_tests.rs | 136 +++++++++--------- .../plotnik-lib/src/query/recursion_tests.rs | 12 +- .../src/query/symbol_table_tests.rs | 14 +- 12 files changed, 204 insertions(+), 211 deletions(-) diff --git a/crates/plotnik-lib/src/diagnostics/message.rs b/crates/plotnik-lib/src/diagnostics/message.rs index 27477bd7..db1d548b 100644 --- a/crates/plotnik-lib/src/diagnostics/message.rs +++ b/crates/plotnik-lib/src/diagnostics/message.rs @@ -169,15 +169,15 @@ impl DiagnosticKind { // Cases with backtick-wrapped placeholders Self::DuplicateDefinition | Self::UndefinedReference => { - format!("{}: `{{}}`", self.fallback_message()) + format!("{}; `{{}}`", self.fallback_message()) } // Cases where custom text differs from fallback - Self::InvalidTypeAnnotationSyntax => "invalid type annotation: {}".to_string(), - Self::MixedAltBranches => "mixed alternation: {}".to_string(), + Self::InvalidTypeAnnotationSyntax => "invalid type annotation; {}".to_string(), + Self::MixedAltBranches => "mixed alternation; {}".to_string(), // Standard pattern: fallback + ": {}" - _ => format!("{}: {{}}", self.fallback_message()), + _ => format!("{}; {{}}", self.fallback_message()), } } diff --git a/crates/plotnik-lib/src/diagnostics/tests.rs b/crates/plotnik-lib/src/diagnostics/tests.rs index 367649ab..eb760634 100644 --- a/crates/plotnik-lib/src/diagnostics/tests.rs +++ b/crates/plotnik-lib/src/diagnostics/tests.rs @@ -63,7 +63,7 @@ fn builder_with_related() { assert_eq!(diagnostics.len(), 1); let result = diagnostics.printer("hello world!").render(); insta::assert_snapshot!(result, @r" - error: unclosed tree: primary + error: unclosed tree; primary | 1 | hello world! | ^^^^^ ---- related info @@ -84,7 +84,7 @@ fn builder_with_fix() { let result = diagnostics.printer("hello world").render(); insta::assert_snapshot!(result, @r" - error: invalid field syntax: fixable + error: invalid field syntax; fixable | 1 | hello world | ^^^^^ @@ -113,7 +113,7 @@ fn builder_with_all_options() { let result = diagnostics.printer("hello world stuff!").render(); insta::assert_snapshot!(result, @r" - error: unclosed tree: main error + error: unclosed tree; main error | 1 | hello world stuff! | ^^^^^ ----- ----- and here @@ -164,7 +164,7 @@ fn printer_with_path() { let result = diagnostics.printer("hello world").path("test.pql").render(); insta::assert_snapshot!(result, @r" - error: undefined reference: `test error` + error: undefined reference; `test error` --> test.pql:1:1 | 1 | hello world @@ -185,7 +185,7 @@ fn printer_zero_width_span() { let result = diagnostics.printer("hello").render(); insta::assert_snapshot!(result, @r" - error: expected expression: zero width error + error: expected expression; zero width error | 1 | hello | ^ @@ -206,7 +206,7 @@ fn printer_related_zero_width() { let result = diagnostics.printer("hello world!").render(); insta::assert_snapshot!(result, @r" - error: unclosed tree: primary + error: unclosed tree; primary | 1 | hello world! | ^^^^^ - zero width related @@ -233,12 +233,12 @@ fn printer_multiple_diagnostics() { let result = diagnostics.printer("hello world!").render(); insta::assert_snapshot!(result, @r" - error: unclosed tree: first error + error: unclosed tree; first error | 1 | hello world! | ^^^^^ - error: undefined reference: `second error` + error: undefined reference; `second error` | 1 | hello world! | ^^^^ diff --git a/crates/plotnik-lib/src/parser/ast_tests.rs b/crates/plotnik-lib/src/parser/ast_tests.rs index 543a5c6d..b0b53ac5 100644 --- a/crates/plotnik-lib/src/parser/ast_tests.rs +++ b/crates/plotnik-lib/src/parser/ast_tests.rs @@ -262,7 +262,7 @@ fn ast_with_errors() { let query = Query::try_from("(call (Undefined))").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: undefined reference: `Undefined` + error: undefined reference; `Undefined` | 1 | (call (Undefined)) | ^^^^^^^^^ diff --git a/crates/plotnik-lib/src/parser/core.rs b/crates/plotnik-lib/src/parser/core.rs index dcbc1c42..a451f03f 100644 --- a/crates/plotnik-lib/src/parser/core.rs +++ b/crates/plotnik-lib/src/parser/core.rs @@ -254,10 +254,8 @@ impl<'src> Parser<'src> { if self.eat(kind) { return true; } - self.error_msg( - DiagnosticKind::UnexpectedToken, - format!("expected {}", what), - ); + let msg = format!("expected {}", what); + self.error_msg(DiagnosticKind::UnexpectedToken, &msg); false } @@ -277,13 +275,7 @@ impl<'src> Parser<'src> { .unwrap_or_else(|| self.current_span()) } - /// Emit diagnostic with default message for the kind. - pub(super) fn error(&mut self, kind: DiagnosticKind) { - self.error_msg(kind, kind.fallback_message()); - } - - /// Emit diagnostic with custom message. - pub(super) fn error_msg(&mut self, kind: DiagnosticKind, message: impl Into) { + fn error_impl(&mut self, kind: DiagnosticKind, message: Option<&str>) { let range = self.current_span(); let pos = range.start(); if self.last_diagnostic_pos == Some(pos) { @@ -292,19 +284,15 @@ impl<'src> Parser<'src> { self.last_diagnostic_pos = Some(pos); let suppression = self.current_suppression_span(); - self.diagnostics - .report(kind, range) - .message(message) - .suppression_range(suppression) - .emit(); - } - - pub(super) fn error_and_bump(&mut self, kind: DiagnosticKind) { - self.error_and_bump_msg(kind, kind.fallback_message()); + let builder = self.diagnostics.report(kind, range); + let builder = match message { + Some(msg) => builder.message(msg), + None => builder, + }; + builder.suppression_range(suppression).emit(); } - pub(super) fn error_and_bump_msg(&mut self, kind: DiagnosticKind, message: &str) { - self.error_msg(kind, message); + fn bump_as_error(&mut self) { if !self.eof() { self.start_node(SyntaxKind::Error); self.bump(); @@ -312,6 +300,26 @@ impl<'src> Parser<'src> { } } + /// Emit diagnostic with default message for the kind. + pub(super) fn error(&mut self, kind: DiagnosticKind) { + self.error_impl(kind, None); + } + + /// Emit diagnostic with custom message detail. + pub(super) fn error_msg(&mut self, kind: DiagnosticKind, message: &str) { + self.error_impl(kind, Some(message)); + } + + pub(super) fn error_and_bump(&mut self, kind: DiagnosticKind) { + self.error(kind); + self.bump_as_error(); + } + + pub(super) fn error_and_bump_msg(&mut self, kind: DiagnosticKind, message: &str) { + self.error_msg(kind, message); + self.bump_as_error(); + } + #[allow(dead_code)] pub(super) fn error_recover( &mut self, diff --git a/crates/plotnik-lib/src/parser/grammar.rs b/crates/plotnik-lib/src/parser/grammar.rs index b0cc3ef4..de633daa 100644 --- a/crates/plotnik-lib/src/parser/grammar.rs +++ b/crates/plotnik-lib/src/parser/grammar.rs @@ -72,7 +72,7 @@ impl Parser<'_> { } else { self.error_msg( DiagnosticKind::ExpectedExpression, - "expected expression after '=' in named definition", + "after '=' in named definition", ); } @@ -95,7 +95,7 @@ impl Parser<'_> { } else { self.error_and_bump_msg( DiagnosticKind::UnexpectedToken, - "unexpected token; expected an expression like (node), [choice], {sequence}, \"literal\", or _", + "expected an expression like (node), [choice], {sequence}, \"literal\", or _", ); false } @@ -137,10 +137,7 @@ impl Parser<'_> { self.error_and_bump(DiagnosticKind::ErrorMissingOutsideParens); } _ => { - self.error_and_bump_msg( - DiagnosticKind::UnexpectedToken, - "unexpected token; expected an expression", - ); + self.error_and_bump_msg(DiagnosticKind::UnexpectedToken, "expected an expression"); } } @@ -204,7 +201,7 @@ impl Parser<'_> { _ => { self.error_msg( DiagnosticKind::ExpectedSubtype, - "expected subtype after '/' (e.g., expression/binary_expression)", + "after '/' (e.g., expression/binary_expression)", ); } } @@ -331,7 +328,7 @@ impl Parser<'_> { } self.error_and_bump_msg( DiagnosticKind::UnexpectedToken, - "unexpected token; expected a child expression or closing delimiter", + "expected a child expression or closing delimiter", ); } } @@ -402,7 +399,7 @@ impl Parser<'_> { } self.error_and_bump_msg( DiagnosticKind::UnexpectedToken, - "unexpected token; expected a child expression or closing delimiter", + "expected a child expression or closing delimiter", ); } } @@ -422,10 +419,7 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr(); } else { - self.error_msg( - DiagnosticKind::ExpectedExpression, - "expected expression after branch label", - ); + self.error_msg(DiagnosticKind::ExpectedExpression, "after branch label"); } self.finish_node(); @@ -454,10 +448,7 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr(); } else { - self.error_msg( - DiagnosticKind::ExpectedExpression, - "expected expression after branch label", - ); + self.error_msg(DiagnosticKind::ExpectedExpression, "after branch label"); } self.finish_node(); @@ -541,7 +532,7 @@ impl Parser<'_> { } else { self.error_msg( DiagnosticKind::ExpectedTypeName, - "expected type name after '::' (e.g., ::MyType or ::string)", + "after '::' (e.g., ::MyType or ::string)", ); } @@ -590,7 +581,7 @@ impl Parser<'_> { if self.peek() != SyntaxKind::Id { self.error_msg( DiagnosticKind::ExpectedFieldName, - "expected field name after '!' (e.g., !value)", + "after '!' (e.g., !value)", ); self.finish_node(); return; @@ -614,7 +605,7 @@ impl Parser<'_> { // Bare identifiers are not valid expressions; trees require parentheses self.error_and_bump_msg( DiagnosticKind::BareIdentifier, - "bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier)", + "nodes must be enclosed in parentheses, e.g., (identifier)", ); } } @@ -638,10 +629,7 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr_no_suffix(); } else { - self.error_msg( - DiagnosticKind::ExpectedExpression, - "expected expression after field name", - ); + self.error_msg(DiagnosticKind::ExpectedExpression, "after field name"); } self.finish_node(); @@ -666,10 +654,7 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr(); } else { - self.error_msg( - DiagnosticKind::ExpectedExpression, - "expected expression after field name", - ); + self.error_msg(DiagnosticKind::ExpectedExpression, "after field name"); } self.finish_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 4f93b76d..adbba77b 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs @@ -131,12 +131,12 @@ fn named_def_missing_equals_with_garbage() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | Expr ^^^ (identifier) | ^^^^ - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | Expr ^^^ (identifier) | ^^^ @@ -153,12 +153,12 @@ fn named_def_missing_equals_recovers_to_next_def() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | Broken ^^^ | ^^^^^^ - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | Broken ^^^ | ^^^ 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 1938c69a..5e89ee2b 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs @@ -10,7 +10,7 @@ fn missing_capture_name() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected capture name: expected capture name + error: expected capture name | 1 | (identifier) @ | ^ @@ -26,7 +26,7 @@ fn missing_field_value() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression: expected expression after field name + error: expected expression; after field name | 1 | (call name:) | ^ @@ -40,7 +40,7 @@ fn named_def_eof_after_equals() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression: expected expression after '=' in named definition + error: expected expression; after '=' in named definition | 1 | Expr = | ^ @@ -56,7 +56,7 @@ fn missing_type_name() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected type name: expected type name after '::' (e.g., ::MyType or ::string) + error: expected type name; after '::' (e.g., ::MyType or ::string) | 1 | (identifier) @name :: | ^ @@ -72,7 +72,7 @@ fn missing_negated_field_name() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected field name: expected field name after '!' (e.g., !value) + error: expected field name; after '!' (e.g., !value) | 1 | (call !) | ^ @@ -88,7 +88,7 @@ fn missing_subtype() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected subtype: expected subtype after '/' (e.g., expression/binary_expression) + error: expected subtype; after '/' (e.g., expression/binary_expression) | 1 | (expression/) | ^ @@ -104,7 +104,7 @@ fn tagged_branch_missing_expression() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression: expected expression after branch label + error: expected expression; after branch label | 1 | [Label:] | ^ @@ -118,7 +118,7 @@ fn type_annotation_missing_name_at_eof() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected type name: expected type name after '::' (e.g., ::MyType or ::string) + error: expected type name; after '::' (e.g., ::MyType or ::string) | 1 | (a) @x :: | ^ @@ -132,7 +132,7 @@ fn type_annotation_missing_name_with_bracket() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected type name: expected type name after '::' (e.g., ::MyType or ::string) + error: expected type name; after '::' (e.g., ::MyType or ::string) | 1 | [(a) @x :: ] | ^ @@ -148,7 +148,7 @@ fn type_annotation_invalid_token_after() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected type name: expected type name after '::' (e.g., ::MyType or ::string) + error: expected type name; after '::' (e.g., ::MyType or ::string) | 1 | (identifier) @name :: ( | ^ @@ -164,7 +164,7 @@ fn field_value_is_garbage() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression: expected expression after field name + error: expected expression; after field name | 1 | (call name: %%%) | ^^^ @@ -180,7 +180,7 @@ fn capture_with_invalid_char() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected capture name: expected capture name + error: expected capture name | 1 | (identifier) @123 | ^^^ @@ -194,7 +194,7 @@ fn bare_capture_at_eof_triggers_sync() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture without target: capture without target + error: capture without target | 1 | @ | ^ @@ -210,12 +210,12 @@ fn bare_capture_at_root() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture without target: capture without target + error: capture without target | 1 | @name | ^ - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | @name | ^^^^ @@ -231,7 +231,7 @@ fn capture_at_start_of_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | [@x (a)] | ^ @@ -247,12 +247,12 @@ fn mixed_valid_invalid_captures() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture without target: capture without target + error: capture without target | 1 | (a) @ok @ @name | ^ - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (a) @ok @ @name | ^^^^ @@ -268,7 +268,7 @@ fn field_equals_typo_missing_value() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression: expected expression after field name + error: expected expression; after field name | 1 | (call name = ) | ^ @@ -282,7 +282,7 @@ fn lowercase_branch_label_missing_expression() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression: expected expression after branch label + error: expected expression; after branch label | 1 | [label:] | ^ diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs index 54a5690c..14a71240 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs @@ -10,7 +10,7 @@ fn missing_paren() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree: unclosed tree; expected ')' + error: unclosed tree; unclosed tree; expected ')' | 1 | (identifier | -^^^^^^^^^^ @@ -28,7 +28,7 @@ fn missing_bracket() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed alternation: unclosed alternation; expected ']' + error: unclosed alternation; unclosed alternation; expected ']' | 1 | [(identifier) (string) | -^^^^^^^^^^^^^^^^^^^^^ @@ -46,7 +46,7 @@ fn missing_brace() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed sequence: unclosed sequence; expected '}' + error: unclosed sequence; unclosed sequence; expected '}' | 1 | {(a) (b) | -^^^^^^^ @@ -64,7 +64,7 @@ fn nested_unclosed() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree: unclosed tree; expected ')' + error: unclosed tree; unclosed tree; expected ')' | 1 | (a (b (c) | -^^^^^ @@ -82,7 +82,7 @@ fn deeply_nested_unclosed() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree: unclosed tree; expected ')' + error: unclosed tree; unclosed tree; expected ')' | 1 | (a (b (c (d | -^ @@ -100,7 +100,7 @@ fn unclosed_alternation_nested() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree: unclosed tree; expected ')' + error: unclosed tree; unclosed tree; expected ')' | 1 | [(a) (b | -^ @@ -118,7 +118,7 @@ fn empty_parens() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: empty tree expression: empty tree expression + error: empty tree expression | 1 | () | ^ @@ -135,7 +135,7 @@ fn unclosed_tree_shows_open_location() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree: unclosed tree; expected ')' + error: unclosed tree; unclosed tree; expected ')' | 1 | (call | ^ tree started here @@ -157,7 +157,7 @@ fn unclosed_alternation_shows_open_location() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed alternation: unclosed alternation; expected ']' + error: unclosed alternation; unclosed alternation; expected ']' | 1 | [ | ^ alternation started here @@ -180,7 +180,7 @@ fn unclosed_sequence_shows_open_location() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed sequence: unclosed sequence; expected '}' + error: unclosed sequence; unclosed sequence; expected '}' | 1 | { | ^ sequence started here @@ -199,7 +199,7 @@ fn unclosed_double_quote_string() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unclosed tree: unclosed tree; expected ')' + error: unclosed tree; unclosed tree; expected ')' | 1 | (call "foo) | -^^^^^^^^^^ @@ -215,7 +215,7 @@ fn unclosed_single_quote_string() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree: unclosed tree; expected ')' + error: unclosed tree; unclosed tree; expected ')' | 1 | (call 'foo) | -^^^^^^^^^^ diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs index a5e84ec1..18421b18 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs @@ -10,7 +10,7 @@ fn unexpected_token() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | (identifier) ^^^ (string) | ^^^ @@ -26,7 +26,7 @@ fn multiple_consecutive_garbage() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | ^^^ $$$ %%% (ok) | ^^^ @@ -42,7 +42,7 @@ fn garbage_at_start() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | ^^^ (a) | ^^^ @@ -58,7 +58,7 @@ fn only_garbage() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | ^^^ $$$ | ^^^ @@ -74,7 +74,7 @@ fn garbage_inside_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token: unexpected token; expected a child expression or closing delimiter + error: unexpected token; expected a child expression or closing delimiter | 1 | [(a) ^^^ (b)] | ^^^ @@ -90,7 +90,7 @@ fn garbage_inside_node() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected capture name: expected capture name + error: expected capture name | 1 | (a (b) @@@ (c)) (d) | ^ @@ -106,12 +106,12 @@ fn xml_tag_garbage() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 |
(identifier)
| ^^^^^ - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 |
(identifier)
| ^^^^^^ @@ -127,7 +127,7 @@ fn xml_self_closing() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 |
(a) | ^^^^^ @@ -143,12 +143,12 @@ fn predicate_unsupported() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (a (#eq? @x "foo") b) | ^ - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (a (#eq? @x "foo") b) | ^ @@ -164,12 +164,12 @@ fn predicate_match() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unsupported predicate: unsupported predicate + error: unsupported predicate | 1 | (identifier) #match? @name "test" | ^^^^^^^ - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (identifier) #match? @name "test" | ^^^^ @@ -183,7 +183,7 @@ fn predicate_in_tree() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (function #eq? @name "test") | ^^^^ @@ -199,7 +199,7 @@ fn predicate_in_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token: unexpected token; expected a child expression or closing delimiter + error: unexpected token; expected a child expression or closing delimiter | 1 | [(a) #eq? (b)] | ^^^^ @@ -215,7 +215,7 @@ fn predicate_in_sequence() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unsupported predicate: unsupported predicate + error: unsupported predicate | 1 | {(a) #set! (b)} | ^^^^^ @@ -233,7 +233,7 @@ fn multiline_garbage_recovery() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 3 | b) | ^ @@ -249,7 +249,7 @@ fn top_level_garbage_recovery() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | Expr = (a) ^^^ Expr2 = (b) | ^^^ @@ -269,12 +269,12 @@ fn multiple_definitions_with_garbage_between() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 2 | ^^^ | ^^^ - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 4 | $$$ | ^^^ @@ -290,7 +290,7 @@ fn alternation_recovery_to_capture() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | [^^^ @name] | ^^^^ @@ -306,7 +306,7 @@ fn comma_between_defs() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | A = (a), B = (b) | ^ @@ -320,7 +320,7 @@ fn bare_colon_in_tree() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token: unexpected token; expected a child expression or closing delimiter + error: unexpected token; expected a child expression or closing delimiter | 1 | (a : (b)) | ^ @@ -334,12 +334,12 @@ fn paren_close_inside_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token: expected closing ']' for alternation + error: unexpected token; expected closing ']' for alternation | 1 | [(a) ) (b)] | ^ - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | [(a) ) (b)] | ^ @@ -353,12 +353,12 @@ fn bracket_close_inside_sequence() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token: expected closing '}' for sequence + error: unexpected token; expected closing '}' for sequence | 1 | {(a) ] (b)} | ^ - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | {(a) ] (b)} | ^ @@ -372,12 +372,12 @@ fn paren_close_inside_sequence() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token: expected closing '}' for sequence + error: unexpected token; expected closing '}' for sequence | 1 | {(a) ) (b)} | ^ - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | {(a) ) (b)} | ^ @@ -391,7 +391,7 @@ fn single_colon_type_annotation_followed_by_non_id() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | (a) @x : (b) | ^ @@ -405,7 +405,7 @@ fn single_colon_type_annotation_at_eof() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ | 1 | (a) @x : | ^ 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 66bbf409..827f2194 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs @@ -59,7 +59,7 @@ fn reference_with_supertype_syntax_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid supertype syntax: invalid supertype syntax + error: invalid supertype syntax | 1 | (RefName/subtype) | ^ @@ -93,7 +93,7 @@ fn error_with_unexpected_content() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: (ERROR) takes no arguments: (ERROR) takes no arguments + error: (ERROR) takes no arguments | 1 | (ERROR (something)) | ^ @@ -109,7 +109,7 @@ fn bare_error_keyword() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: ERROR/MISSING outside parentheses: ERROR/MISSING outside parentheses + error: ERROR/MISSING outside parentheses | 1 | ERROR | ^^^^^ @@ -125,7 +125,7 @@ fn bare_missing_keyword() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: ERROR/MISSING outside parentheses: ERROR/MISSING outside parentheses + error: ERROR/MISSING outside parentheses | 1 | MISSING | ^^^^^^^ @@ -141,12 +141,12 @@ fn upper_ident_in_alternation_not_followed_by_colon() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: undefined reference: `Expr` + error: undefined reference; `Expr` | 1 | [(Expr) (Statement)] | ^^^^ - error: undefined reference: `Statement` + error: undefined reference; `Statement` | 1 | [(Expr) (Statement)] | ^^^^^^^^^ @@ -162,7 +162,7 @@ fn upper_ident_not_followed_by_equals_is_expression() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: undefined reference: `Expr` + error: undefined reference; `Expr` | 1 | (Expr) | ^^^^ @@ -178,7 +178,7 @@ fn bare_upper_ident_not_followed_by_equals_is_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | Expr | ^^^^ @@ -194,7 +194,7 @@ fn named_def_missing_equals() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | Expr (identifier) | ^^^^ @@ -212,7 +212,7 @@ fn unnamed_def_not_allowed_in_middle() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unnamed definition must be last: add a name: `Name = (first)` + error: unnamed definition must be last; add a name: `Name = (first)` | 1 | (first) | ^^^^^^^ @@ -230,12 +230,12 @@ fn multiple_unnamed_defs_errors_for_all_but_last() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unnamed definition must be last: add a name: `Name = (first)` + error: unnamed definition must be last; add a name: `Name = (first)` | 1 | (first) | ^^^^^^^ - error: unnamed definition must be last: add a name: `Name = (second)` + error: unnamed definition must be last; add a name: `Name = (second)` | 2 | (second) | ^^^^^^^^ @@ -251,12 +251,12 @@ fn capture_space_after_dot_is_anchor() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unnamed definition must be last: add a name: `Name = (identifier) @foo` + error: unnamed definition must be last; add a name: `Name = (identifier) @foo` | 1 | (identifier) @foo . (other) | ^^^^^^^^^^^^^^^^^ - error: unnamed definition must be last: add a name: `Name = .` + error: unnamed definition must be last; add a name: `Name = .` | 1 | (identifier) @foo . (other) | ^ @@ -270,7 +270,7 @@ fn def_name_lowercase_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition name starts with lowercase: definition names must start with uppercase + error: definition name starts with lowercase; definition names must start with uppercase | 1 | lowercase = (x) | ^^^^^^^^^ @@ -292,7 +292,7 @@ fn def_name_snake_case_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition name starts with lowercase: definition names must start with uppercase + error: definition name starts with lowercase; definition names must start with uppercase | 1 | my_expr = (identifier) | ^^^^^^^ @@ -314,7 +314,7 @@ fn def_name_kebab_case_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition name starts with lowercase: definition names must start with uppercase + error: definition name starts with lowercase; definition names must start with uppercase | 1 | my-expr = (identifier) | ^^^^^^^ @@ -336,7 +336,7 @@ fn def_name_dotted_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition name starts with lowercase: definition names must start with uppercase + error: definition name starts with lowercase; definition names must start with uppercase | 1 | my.expr = (identifier) | ^^^^^^^ @@ -356,7 +356,7 @@ fn def_name_with_underscores_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition name contains separators: definition names cannot contain separators + error: definition name contains separators; definition names cannot contain separators | 1 | Some_Thing = (x) | ^^^^^^^^^^ @@ -376,7 +376,7 @@ fn def_name_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition name contains separators: definition names cannot contain separators + error: definition name contains separators; definition names cannot contain separators | 1 | Some-Thing = (x) | ^^^^^^^^^^ @@ -398,7 +398,7 @@ fn capture_name_pascal_case_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name starts with uppercase: capture names must start with lowercase + error: capture name starts with uppercase; capture names must start with lowercase | 1 | (a) @Name | ^^^^ @@ -420,7 +420,7 @@ fn capture_name_pascal_case_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains hyphens: capture names cannot contain hyphens + error: capture name contains hyphens; capture names cannot contain hyphens | 1 | (a) @My-Name | ^^^^^^^ @@ -442,7 +442,7 @@ fn capture_name_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains hyphens: capture names cannot contain hyphens + error: capture name contains hyphens; capture names cannot contain hyphens | 1 | (a) @my-name | ^^^^^^^ @@ -464,7 +464,7 @@ fn capture_dotted_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains dots: capture names cannot contain dots + error: capture name contains dots; capture names cannot contain dots | 1 | (identifier) @foo.bar | ^^^^^^^ @@ -486,7 +486,7 @@ fn capture_dotted_multiple_parts() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains dots: capture names cannot contain dots + error: capture name contains dots; capture names cannot contain dots | 1 | (identifier) @foo.bar.baz | ^^^^^^^^^^^ @@ -508,7 +508,7 @@ fn capture_dotted_followed_by_field() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains dots: capture names cannot contain dots + error: capture name contains dots; capture names cannot contain dots | 1 | (node) @foo.bar name: (other) | ^^^^^^^ @@ -530,7 +530,7 @@ fn capture_space_after_dot_breaks_chain() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains dots: capture names cannot contain dots + error: capture name contains dots; capture names cannot contain dots | 1 | (identifier) @foo. bar | ^^^^ @@ -541,7 +541,7 @@ fn capture_space_after_dot_breaks_chain() { 1 + (identifier) @foo_ bar | - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (identifier) @foo. bar | ^^^ @@ -557,7 +557,7 @@ fn capture_hyphenated_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains hyphens: capture names cannot contain hyphens + error: capture name contains hyphens; capture names cannot contain hyphens | 1 | (identifier) @foo-bar | ^^^^^^^ @@ -579,7 +579,7 @@ fn capture_hyphenated_multiple() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains hyphens: capture names cannot contain hyphens + error: capture name contains hyphens; capture names cannot contain hyphens | 1 | (identifier) @foo-bar-baz | ^^^^^^^^^^^ @@ -601,7 +601,7 @@ fn capture_mixed_dots_and_hyphens() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains dots: capture names cannot contain dots + error: capture name contains dots; capture names cannot contain dots | 1 | (identifier) @foo.bar-baz | ^^^^^^^^^^^ @@ -623,7 +623,7 @@ fn field_name_pascal_case_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: field name starts with uppercase: field names must start with lowercase + error: field name starts with uppercase; field names must start with lowercase | 1 | (call Name: (a)) | ^^^^ @@ -643,7 +643,7 @@ fn field_name_with_dots_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: field name contains dots: field names cannot contain dots + error: field name contains dots; field names cannot contain dots | 1 | (call foo.bar: (x)) | ^^^^^^^ @@ -663,7 +663,7 @@ fn field_name_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: field name contains hyphens: field names cannot contain hyphens + error: field name contains hyphens; field names cannot contain hyphens | 1 | (call foo-bar: (x)) | ^^^^^^^ @@ -685,7 +685,7 @@ fn negated_field_with_upper_ident_parses() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: field name starts with uppercase: field names must start with lowercase + error: field name starts with uppercase; field names must start with lowercase | 1 | (call !Arguments) | ^^^^^^^^^ @@ -707,7 +707,7 @@ fn branch_label_snake_case_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: branch label contains separators: branch labels cannot contain separators + error: branch label contains separators; branch labels cannot contain separators | 1 | [My_branch: (a) Other: (b)] | ^^^^^^^^^ @@ -729,7 +729,7 @@ fn branch_label_kebab_case_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: branch label contains separators: branch labels cannot contain separators + error: branch label contains separators; branch labels cannot contain separators | 1 | [My-branch: (a) Other: (b)] | ^^^^^^^^^ @@ -751,7 +751,7 @@ fn branch_label_dotted_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: branch label contains separators: branch labels cannot contain separators + error: branch label contains separators; branch labels cannot contain separators | 1 | [My.branch: (a) Other: (b)] | ^^^^^^^^^ @@ -771,7 +771,7 @@ fn branch_label_with_underscores_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: branch label contains separators: branch labels cannot contain separators + error: branch label contains separators; branch labels cannot contain separators | 1 | [Some_Label: (x)] | ^^^^^^^^^^ @@ -791,7 +791,7 @@ fn branch_label_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: branch label contains separators: branch labels cannot contain separators + error: branch label contains separators; branch labels cannot contain separators | 1 | [Some-Label: (x)] | ^^^^^^^^^^ @@ -816,7 +816,7 @@ fn lowercase_branch_label() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) + error: lowercase branch label; tagged alternation labels must be Capitalized (they map to enum variants) | 2 | left: (a) | ^^^^ @@ -827,7 +827,7 @@ fn lowercase_branch_label() { 2 + Left: (a) | - error: lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) + error: lowercase branch label; tagged alternation labels must be Capitalized (they map to enum variants) | 3 | right: (b) | ^^^^^ @@ -849,7 +849,7 @@ fn lowercase_branch_label_suggests_capitalized() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) + error: lowercase branch label; tagged alternation labels must be Capitalized (they map to enum variants) | 1 | [first: (a) Second: (b)] | ^^^^^ @@ -869,7 +869,7 @@ fn mixed_case_branch_labels() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: lowercase branch label: tagged alternation labels must be Capitalized (they map to enum variants) + error: lowercase branch label; tagged alternation labels must be Capitalized (they map to enum variants) | 1 | [foo: (a) Bar: (b)] | ^^^ @@ -891,7 +891,7 @@ fn type_annotation_dotted_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: type name contains invalid characters: type names cannot contain dots or hyphens + error: type name contains invalid characters; type names cannot contain dots or hyphens | 1 | (a) @x :: My.Type | ^^^^^^^ @@ -913,7 +913,7 @@ fn type_annotation_kebab_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: type name contains invalid characters: type names cannot contain dots or hyphens + error: type name contains invalid characters; type names cannot contain dots or hyphens | 1 | (a) @x :: My-Type | ^^^^^^^ @@ -933,7 +933,7 @@ fn type_name_with_dots_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: type name contains invalid characters: type names cannot contain dots or hyphens + error: type name contains invalid characters; type names cannot contain dots or hyphens | 1 | (x) @name :: Some.Type | ^^^^^^^^^ @@ -953,7 +953,7 @@ fn type_name_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: type name contains invalid characters: type names cannot contain dots or hyphens + error: type name contains invalid characters; type names cannot contain dots or hyphens | 1 | (x) @name :: Some-Type | ^^^^^^^^^ @@ -973,7 +973,7 @@ fn comma_in_node_children() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation + error: invalid separator; ',' is not valid syntax; plotnik uses whitespace for separation | 1 | (node (a), (b)) | ^ @@ -993,7 +993,7 @@ fn comma_in_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation + error: invalid separator; ',' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a), (b), (c)] | ^ @@ -1004,7 +1004,7 @@ fn comma_in_alternation() { 1 + [(a) (b), (c)] | - error: invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation + error: invalid separator; ',' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a), (b), (c)] | ^ @@ -1024,7 +1024,7 @@ fn comma_in_sequence() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid separator: ',' is not valid syntax; plotnik uses whitespace for separation + error: invalid separator; ',' is not valid syntax; plotnik uses whitespace for separation | 1 | {(a), (b)} | ^ @@ -1044,7 +1044,7 @@ fn pipe_in_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation + error: invalid separator; '|' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a) | (b) | (c)] | ^ @@ -1055,7 +1055,7 @@ fn pipe_in_alternation() { 1 + [(a) (b) | (c)] | - error: invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation + error: invalid separator; '|' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a) | (b) | (c)] | ^ @@ -1077,7 +1077,7 @@ fn pipe_between_branches() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation + error: invalid separator; '|' is not valid syntax; plotnik uses whitespace for separation | 1 | [(a) | (b)] | ^ @@ -1097,7 +1097,7 @@ fn pipe_in_tree() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | (a | b) | ^ @@ -1111,7 +1111,7 @@ fn pipe_in_sequence() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid separator: '|' is not valid syntax; plotnik uses whitespace for separation + error: invalid separator; '|' is not valid syntax; plotnik uses whitespace for separation | 1 | {(a) | (b)} | ^ @@ -1131,7 +1131,7 @@ fn field_equals_typo() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid field syntax: '=' is not valid for field constraints + error: invalid field syntax; '=' is not valid for field constraints | 1 | (node name = (identifier)) | ^ @@ -1151,7 +1151,7 @@ fn field_equals_typo_no_space() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid field syntax: '=' is not valid for field constraints + error: invalid field syntax; '=' is not valid for field constraints | 1 | (node name=(identifier)) | ^ @@ -1171,7 +1171,7 @@ fn field_equals_typo_no_expression() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression: expected expression after field name + error: expected expression; after field name | 1 | (call name=) | ^ @@ -1187,7 +1187,7 @@ fn field_equals_typo_in_tree() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid field syntax: '=' is not valid for field constraints + error: invalid field syntax; '=' is not valid for field constraints | 1 | (call name = (identifier)) | ^ @@ -1207,7 +1207,7 @@ fn single_colon_type_annotation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid type annotation: single colon is not valid for type annotations + error: invalid type annotation; single colon is not valid for type annotations | 1 | (identifier) @name : Type | ^ @@ -1226,7 +1226,7 @@ fn single_colon_type_annotation_no_space() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid type annotation: single colon is not valid for type annotations + error: invalid type annotation; single colon is not valid for type annotations | 1 | (identifier) @name:Type | ^ @@ -1247,7 +1247,7 @@ fn single_colon_type_annotation_with_space() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid type annotation: single colon is not valid for type annotations + error: invalid type annotation; single colon is not valid for type annotations | 1 | (a) @x : Type | ^ @@ -1266,22 +1266,22 @@ fn single_colon_primitive_type() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture without target: capture without target + error: capture without target | 1 | @val : string | ^ - error: unexpected token: expected ':' to separate field name from its value + error: unexpected token; expected ':' to separate field name from its value | 1 | @val : string | ^ - error: expected expression: expected expression after field name + error: expected expression; after field name | 1 | @val : string | ^ - error: bare identifier not allowed: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) | 1 | @val : string | ^^^^^^ diff --git a/crates/plotnik-lib/src/query/recursion_tests.rs b/crates/plotnik-lib/src/query/recursion_tests.rs index 24089a66..2d602d53 100644 --- a/crates/plotnik-lib/src/query/recursion_tests.rs +++ b/crates/plotnik-lib/src/query/recursion_tests.rs @@ -24,7 +24,7 @@ fn no_escape_via_plus() { let query = Query::try_from("E = (call (E)+)").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: recursive pattern can never match: cycle `E` → `E` has no escape path + error: recursive pattern can never match; cycle `E` → `E` has no escape path | 1 | E = (call (E)+) | ^ @@ -51,7 +51,7 @@ fn recursion_in_tree_child() { let query = Query::try_from("E = (call (E))").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: recursive pattern can never match: cycle `E` → `E` has no escape path + error: recursive pattern can never match; cycle `E` → `E` has no escape path | 1 | E = (call (E)) | ^ @@ -96,7 +96,7 @@ fn mutual_recursion_no_escape() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: recursive pattern can never match: cycle `B` → `A` → `B` has no escape path + error: recursive pattern can never match; cycle `B` → `A` → `B` has no escape path | 1 | A = (foo (B)) | - `A` references `B` (completing cycle) @@ -162,7 +162,7 @@ fn cycle_ref_in_field() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: recursive pattern can never match: cycle `B` → `A` → `B` has no escape path + error: recursive pattern can never match; cycle `B` → `A` → `B` has no escape path | 1 | A = (foo body: (B)) | - `A` references `B` (completing cycle) @@ -182,7 +182,7 @@ fn cycle_ref_in_capture() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: recursive pattern can never match: cycle `B` → `A` → `B` has no escape path + error: recursive pattern can never match; cycle `B` → `A` → `B` has no escape path | 1 | A = (foo (B) @cap) | - `A` references `B` (completing cycle) @@ -202,7 +202,7 @@ fn cycle_ref_in_sequence() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: recursive pattern can never match: cycle `B` → `A` → `B` has no escape path + error: recursive pattern can never match; cycle `B` → `A` → `B` has no escape path | 1 | A = (foo {(x) (B)}) | - `A` references `B` (completing cycle) diff --git a/crates/plotnik-lib/src/query/symbol_table_tests.rs b/crates/plotnik-lib/src/query/symbol_table_tests.rs index 631f9ffc..9fcbbf76 100644 --- a/crates/plotnik-lib/src/query/symbol_table_tests.rs +++ b/crates/plotnik-lib/src/query/symbol_table_tests.rs @@ -49,7 +49,7 @@ fn undefined_reference() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: undefined reference: `Undefined` + error: undefined reference; `Undefined` | 1 | Call = (call_expression function: (Undefined)) | ^^^^^^^^^ @@ -78,7 +78,7 @@ fn mutual_recursion() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: recursive pattern can never match: cycle `B` → `A` → `B` has no escape path + error: recursive pattern can never match; cycle `B` → `A` → `B` has no escape path | 1 | A = (foo (B)) | - `A` references `B` (completing cycle) @@ -99,7 +99,7 @@ fn duplicate_definition() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: duplicate definition: `Expr` + error: duplicate definition; `Expr` | 2 | Expr = (other) | ^^^^ @@ -189,7 +189,7 @@ fn entry_point_undefined_reference() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: undefined reference: `Unknown` + error: undefined reference; `Unknown` | 1 | (call function: (Unknown)) | ^^^^^^^ @@ -237,17 +237,17 @@ fn multiple_undefined() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: undefined reference: `X` + error: undefined reference; `X` | 1 | (foo (X) (Y) (Z)) | ^ - error: undefined reference: `Y` + error: undefined reference; `Y` | 1 | (foo (X) (Y) (Z)) | ^ - error: undefined reference: `Z` + error: undefined reference; `Z` | 1 | (foo (X) (Y) (Z)) | ^ From 09b38042baeb9607e04d286b5a9f945b53694204 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 6 Dec 2025 12:07:34 -0300 Subject: [PATCH 13/15] Refactor diagnostic messages to be more clear and precise --- crates/plotnik-lib/src/diagnostics/message.rs | 124 +++++---- crates/plotnik-lib/src/diagnostics/tests.rs | 39 +-- crates/plotnik-lib/src/parser/ast_tests.rs | 2 +- crates/plotnik-lib/src/parser/grammar.rs | 113 ++++----- .../parser/tests/recovery/coverage_tests.rs | 8 +- .../parser/tests/recovery/incomplete_tests.rs | 40 +-- .../parser/tests/recovery/unclosed_tests.rs | 24 +- .../parser/tests/recovery/unexpected_tests.rs | 56 ++--- .../parser/tests/recovery/validation_tests.rs | 236 +++++++++--------- .../plotnik-lib/src/query/alt_kinds_tests.rs | 10 +- crates/plotnik-lib/src/query/mod_tests.rs | 2 +- .../plotnik-lib/src/query/recursion_tests.rs | 26 +- crates/plotnik-lib/src/query/shapes_tests.rs | 4 +- .../src/query/symbol_table_tests.rs | 14 +- 14 files changed, 350 insertions(+), 348 deletions(-) diff --git a/crates/plotnik-lib/src/diagnostics/message.rs b/crates/plotnik-lib/src/diagnostics/message.rs index db1d548b..940632b6 100644 --- a/crates/plotnik-lib/src/diagnostics/message.rs +++ b/crates/plotnik-lib/src/diagnostics/message.rs @@ -109,74 +109,94 @@ impl DiagnosticKind { /// Base message for this diagnostic kind, used when no custom message is provided. pub fn fallback_message(&self) -> &'static str { match self { - // Unclosed delimiters - Self::UnclosedTree => "unclosed tree", - Self::UnclosedSequence => "unclosed sequence", - Self::UnclosedAlternation => "unclosed alternation", - - // Expected token errors - Self::ExpectedExpression => "expected expression", - Self::ExpectedTypeName => "expected type name", - Self::ExpectedCaptureName => "expected capture name", + // Unclosed delimiters - clear about what's missing + Self::UnclosedTree => "missing closing `)`", + Self::UnclosedSequence => "missing closing `}`", + Self::UnclosedAlternation => "missing closing `]`", + + // Expected token errors - specific about what's needed + Self::ExpectedExpression => "expected an expression", + Self::ExpectedTypeName => "expected type name after `::`", + Self::ExpectedCaptureName => "expected name after `@`", Self::ExpectedFieldName => "expected field name", - Self::ExpectedSubtype => "expected subtype", - - // Invalid token/syntax usage - Self::EmptyTree => "empty tree expression", - Self::BareIdentifier => "bare identifier not allowed", - Self::InvalidSeparator => "invalid separator", - Self::InvalidFieldEquals => "invalid field syntax", - Self::InvalidSupertypeSyntax => "invalid supertype syntax", - Self::InvalidTypeAnnotationSyntax => "invalid type annotation syntax", - Self::ErrorTakesNoArguments => "(ERROR) takes no arguments", - Self::RefCannotHaveChildren => "reference cannot contain children", - Self::ErrorMissingOutsideParens => "ERROR/MISSING outside parentheses", - Self::UnsupportedPredicate => "unsupported predicate", + Self::ExpectedSubtype => "expected subtype after `/`", + + // Invalid syntax - explain what's wrong + Self::EmptyTree => "empty parentheses are not allowed", + Self::BareIdentifier => "bare identifier is not a valid expression", + Self::InvalidSeparator => "separators are not needed", + Self::InvalidFieldEquals => "use `:` for field constraints, not `=`", + Self::InvalidSupertypeSyntax => "supertype syntax not allowed on references", + Self::InvalidTypeAnnotationSyntax => "use `::` for type annotations, not `:`", + Self::ErrorTakesNoArguments => "`(ERROR)` cannot have child nodes", + Self::RefCannotHaveChildren => "references cannot have children", + Self::ErrorMissingOutsideParens => { + "`ERROR` and `MISSING` must be wrapped in parentheses" + } + Self::UnsupportedPredicate => "predicates like `#match?` are not supported", Self::UnexpectedToken => "unexpected token", - Self::CaptureWithoutTarget => "capture without target", - Self::LowercaseBranchLabel => "lowercase branch label", - - // Naming validation - Self::CaptureNameHasDots => "capture name contains dots", - Self::CaptureNameHasHyphens => "capture name contains hyphens", - Self::CaptureNameUppercase => "capture name starts with uppercase", - Self::DefNameLowercase => "definition name starts with lowercase", - Self::DefNameHasSeparators => "definition name contains separators", - Self::BranchLabelHasSeparators => "branch label contains separators", - Self::FieldNameHasDots => "field name contains dots", - Self::FieldNameHasHyphens => "field name contains hyphens", - Self::FieldNameUppercase => "field name starts with uppercase", - Self::TypeNameInvalidChars => "type name contains invalid characters", + Self::CaptureWithoutTarget => "`@` must follow an expression to capture", + Self::LowercaseBranchLabel => "branch labels must be capitalized", + + // Naming convention violations + Self::CaptureNameHasDots => "capture names cannot contain `.`", + Self::CaptureNameHasHyphens => "capture names cannot contain `-`", + Self::CaptureNameUppercase => "capture names must be lowercase", + Self::DefNameLowercase => "definition names must start uppercase", + Self::DefNameHasSeparators => "definition names must be PascalCase", + Self::BranchLabelHasSeparators => "branch labels must be PascalCase", + Self::FieldNameHasDots => "field names cannot contain `.`", + Self::FieldNameHasHyphens => "field names cannot contain `-`", + Self::FieldNameUppercase => "field names must be lowercase", + Self::TypeNameInvalidChars => "type names cannot contain `.` or `-`", // Semantic errors - Self::DuplicateDefinition => "duplicate definition", + Self::DuplicateDefinition => "name already defined", Self::UndefinedReference => "undefined reference", - Self::MixedAltBranches => "mixed tagged and untagged branches in alternation", - Self::RecursionNoEscape => "recursive pattern can never match", - Self::FieldSequenceValue => "field value must be a single node", + Self::MixedAltBranches => "cannot mix labeled and unlabeled branches", + Self::RecursionNoEscape => "infinite recursion detected", + Self::FieldSequenceValue => "field must match exactly one node", - // Structural observations - Self::UnnamedDefNotLast => "unnamed definition must be last", + // Structural + Self::UnnamedDefNotLast => "only the last definition can be unnamed", } } /// Template for custom messages. Contains `{}` placeholder for caller-provided detail. pub fn custom_message(&self) -> String { match self { - // Special cases: placeholder embedded in message - Self::RefCannotHaveChildren => "reference `{}` cannot contain children".to_string(), - Self::FieldSequenceValue => "field `{}` value must be a single node".to_string(), + // Special formatting for references + Self::RefCannotHaveChildren => { + "`{}` is a reference and cannot have children".to_string() + } + Self::FieldSequenceValue => { + "field `{}` must match exactly one node, not a sequence".to_string() + } + + // Semantic errors with name context + Self::DuplicateDefinition => "`{}` is already defined".to_string(), + Self::UndefinedReference => "`{}` is not defined".to_string(), + + // Recursion with cycle path + Self::RecursionNoEscape => "infinite recursion: {}".to_string(), + + // Alternation mixing + Self::MixedAltBranches => "cannot mix labeled and unlabeled branches: {}".to_string(), + + // Unclosed with context + Self::UnclosedTree | Self::UnclosedSequence | Self::UnclosedAlternation => { + format!("{}; {{}}", self.fallback_message()) + } - // Cases with backtick-wrapped placeholders - Self::DuplicateDefinition | Self::UndefinedReference => { - format!("{}; `{{}}`", self.fallback_message()) + // Type annotation specifics + Self::InvalidTypeAnnotationSyntax => { + "type annotations use `::`, not `:` — {}".to_string() } - // Cases where custom text differs from fallback - Self::InvalidTypeAnnotationSyntax => "invalid type annotation; {}".to_string(), - Self::MixedAltBranches => "mixed alternation; {}".to_string(), + // Named def ordering + Self::UnnamedDefNotLast => "only the last definition can be unnamed — {}".to_string(), - // Standard pattern: fallback + ": {}" + // Standard pattern: fallback + context _ => format!("{}; {{}}", self.fallback_message()), } } diff --git a/crates/plotnik-lib/src/diagnostics/tests.rs b/crates/plotnik-lib/src/diagnostics/tests.rs index eb760634..0f921aeb 100644 --- a/crates/plotnik-lib/src/diagnostics/tests.rs +++ b/crates/plotnik-lib/src/diagnostics/tests.rs @@ -63,7 +63,7 @@ fn builder_with_related() { assert_eq!(diagnostics.len(), 1); let result = diagnostics.printer("hello world!").render(); insta::assert_snapshot!(result, @r" - error: unclosed tree; primary + error: missing closing `)`; primary | 1 | hello world! | ^^^^^ ---- related info @@ -84,7 +84,7 @@ fn builder_with_fix() { let result = diagnostics.printer("hello world").render(); insta::assert_snapshot!(result, @r" - error: invalid field syntax; fixable + error: use `:` for field constraints, not `=`; fixable | 1 | hello world | ^^^^^ @@ -113,7 +113,7 @@ fn builder_with_all_options() { let result = diagnostics.printer("hello world stuff!").render(); insta::assert_snapshot!(result, @r" - error: unclosed tree; main error + error: missing closing `)`; main error | 1 | hello world stuff! | ^^^^^ ----- ----- and here @@ -164,7 +164,7 @@ fn printer_with_path() { let result = diagnostics.printer("hello world").path("test.pql").render(); insta::assert_snapshot!(result, @r" - error: undefined reference; `test error` + error: `test error` is not defined --> test.pql:1:1 | 1 | hello world @@ -185,7 +185,7 @@ fn printer_zero_width_span() { let result = diagnostics.printer("hello").render(); insta::assert_snapshot!(result, @r" - error: expected expression; zero width error + error: expected an expression; zero width error | 1 | hello | ^ @@ -206,7 +206,7 @@ fn printer_related_zero_width() { let result = diagnostics.printer("hello world!").render(); insta::assert_snapshot!(result, @r" - error: unclosed tree; primary + error: missing closing `)`; primary | 1 | hello world! | ^^^^^ - zero width related @@ -233,12 +233,12 @@ fn printer_multiple_diagnostics() { let result = diagnostics.printer("hello world!").render(); insta::assert_snapshot!(result, @r" - error: unclosed tree; first error + error: missing closing `)`; first error | 1 | hello world! | ^^^^^ - error: undefined reference; `second error` + error: `second error` is not defined | 1 | hello world! | ^^^^ @@ -294,19 +294,19 @@ fn diagnostic_kind_suppression_order() { fn diagnostic_kind_fallback_messages() { assert_eq!( DiagnosticKind::UnclosedTree.fallback_message(), - "unclosed tree" + "missing closing `)`" ); assert_eq!( DiagnosticKind::UnclosedSequence.fallback_message(), - "unclosed sequence" + "missing closing `}`" ); assert_eq!( DiagnosticKind::UnclosedAlternation.fallback_message(), - "unclosed alternation" + "missing closing `]`" ); assert_eq!( DiagnosticKind::ExpectedExpression.fallback_message(), - "expected expression" + "expected an expression" ); } @@ -314,26 +314,29 @@ fn diagnostic_kind_fallback_messages() { fn diagnostic_kind_custom_messages() { assert_eq!( DiagnosticKind::UnclosedTree.custom_message(), - "unclosed tree: {}" + "missing closing `)`; {}" ); assert_eq!( DiagnosticKind::UndefinedReference.custom_message(), - "undefined reference: `{}`" + "`{}` is not defined" ); } #[test] fn diagnostic_kind_message_rendering() { // No custom message → fallback - assert_eq!(DiagnosticKind::UnclosedTree.message(None), "unclosed tree"); + assert_eq!( + DiagnosticKind::UnclosedTree.message(None), + "missing closing `)`" + ); // With custom message → template applied assert_eq!( - DiagnosticKind::UnclosedTree.message(Some("expected ')'")), - "unclosed tree: expected ')'" + DiagnosticKind::UnclosedTree.message(Some("expected `)`")), + "missing closing `)`; expected `)`" ); assert_eq!( DiagnosticKind::UndefinedReference.message(Some("Foo")), - "undefined reference: `Foo`" + "`Foo` is not defined" ); } diff --git a/crates/plotnik-lib/src/parser/ast_tests.rs b/crates/plotnik-lib/src/parser/ast_tests.rs index b0b53ac5..eb18a1f6 100644 --- a/crates/plotnik-lib/src/parser/ast_tests.rs +++ b/crates/plotnik-lib/src/parser/ast_tests.rs @@ -262,7 +262,7 @@ fn ast_with_errors() { let query = Query::try_from("(call (Undefined))").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: undefined reference; `Undefined` + error: `Undefined` is not defined | 1 | (call (Undefined)) | ^^^^^^^^^ diff --git a/crates/plotnik-lib/src/parser/grammar.rs b/crates/plotnik-lib/src/parser/grammar.rs index de633daa..5513b4b9 100644 --- a/crates/plotnik-lib/src/parser/grammar.rs +++ b/crates/plotnik-lib/src/parser/grammar.rs @@ -45,7 +45,7 @@ impl Parser<'_> { let def_text = &self.source[usize::from(span.start())..usize::from(span.end())]; self.diagnostics .report(DiagnosticKind::UnnamedDefNotLast, *span) - .message(format!("add a name: `Name = {}`", def_text.trim())) + .message(format!("give it a name like `Name = {}`", def_text.trim())) .emit(); } } @@ -72,7 +72,7 @@ impl Parser<'_> { } else { self.error_msg( DiagnosticKind::ExpectedExpression, - "after '=' in named definition", + "after `=` in definition", ); } @@ -95,7 +95,7 @@ impl Parser<'_> { } else { self.error_and_bump_msg( DiagnosticKind::UnexpectedToken, - "expected an expression like (node), [choice], {sequence}, \"literal\", or _", + "try `(node)`, `[a b]`, `{a b}`, `\"literal\"`, or `_`", ); false } @@ -137,7 +137,7 @@ impl Parser<'_> { self.error_and_bump(DiagnosticKind::ErrorMissingOutsideParens); } _ => { - self.error_and_bump_msg(DiagnosticKind::UnexpectedToken, "expected an expression"); + self.error_and_bump_msg(DiagnosticKind::UnexpectedToken, "not a valid expression"); } } @@ -201,7 +201,7 @@ impl Parser<'_> { _ => { self.error_msg( DiagnosticKind::ExpectedSubtype, - "after '/' (e.g., expression/binary_expression)", + "e.g., `expression/binary_expression`", ); } } @@ -282,14 +282,14 @@ impl Parser<'_> { loop { if self.eof() { let (construct, delim, kind) = match until { - SyntaxKind::ParenClose => ("tree", "')'", DiagnosticKind::UnclosedTree), - SyntaxKind::BraceClose => ("sequence", "'}'", DiagnosticKind::UnclosedSequence), + SyntaxKind::ParenClose => ("tree", "`)`", DiagnosticKind::UnclosedTree), + SyntaxKind::BraceClose => ("sequence", "`}`", DiagnosticKind::UnclosedSequence), _ => panic!( "parse_children: unexpected delimiter {:?} (only ParenClose/BraceClose supported)", until ), }; - let msg = format!("unclosed {construct}; expected {delim}"); + let msg = format!("expected {delim}"); let open = self.delimiter_stack.last().unwrap_or_else(|| { panic!( "parse_children: unclosed {construct} at EOF but delimiter_stack is empty \ @@ -328,7 +328,7 @@ impl Parser<'_> { } self.error_and_bump_msg( DiagnosticKind::UnexpectedToken, - "expected a child expression or closing delimiter", + "not valid inside a node — try `(child)` or close with `)`", ); } } @@ -350,7 +350,7 @@ impl Parser<'_> { fn parse_alt_children(&mut self) { loop { if self.eof() { - let msg = "unclosed alternation; expected ']'"; + let msg = "expected `]`"; let open = self.delimiter_stack.last().unwrap_or_else(|| { panic!( "parse_alt_children: unclosed alternation at EOF but delimiter_stack is empty \ @@ -399,7 +399,7 @@ impl Parser<'_> { } self.error_and_bump_msg( DiagnosticKind::UnexpectedToken, - "expected a child expression or closing delimiter", + "not valid inside alternation — try `(node)` or close with `]`", ); } } @@ -419,7 +419,7 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr(); } else { - self.error_msg(DiagnosticKind::ExpectedExpression, "after branch label"); + self.error_msg(DiagnosticKind::ExpectedExpression, "after `Label:`"); } self.finish_node(); @@ -436,8 +436,8 @@ impl Parser<'_> { self.error_with_fix( DiagnosticKind::LowercaseBranchLabel, span, - "tagged alternation labels must be Capitalized (they map to enum variants)", - format!("capitalize as `{}`", capitalized), + "branch labels map to enum variants", + format!("use `{}`", capitalized), capitalized, ); @@ -448,7 +448,7 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr(); } else { - self.error_msg(DiagnosticKind::ExpectedExpression, "after branch label"); + self.error_msg(DiagnosticKind::ExpectedExpression, "after `label:`"); } self.finish_node(); @@ -532,7 +532,7 @@ impl Parser<'_> { } else { self.error_msg( DiagnosticKind::ExpectedTypeName, - "after '::' (e.g., ::MyType or ::string)", + "e.g., `::MyType` or `::string`", ); } @@ -551,8 +551,8 @@ impl Parser<'_> { self.error_with_fix( DiagnosticKind::InvalidTypeAnnotationSyntax, span, - "single colon is not valid for type annotations", - "use '::'", + "single `:` looks like a field", + "use `::`", "::", ); @@ -579,10 +579,7 @@ impl Parser<'_> { self.expect(SyntaxKind::Negation, "'!' for negated field"); if self.peek() != SyntaxKind::Id { - self.error_msg( - DiagnosticKind::ExpectedFieldName, - "after '!' (e.g., !value)", - ); + self.error_msg(DiagnosticKind::ExpectedFieldName, "e.g., `!value`"); self.finish_node(); return; } @@ -605,7 +602,7 @@ impl Parser<'_> { // Bare identifiers are not valid expressions; trees require parentheses self.error_and_bump_msg( DiagnosticKind::BareIdentifier, - "nodes must be enclosed in parentheses, e.g., (identifier)", + "wrap in parentheses: `(identifier)`", ); } } @@ -629,7 +626,7 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr_no_suffix(); } else { - self.error_msg(DiagnosticKind::ExpectedExpression, "after field name"); + self.error_msg(DiagnosticKind::ExpectedExpression, "after `field:`"); } self.finish_node(); @@ -645,8 +642,8 @@ impl Parser<'_> { self.error_with_fix( DiagnosticKind::InvalidFieldEquals, span, - "'=' is not valid for field constraints", - "use ':'", + "this isn't a definition", + "use `:`", ":", ); self.bump(); @@ -654,7 +651,7 @@ impl Parser<'_> { if EXPR_FIRST.contains(self.peek()) { self.parse_expr(); } else { - self.error_msg(DiagnosticKind::ExpectedExpression, "after field name"); + self.error_msg(DiagnosticKind::ExpectedExpression, "after `field =`"); } self.finish_node(); @@ -676,11 +673,8 @@ impl Parser<'_> { self.error_with_fix( DiagnosticKind::InvalidSeparator, span, - format!( - "'{}' is not valid syntax; plotnik uses whitespace for separation", - char_name - ), - "remove separator", + format!("plotnik uses whitespace, not `{}`", char_name), + "remove", "", ); self.skip_token(); @@ -712,8 +706,8 @@ impl Parser<'_> { self.error_with_fix( DiagnosticKind::CaptureNameHasDots, span, - "capture names cannot contain dots", - format!("captures become struct fields; use @{} instead", suggested), + "captures become struct fields", + format!("use `@{}`", suggested), suggested, ); return; @@ -725,8 +719,8 @@ impl Parser<'_> { self.error_with_fix( DiagnosticKind::CaptureNameHasHyphens, span, - "capture names cannot contain hyphens", - format!("captures become struct fields; use @{} instead", suggested), + "captures become struct fields", + format!("use `@{}`", suggested), suggested, ); return; @@ -737,11 +731,8 @@ impl Parser<'_> { self.error_with_fix( DiagnosticKind::CaptureNameUppercase, span, - "capture names must start with lowercase", - format!( - "capture names must be snake_case; use @{} instead", - suggested - ), + "captures become struct fields", + format!("use `@{}`", suggested), suggested, ); } @@ -754,11 +745,8 @@ impl Parser<'_> { self.error_with_fix( DiagnosticKind::DefNameLowercase, span, - "definition names must start with uppercase", - format!( - "definition names must be PascalCase; use {} instead", - suggested - ), + "definitions map to types", + format!("use `{}`", suggested), suggested, ); return; @@ -769,11 +757,8 @@ impl Parser<'_> { self.error_with_fix( DiagnosticKind::DefNameHasSeparators, span, - "definition names cannot contain separators", - format!( - "definition names must be PascalCase; use {} instead", - suggested - ), + "definitions map to types", + format!("use `{}`", suggested), suggested, ); } @@ -786,11 +771,8 @@ impl Parser<'_> { self.error_with_fix( DiagnosticKind::BranchLabelHasSeparators, span, - "branch labels cannot contain separators", - format!( - "branch labels must be PascalCase; use {}: instead", - suggested - ), + "branch labels map to enum variants", + format!("use `{}:`", suggested), format!("{}:", suggested), ); } @@ -804,8 +786,8 @@ impl Parser<'_> { self.error_with_fix( DiagnosticKind::FieldNameHasDots, span, - "field names cannot contain dots", - format!("field names must be snake_case; use {}: instead", suggested), + "field names become struct fields", + format!("use `{}:`", suggested), format!("{}:", suggested), ); return; @@ -817,8 +799,8 @@ impl Parser<'_> { self.error_with_fix( DiagnosticKind::FieldNameHasHyphens, span, - "field names cannot contain hyphens", - format!("field names must be snake_case; use {}: instead", suggested), + "field names become struct fields", + format!("use `{}:`", suggested), format!("{}:", suggested), ); return; @@ -829,8 +811,8 @@ impl Parser<'_> { self.error_with_fix( DiagnosticKind::FieldNameUppercase, span, - "field names must start with lowercase", - format!("field names must be snake_case; use {}: instead", suggested), + "field names become struct fields", + format!("use `{}:`", suggested), format!("{}:", suggested), ); } @@ -843,11 +825,8 @@ impl Parser<'_> { self.error_with_fix( DiagnosticKind::TypeNameInvalidChars, span, - "type names cannot contain dots or hyphens", - format!( - "type names cannot contain separators; use ::{} instead", - suggested - ), + "type annotations map to types", + format!("use `::{}`", suggested), format!("::{}", suggested), ); } 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 adbba77b..52c33c7f 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs @@ -131,12 +131,12 @@ fn named_def_missing_equals_with_garbage() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | Expr ^^^ (identifier) | ^^^^ - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 1 | Expr ^^^ (identifier) | ^^^ @@ -153,12 +153,12 @@ fn named_def_missing_equals_recovers_to_next_def() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | Broken ^^^ | ^^^^^^ - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 1 | Broken ^^^ | ^^^ 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 5e89ee2b..ea7bc7a9 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs @@ -10,7 +10,7 @@ fn missing_capture_name() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected capture name + error: expected name after `@` | 1 | (identifier) @ | ^ @@ -26,7 +26,7 @@ fn missing_field_value() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression; after field name + error: expected an expression; after `field:` | 1 | (call name:) | ^ @@ -40,7 +40,7 @@ fn named_def_eof_after_equals() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression; after '=' in named definition + error: expected an expression; after `=` in definition | 1 | Expr = | ^ @@ -56,7 +56,7 @@ fn missing_type_name() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected type name; after '::' (e.g., ::MyType or ::string) + error: expected type name after `::`; e.g., `::MyType` or `::string` | 1 | (identifier) @name :: | ^ @@ -72,7 +72,7 @@ fn missing_negated_field_name() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected field name; after '!' (e.g., !value) + error: expected field name; e.g., `!value` | 1 | (call !) | ^ @@ -88,7 +88,7 @@ fn missing_subtype() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected subtype; after '/' (e.g., expression/binary_expression) + error: expected subtype after `/`; e.g., `expression/binary_expression` | 1 | (expression/) | ^ @@ -104,7 +104,7 @@ fn tagged_branch_missing_expression() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression; after branch label + error: expected an expression; after `Label:` | 1 | [Label:] | ^ @@ -118,7 +118,7 @@ fn type_annotation_missing_name_at_eof() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected type name; after '::' (e.g., ::MyType or ::string) + error: expected type name after `::`; e.g., `::MyType` or `::string` | 1 | (a) @x :: | ^ @@ -132,7 +132,7 @@ fn type_annotation_missing_name_with_bracket() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected type name; after '::' (e.g., ::MyType or ::string) + error: expected type name after `::`; e.g., `::MyType` or `::string` | 1 | [(a) @x :: ] | ^ @@ -148,7 +148,7 @@ fn type_annotation_invalid_token_after() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected type name; after '::' (e.g., ::MyType or ::string) + error: expected type name after `::`; e.g., `::MyType` or `::string` | 1 | (identifier) @name :: ( | ^ @@ -164,7 +164,7 @@ fn field_value_is_garbage() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression; after field name + error: expected an expression; after `field:` | 1 | (call name: %%%) | ^^^ @@ -180,7 +180,7 @@ fn capture_with_invalid_char() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected capture name + error: expected name after `@` | 1 | (identifier) @123 | ^^^ @@ -194,7 +194,7 @@ fn bare_capture_at_eof_triggers_sync() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture without target + error: `@` must follow an expression to capture | 1 | @ | ^ @@ -210,12 +210,12 @@ fn bare_capture_at_root() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture without target + error: `@` must follow an expression to capture | 1 | @name | ^ - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | @name | ^^^^ @@ -231,7 +231,7 @@ fn capture_at_start_of_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | [@x (a)] | ^ @@ -247,12 +247,12 @@ fn mixed_valid_invalid_captures() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture without target + error: `@` must follow an expression to capture | 1 | (a) @ok @ @name | ^ - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | (a) @ok @ @name | ^^^^ @@ -268,7 +268,7 @@ fn field_equals_typo_missing_value() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression; after field name + error: expected an expression; after `field =` | 1 | (call name = ) | ^ @@ -282,7 +282,7 @@ fn lowercase_branch_label_missing_expression() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression; after branch label + error: expected an expression; after `label:` | 1 | [label:] | ^ diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs index 14a71240..deeb308b 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs @@ -10,7 +10,7 @@ fn missing_paren() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree; unclosed tree; expected ')' + error: missing closing `)`; expected `)` | 1 | (identifier | -^^^^^^^^^^ @@ -28,7 +28,7 @@ fn missing_bracket() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed alternation; unclosed alternation; expected ']' + error: missing closing `]`; expected `]` | 1 | [(identifier) (string) | -^^^^^^^^^^^^^^^^^^^^^ @@ -46,7 +46,7 @@ fn missing_brace() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed sequence; unclosed sequence; expected '}' + error: missing closing `}`; expected `}` | 1 | {(a) (b) | -^^^^^^^ @@ -64,7 +64,7 @@ fn nested_unclosed() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree; unclosed tree; expected ')' + error: missing closing `)`; expected `)` | 1 | (a (b (c) | -^^^^^ @@ -82,7 +82,7 @@ fn deeply_nested_unclosed() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree; unclosed tree; expected ')' + error: missing closing `)`; expected `)` | 1 | (a (b (c (d | -^ @@ -100,7 +100,7 @@ fn unclosed_alternation_nested() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree; unclosed tree; expected ')' + error: missing closing `)`; expected `)` | 1 | [(a) (b | -^ @@ -118,7 +118,7 @@ fn empty_parens() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: empty tree expression + error: empty parentheses are not allowed | 1 | () | ^ @@ -135,7 +135,7 @@ fn unclosed_tree_shows_open_location() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree; unclosed tree; expected ')' + error: missing closing `)`; expected `)` | 1 | (call | ^ tree started here @@ -157,7 +157,7 @@ fn unclosed_alternation_shows_open_location() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed alternation; unclosed alternation; expected ']' + error: missing closing `]`; expected `]` | 1 | [ | ^ alternation started here @@ -180,7 +180,7 @@ fn unclosed_sequence_shows_open_location() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed sequence; unclosed sequence; expected '}' + error: missing closing `}`; expected `}` | 1 | { | ^ sequence started here @@ -199,7 +199,7 @@ fn unclosed_double_quote_string() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unclosed tree; unclosed tree; expected ')' + error: missing closing `)`; expected `)` | 1 | (call "foo) | -^^^^^^^^^^ @@ -215,7 +215,7 @@ fn unclosed_single_quote_string() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unclosed tree; unclosed tree; expected ')' + error: missing closing `)`; expected `)` | 1 | (call 'foo) | -^^^^^^^^^^ diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs index 18421b18..eb38bd33 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs @@ -10,7 +10,7 @@ fn unexpected_token() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 1 | (identifier) ^^^ (string) | ^^^ @@ -26,7 +26,7 @@ fn multiple_consecutive_garbage() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 1 | ^^^ $$$ %%% (ok) | ^^^ @@ -42,7 +42,7 @@ fn garbage_at_start() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 1 | ^^^ (a) | ^^^ @@ -58,7 +58,7 @@ fn only_garbage() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 1 | ^^^ $$$ | ^^^ @@ -74,7 +74,7 @@ fn garbage_inside_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token; expected a child expression or closing delimiter + error: unexpected token; not valid inside alternation — try `(node)` or close with `]` | 1 | [(a) ^^^ (b)] | ^^^ @@ -90,7 +90,7 @@ fn garbage_inside_node() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected capture name + error: expected name after `@` | 1 | (a (b) @@@ (c)) (d) | ^ @@ -106,12 +106,12 @@ fn xml_tag_garbage() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 1 |
(identifier)
| ^^^^^ - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 1 |
(identifier)
| ^^^^^^ @@ -127,7 +127,7 @@ fn xml_self_closing() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 1 |
(a) | ^^^^^ @@ -143,12 +143,12 @@ fn predicate_unsupported() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | (a (#eq? @x "foo") b) | ^ - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | (a (#eq? @x "foo") b) | ^ @@ -164,12 +164,12 @@ fn predicate_match() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unsupported predicate + error: predicates like `#match?` are not supported | 1 | (identifier) #match? @name "test" | ^^^^^^^ - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | (identifier) #match? @name "test" | ^^^^ @@ -183,7 +183,7 @@ fn predicate_in_tree() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | (function #eq? @name "test") | ^^^^ @@ -199,7 +199,7 @@ fn predicate_in_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token; expected a child expression or closing delimiter + error: unexpected token; not valid inside alternation — try `(node)` or close with `]` | 1 | [(a) #eq? (b)] | ^^^^ @@ -215,7 +215,7 @@ fn predicate_in_sequence() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unsupported predicate + error: predicates like `#match?` are not supported | 1 | {(a) #set! (b)} | ^^^^^ @@ -233,7 +233,7 @@ fn multiline_garbage_recovery() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 3 | b) | ^ @@ -249,7 +249,7 @@ fn top_level_garbage_recovery() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 1 | Expr = (a) ^^^ Expr2 = (b) | ^^^ @@ -269,12 +269,12 @@ fn multiple_definitions_with_garbage_between() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 2 | ^^^ | ^^^ - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 4 | $$$ | ^^^ @@ -290,7 +290,7 @@ fn alternation_recovery_to_capture() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | [^^^ @name] | ^^^^ @@ -306,7 +306,7 @@ fn comma_between_defs() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 1 | A = (a), B = (b) | ^ @@ -320,7 +320,7 @@ fn bare_colon_in_tree() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unexpected token; expected a child expression or closing delimiter + error: unexpected token; not valid inside a node — try `(child)` or close with `)` | 1 | (a : (b)) | ^ @@ -339,7 +339,7 @@ fn paren_close_inside_alternation() { 1 | [(a) ) (b)] | ^ - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 1 | [(a) ) (b)] | ^ @@ -358,7 +358,7 @@ fn bracket_close_inside_sequence() { 1 | {(a) ] (b)} | ^ - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 1 | {(a) ] (b)} | ^ @@ -377,7 +377,7 @@ fn paren_close_inside_sequence() { 1 | {(a) ) (b)} | ^ - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 1 | {(a) ) (b)} | ^ @@ -391,7 +391,7 @@ fn single_colon_type_annotation_followed_by_non_id() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 1 | (a) @x : (b) | ^ @@ -405,7 +405,7 @@ fn single_colon_type_annotation_at_eof() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ + error: unexpected token; try `(node)`, `[a b]`, `{a b}`, `"literal"`, or `_` | 1 | (a) @x : | ^ 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 827f2194..5121c7be 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs @@ -11,7 +11,7 @@ fn ref_with_children_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: reference `Expr` cannot contain children + error: `Expr` is a reference and cannot have children | 2 | (Expr (child)) | ^^^^^^^ @@ -28,7 +28,7 @@ fn ref_with_multiple_children_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: reference `Expr` cannot contain children + error: `Expr` is a reference and cannot have children | 2 | (Expr (a) (b) @cap) | ^^^^^^^^^^^^ @@ -45,7 +45,7 @@ fn ref_with_field_children_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: reference `Expr` cannot contain children + error: `Expr` is a reference and cannot have children | 2 | (Expr name: (identifier)) | ^^^^^^^^^^^^^^^^^^ @@ -59,7 +59,7 @@ fn reference_with_supertype_syntax_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid supertype syntax + error: supertype syntax not allowed on references | 1 | (RefName/subtype) | ^ @@ -75,7 +75,7 @@ fn mixed_tagged_and_untagged() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: mixed tagged and untagged branches in alternation + error: cannot mix labeled and unlabeled branches | 1 | [Tagged: (a) (b) Another: (c)] | ------ ^^^ @@ -93,7 +93,7 @@ fn error_with_unexpected_content() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: (ERROR) takes no arguments + error: `(ERROR)` cannot have child nodes | 1 | (ERROR (something)) | ^ @@ -109,7 +109,7 @@ fn bare_error_keyword() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: ERROR/MISSING outside parentheses + error: `ERROR` and `MISSING` must be wrapped in parentheses | 1 | ERROR | ^^^^^ @@ -125,7 +125,7 @@ fn bare_missing_keyword() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: ERROR/MISSING outside parentheses + error: `ERROR` and `MISSING` must be wrapped in parentheses | 1 | MISSING | ^^^^^^^ @@ -141,12 +141,12 @@ fn upper_ident_in_alternation_not_followed_by_colon() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: undefined reference; `Expr` + error: `Expr` is not defined | 1 | [(Expr) (Statement)] | ^^^^ - error: undefined reference; `Statement` + error: `Statement` is not defined | 1 | [(Expr) (Statement)] | ^^^^^^^^^ @@ -162,7 +162,7 @@ fn upper_ident_not_followed_by_equals_is_expression() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: undefined reference; `Expr` + error: `Expr` is not defined | 1 | (Expr) | ^^^^ @@ -178,7 +178,7 @@ fn bare_upper_ident_not_followed_by_equals_is_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | Expr | ^^^^ @@ -194,7 +194,7 @@ fn named_def_missing_equals() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | Expr (identifier) | ^^^^ @@ -212,7 +212,7 @@ fn unnamed_def_not_allowed_in_middle() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unnamed definition must be last; add a name: `Name = (first)` + error: only the last definition can be unnamed — give it a name like `Name = (first)` | 1 | (first) | ^^^^^^^ @@ -230,12 +230,12 @@ fn multiple_unnamed_defs_errors_for_all_but_last() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unnamed definition must be last; add a name: `Name = (first)` + error: only the last definition can be unnamed — give it a name like `Name = (first)` | 1 | (first) | ^^^^^^^ - error: unnamed definition must be last; add a name: `Name = (second)` + error: only the last definition can be unnamed — give it a name like `Name = (second)` | 2 | (second) | ^^^^^^^^ @@ -251,12 +251,12 @@ fn capture_space_after_dot_is_anchor() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: unnamed definition must be last; add a name: `Name = (identifier) @foo` + error: only the last definition can be unnamed — give it a name like `Name = (identifier) @foo` | 1 | (identifier) @foo . (other) | ^^^^^^^^^^^^^^^^^ - error: unnamed definition must be last; add a name: `Name = .` + error: only the last definition can be unnamed — give it a name like `Name = .` | 1 | (identifier) @foo . (other) | ^ @@ -270,12 +270,12 @@ fn def_name_lowercase_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition name starts with lowercase; definition names must start with uppercase + error: definition names must start uppercase; definitions map to types | 1 | lowercase = (x) | ^^^^^^^^^ | - help: definition names must be PascalCase; use Lowercase instead + help: use `Lowercase` | 1 - lowercase = (x) 1 + Lowercase = (x) @@ -292,12 +292,12 @@ fn def_name_snake_case_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition name starts with lowercase; definition names must start with uppercase + error: definition names must start uppercase; definitions map to types | 1 | my_expr = (identifier) | ^^^^^^^ | - help: definition names must be PascalCase; use MyExpr instead + help: use `MyExpr` | 1 - my_expr = (identifier) 1 + MyExpr = (identifier) @@ -314,12 +314,12 @@ fn def_name_kebab_case_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition name starts with lowercase; definition names must start with uppercase + error: definition names must start uppercase; definitions map to types | 1 | my-expr = (identifier) | ^^^^^^^ | - help: definition names must be PascalCase; use MyExpr instead + help: use `MyExpr` | 1 - my-expr = (identifier) 1 + MyExpr = (identifier) @@ -336,12 +336,12 @@ fn def_name_dotted_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition name starts with lowercase; definition names must start with uppercase + error: definition names must start uppercase; definitions map to types | 1 | my.expr = (identifier) | ^^^^^^^ | - help: definition names must be PascalCase; use MyExpr instead + help: use `MyExpr` | 1 - my.expr = (identifier) 1 + MyExpr = (identifier) @@ -356,12 +356,12 @@ fn def_name_with_underscores_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition name contains separators; definition names cannot contain separators + error: definition names must be PascalCase; definitions map to types | 1 | Some_Thing = (x) | ^^^^^^^^^^ | - help: definition names must be PascalCase; use SomeThing instead + help: use `SomeThing` | 1 - Some_Thing = (x) 1 + SomeThing = (x) @@ -376,12 +376,12 @@ fn def_name_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: definition name contains separators; definition names cannot contain separators + error: definition names must be PascalCase; definitions map to types | 1 | Some-Thing = (x) | ^^^^^^^^^^ | - help: definition names must be PascalCase; use SomeThing instead + help: use `SomeThing` | 1 - Some-Thing = (x) 1 + SomeThing = (x) @@ -398,12 +398,12 @@ fn capture_name_pascal_case_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name starts with uppercase; capture names must start with lowercase + error: capture names must be lowercase; captures become struct fields | 1 | (a) @Name | ^^^^ | - help: capture names must be snake_case; use @name instead + help: use `@name` | 1 - (a) @Name 1 + (a) @name @@ -420,12 +420,12 @@ fn capture_name_pascal_case_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains hyphens; capture names cannot contain hyphens + error: capture names cannot contain `-`; captures become struct fields | 1 | (a) @My-Name | ^^^^^^^ | - help: captures become struct fields; use @my_name instead + help: use `@my_name` | 1 - (a) @My-Name 1 + (a) @my_name @@ -442,12 +442,12 @@ fn capture_name_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains hyphens; capture names cannot contain hyphens + error: capture names cannot contain `-`; captures become struct fields | 1 | (a) @my-name | ^^^^^^^ | - help: captures become struct fields; use @my_name instead + help: use `@my_name` | 1 - (a) @my-name 1 + (a) @my_name @@ -464,12 +464,12 @@ fn capture_dotted_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains dots; capture names cannot contain dots + error: capture names cannot contain `.`; captures become struct fields | 1 | (identifier) @foo.bar | ^^^^^^^ | - help: captures become struct fields; use @foo_bar instead + help: use `@foo_bar` | 1 - (identifier) @foo.bar 1 + (identifier) @foo_bar @@ -486,12 +486,12 @@ fn capture_dotted_multiple_parts() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains dots; capture names cannot contain dots + error: capture names cannot contain `.`; captures become struct fields | 1 | (identifier) @foo.bar.baz | ^^^^^^^^^^^ | - help: captures become struct fields; use @foo_bar_baz instead + help: use `@foo_bar_baz` | 1 - (identifier) @foo.bar.baz 1 + (identifier) @foo_bar_baz @@ -508,12 +508,12 @@ fn capture_dotted_followed_by_field() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains dots; capture names cannot contain dots + error: capture names cannot contain `.`; captures become struct fields | 1 | (node) @foo.bar name: (other) | ^^^^^^^ | - help: captures become struct fields; use @foo_bar instead + help: use `@foo_bar` | 1 - (node) @foo.bar name: (other) 1 + (node) @foo_bar name: (other) @@ -530,18 +530,18 @@ fn capture_space_after_dot_breaks_chain() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains dots; capture names cannot contain dots + error: capture names cannot contain `.`; captures become struct fields | 1 | (identifier) @foo. bar | ^^^^ | - help: captures become struct fields; use @foo_ instead + help: use `@foo_` | 1 - (identifier) @foo. bar 1 + (identifier) @foo_ bar | - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | (identifier) @foo. bar | ^^^ @@ -557,12 +557,12 @@ fn capture_hyphenated_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains hyphens; capture names cannot contain hyphens + error: capture names cannot contain `-`; captures become struct fields | 1 | (identifier) @foo-bar | ^^^^^^^ | - help: captures become struct fields; use @foo_bar instead + help: use `@foo_bar` | 1 - (identifier) @foo-bar 1 + (identifier) @foo_bar @@ -579,12 +579,12 @@ fn capture_hyphenated_multiple() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains hyphens; capture names cannot contain hyphens + error: capture names cannot contain `-`; captures become struct fields | 1 | (identifier) @foo-bar-baz | ^^^^^^^^^^^ | - help: captures become struct fields; use @foo_bar_baz instead + help: use `@foo_bar_baz` | 1 - (identifier) @foo-bar-baz 1 + (identifier) @foo_bar_baz @@ -601,12 +601,12 @@ fn capture_mixed_dots_and_hyphens() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture name contains dots; capture names cannot contain dots + error: capture names cannot contain `.`; captures become struct fields | 1 | (identifier) @foo.bar-baz | ^^^^^^^^^^^ | - help: captures become struct fields; use @foo_bar_baz instead + help: use `@foo_bar_baz` | 1 - (identifier) @foo.bar-baz 1 + (identifier) @foo_bar_baz @@ -623,12 +623,12 @@ fn field_name_pascal_case_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: field name starts with uppercase; field names must start with lowercase + error: field names must be lowercase; field names become struct fields | 1 | (call Name: (a)) | ^^^^ | - help: field names must be snake_case; use name: instead + help: use `name:` | 1 - (call Name: (a)) 1 + (call name:: (a)) @@ -643,12 +643,12 @@ fn field_name_with_dots_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: field name contains dots; field names cannot contain dots + error: field names cannot contain `.`; field names become struct fields | 1 | (call foo.bar: (x)) | ^^^^^^^ | - help: field names must be snake_case; use foo_bar: instead + help: use `foo_bar:` | 1 - (call foo.bar: (x)) 1 + (call foo_bar:: (x)) @@ -663,12 +663,12 @@ fn field_name_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: field name contains hyphens; field names cannot contain hyphens + error: field names cannot contain `-`; field names become struct fields | 1 | (call foo-bar: (x)) | ^^^^^^^ | - help: field names must be snake_case; use foo_bar: instead + help: use `foo_bar:` | 1 - (call foo-bar: (x)) 1 + (call foo_bar:: (x)) @@ -685,12 +685,12 @@ fn negated_field_with_upper_ident_parses() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: field name starts with uppercase; field names must start with lowercase + error: field names must be lowercase; field names become struct fields | 1 | (call !Arguments) | ^^^^^^^^^ | - help: field names must be snake_case; use arguments: instead + help: use `arguments:` | 1 - (call !Arguments) 1 + (call !arguments:) @@ -707,12 +707,12 @@ fn branch_label_snake_case_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: branch label contains separators; branch labels cannot contain separators + error: branch labels must be PascalCase; branch labels map to enum variants | 1 | [My_branch: (a) Other: (b)] | ^^^^^^^^^ | - help: branch labels must be PascalCase; use MyBranch: instead + help: use `MyBranch:` | 1 - [My_branch: (a) Other: (b)] 1 + [MyBranch:: (a) Other: (b)] @@ -729,12 +729,12 @@ fn branch_label_kebab_case_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: branch label contains separators; branch labels cannot contain separators + error: branch labels must be PascalCase; branch labels map to enum variants | 1 | [My-branch: (a) Other: (b)] | ^^^^^^^^^ | - help: branch labels must be PascalCase; use MyBranch: instead + help: use `MyBranch:` | 1 - [My-branch: (a) Other: (b)] 1 + [MyBranch:: (a) Other: (b)] @@ -751,12 +751,12 @@ fn branch_label_dotted_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: branch label contains separators; branch labels cannot contain separators + error: branch labels must be PascalCase; branch labels map to enum variants | 1 | [My.branch: (a) Other: (b)] | ^^^^^^^^^ | - help: branch labels must be PascalCase; use MyBranch: instead + help: use `MyBranch:` | 1 - [My.branch: (a) Other: (b)] 1 + [MyBranch:: (a) Other: (b)] @@ -771,12 +771,12 @@ fn branch_label_with_underscores_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: branch label contains separators; branch labels cannot contain separators + error: branch labels must be PascalCase; branch labels map to enum variants | 1 | [Some_Label: (x)] | ^^^^^^^^^^ | - help: branch labels must be PascalCase; use SomeLabel: instead + help: use `SomeLabel:` | 1 - [Some_Label: (x)] 1 + [SomeLabel:: (x)] @@ -791,12 +791,12 @@ fn branch_label_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: branch label contains separators; branch labels cannot contain separators + error: branch labels must be PascalCase; branch labels map to enum variants | 1 | [Some-Label: (x)] | ^^^^^^^^^^ | - help: branch labels must be PascalCase; use SomeLabel: instead + help: use `SomeLabel:` | 1 - [Some-Label: (x)] 1 + [SomeLabel:: (x)] @@ -816,23 +816,23 @@ fn lowercase_branch_label() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: lowercase branch label; tagged alternation labels must be Capitalized (they map to enum variants) + error: branch labels must be capitalized; branch labels map to enum variants | 2 | left: (a) | ^^^^ | - help: capitalize as `Left` + help: use `Left` | 2 - left: (a) 2 + Left: (a) | - error: lowercase branch label; tagged alternation labels must be Capitalized (they map to enum variants) + error: branch labels must be capitalized; branch labels map to enum variants | 3 | right: (b) | ^^^^^ | - help: capitalize as `Right` + help: use `Right` | 3 - right: (b) 3 + Right: (b) @@ -849,12 +849,12 @@ fn lowercase_branch_label_suggests_capitalized() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: lowercase branch label; tagged alternation labels must be Capitalized (they map to enum variants) + error: branch labels must be capitalized; branch labels map to enum variants | 1 | [first: (a) Second: (b)] | ^^^^^ | - help: capitalize as `First` + help: use `First` | 1 - [first: (a) Second: (b)] 1 + [First: (a) Second: (b)] @@ -869,12 +869,12 @@ fn mixed_case_branch_labels() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: lowercase branch label; tagged alternation labels must be Capitalized (they map to enum variants) + error: branch labels must be capitalized; branch labels map to enum variants | 1 | [foo: (a) Bar: (b)] | ^^^ | - help: capitalize as `Foo` + help: use `Foo` | 1 - [foo: (a) Bar: (b)] 1 + [Foo: (a) Bar: (b)] @@ -891,12 +891,12 @@ fn type_annotation_dotted_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: type name contains invalid characters; type names cannot contain dots or hyphens + error: type names cannot contain `.` or `-`; type annotations map to types | 1 | (a) @x :: My.Type | ^^^^^^^ | - help: type names cannot contain separators; use ::MyType instead + help: use `::MyType` | 1 - (a) @x :: My.Type 1 + (a) @x :: ::MyType @@ -913,12 +913,12 @@ fn type_annotation_kebab_suggests_pascal() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: type name contains invalid characters; type names cannot contain dots or hyphens + error: type names cannot contain `.` or `-`; type annotations map to types | 1 | (a) @x :: My-Type | ^^^^^^^ | - help: type names cannot contain separators; use ::MyType instead + help: use `::MyType` | 1 - (a) @x :: My-Type 1 + (a) @x :: ::MyType @@ -933,12 +933,12 @@ fn type_name_with_dots_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: type name contains invalid characters; type names cannot contain dots or hyphens + error: type names cannot contain `.` or `-`; type annotations map to types | 1 | (x) @name :: Some.Type | ^^^^^^^^^ | - help: type names cannot contain separators; use ::SomeType instead + help: use `::SomeType` | 1 - (x) @name :: Some.Type 1 + (x) @name :: ::SomeType @@ -953,12 +953,12 @@ fn type_name_with_hyphens_error() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: type name contains invalid characters; type names cannot contain dots or hyphens + error: type names cannot contain `.` or `-`; type annotations map to types | 1 | (x) @name :: Some-Type | ^^^^^^^^^ | - help: type names cannot contain separators; use ::SomeType instead + help: use `::SomeType` | 1 - (x) @name :: Some-Type 1 + (x) @name :: ::SomeType @@ -973,12 +973,12 @@ fn comma_in_node_children() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid separator; ',' is not valid syntax; plotnik uses whitespace for separation + error: separators are not needed; plotnik uses whitespace, not `,` | 1 | (node (a), (b)) | ^ | - help: remove separator + help: remove | 1 - (node (a), (b)) 1 + (node (a) (b)) @@ -993,23 +993,23 @@ fn comma_in_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid separator; ',' is not valid syntax; plotnik uses whitespace for separation + error: separators are not needed; plotnik uses whitespace, not `,` | 1 | [(a), (b), (c)] | ^ | - help: remove separator + help: remove | 1 - [(a), (b), (c)] 1 + [(a) (b), (c)] | - error: invalid separator; ',' is not valid syntax; plotnik uses whitespace for separation + error: separators are not needed; plotnik uses whitespace, not `,` | 1 | [(a), (b), (c)] | ^ | - help: remove separator + help: remove | 1 - [(a), (b), (c)] 1 + [(a), (b) (c)] @@ -1024,12 +1024,12 @@ fn comma_in_sequence() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid separator; ',' is not valid syntax; plotnik uses whitespace for separation + error: separators are not needed; plotnik uses whitespace, not `,` | 1 | {(a), (b)} | ^ | - help: remove separator + help: remove | 1 - {(a), (b)} 1 + {(a) (b)} @@ -1044,23 +1044,23 @@ fn pipe_in_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid separator; '|' is not valid syntax; plotnik uses whitespace for separation + error: separators are not needed; plotnik uses whitespace, not `|` | 1 | [(a) | (b) | (c)] | ^ | - help: remove separator + help: remove | 1 - [(a) | (b) | (c)] 1 + [(a) (b) | (c)] | - error: invalid separator; '|' is not valid syntax; plotnik uses whitespace for separation + error: separators are not needed; plotnik uses whitespace, not `|` | 1 | [(a) | (b) | (c)] | ^ | - help: remove separator + help: remove | 1 - [(a) | (b) | (c)] 1 + [(a) | (b) (c)] @@ -1077,12 +1077,12 @@ fn pipe_between_branches() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid separator; '|' is not valid syntax; plotnik uses whitespace for separation + error: separators are not needed; plotnik uses whitespace, not `|` | 1 | [(a) | (b)] | ^ | - help: remove separator + help: remove | 1 - [(a) | (b)] 1 + [(a) (b)] @@ -1097,7 +1097,7 @@ fn pipe_in_tree() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | (a | b) | ^ @@ -1111,12 +1111,12 @@ fn pipe_in_sequence() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid separator; '|' is not valid syntax; plotnik uses whitespace for separation + error: separators are not needed; plotnik uses whitespace, not `|` | 1 | {(a) | (b)} | ^ | - help: remove separator + help: remove | 1 - {(a) | (b)} 1 + {(a) (b)} @@ -1131,12 +1131,12 @@ fn field_equals_typo() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid field syntax; '=' is not valid for field constraints + error: use `:` for field constraints, not `=`; this isn't a definition | 1 | (node name = (identifier)) | ^ | - help: use ':' + help: use `:` | 1 - (node name = (identifier)) 1 + (node name : (identifier)) @@ -1151,12 +1151,12 @@ fn field_equals_typo_no_space() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid field syntax; '=' is not valid for field constraints + error: use `:` for field constraints, not `=`; this isn't a definition | 1 | (node name=(identifier)) | ^ | - help: use ':' + help: use `:` | 1 - (node name=(identifier)) 1 + (node name:(identifier)) @@ -1171,7 +1171,7 @@ fn field_equals_typo_no_expression() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected expression; after field name + error: expected an expression; after `field =` | 1 | (call name=) | ^ @@ -1187,12 +1187,12 @@ fn field_equals_typo_in_tree() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid field syntax; '=' is not valid for field constraints + error: use `:` for field constraints, not `=`; this isn't a definition | 1 | (call name = (identifier)) | ^ | - help: use ':' + help: use `:` | 1 - (call name = (identifier)) 1 + (call name : (identifier)) @@ -1207,12 +1207,12 @@ fn single_colon_type_annotation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid type annotation; single colon is not valid for type annotations + error: type annotations use `::`, not `:` — single `:` looks like a field | 1 | (identifier) @name : Type | ^ | - help: use '::' + help: use `::` | 1 | (identifier) @name :: Type | + @@ -1226,12 +1226,12 @@ fn single_colon_type_annotation_no_space() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid type annotation; single colon is not valid for type annotations + error: type annotations use `::`, not `:` — single `:` looks like a field | 1 | (identifier) @name:Type | ^ | - help: use '::' + help: use `::` | 1 | (identifier) @name::Type | + @@ -1247,12 +1247,12 @@ fn single_colon_type_annotation_with_space() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: invalid type annotation; single colon is not valid for type annotations + error: type annotations use `::`, not `:` — single `:` looks like a field | 1 | (a) @x : Type | ^ | - help: use '::' + help: use `::` | 1 | (a) @x :: Type | + @@ -1266,7 +1266,7 @@ fn single_colon_primitive_type() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: capture without target + error: `@` must follow an expression to capture | 1 | @val : string | ^ @@ -1276,12 +1276,12 @@ fn single_colon_primitive_type() { 1 | @val : string | ^ - error: expected expression; after field name + error: expected an expression; after `field:` | 1 | @val : string | ^ - error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | @val : string | ^^^^^^ diff --git a/crates/plotnik-lib/src/query/alt_kinds_tests.rs b/crates/plotnik-lib/src/query/alt_kinds_tests.rs index a1844921..307806b5 100644 --- a/crates/plotnik-lib/src/query/alt_kinds_tests.rs +++ b/crates/plotnik-lib/src/query/alt_kinds_tests.rs @@ -35,7 +35,7 @@ fn mixed_alternation_tagged_first() { let query = Query::try_from("[A: (a) (b)]").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: mixed tagged and untagged branches in alternation + error: cannot mix labeled and unlabeled branches | 1 | [A: (a) (b)] | - ^^^ @@ -57,7 +57,7 @@ fn mixed_alternation_untagged_first() { .unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: mixed tagged and untagged branches in alternation + error: cannot mix labeled and unlabeled branches | 3 | (a) | ^^^ @@ -71,7 +71,7 @@ fn nested_mixed_alternation() { let query = Query::try_from("(call [A: (a) (b)])").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: mixed tagged and untagged branches in alternation + error: cannot mix labeled and unlabeled branches | 1 | (call [A: (a) (b)]) | - ^^^ @@ -85,14 +85,14 @@ fn multiple_mixed_alternations() { let query = Query::try_from("(foo [A: (a) (b)] [C: (c) (d)])").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: mixed tagged and untagged branches in alternation + error: cannot mix labeled and unlabeled branches | 1 | (foo [A: (a) (b)] [C: (c) (d)]) | - ^^^ | | | tagged branch here - error: mixed tagged and untagged branches in alternation + error: cannot mix labeled and unlabeled branches | 1 | (foo [A: (a) (b)] [C: (c) (d)]) | - ^^^ diff --git a/crates/plotnik-lib/src/query/mod_tests.rs b/crates/plotnik-lib/src/query/mod_tests.rs index 50e2fd2c..ed72754e 100644 --- a/crates/plotnik-lib/src/query/mod_tests.rs +++ b/crates/plotnik-lib/src/query/mod_tests.rs @@ -17,7 +17,7 @@ fn parse_error() { fn resolution_error() { let q = Query::try_from("(call (Undefined))").unwrap(); assert!(!q.is_valid()); - assert!(q.dump_diagnostics().contains("undefined reference")); + assert!(q.dump_diagnostics().contains("is not defined")); } #[test] diff --git a/crates/plotnik-lib/src/query/recursion_tests.rs b/crates/plotnik-lib/src/query/recursion_tests.rs index 2d602d53..54204720 100644 --- a/crates/plotnik-lib/src/query/recursion_tests.rs +++ b/crates/plotnik-lib/src/query/recursion_tests.rs @@ -24,7 +24,7 @@ fn no_escape_via_plus() { let query = Query::try_from("E = (call (E)+)").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: recursive pattern can never match; cycle `E` → `E` has no escape path + error: infinite recursion: cycle `E` → `E` has no escape path | 1 | E = (call (E)+) | ^ @@ -51,7 +51,7 @@ fn recursion_in_tree_child() { let query = Query::try_from("E = (call (E))").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: recursive pattern can never match; cycle `E` → `E` has no escape path + error: infinite recursion: cycle `E` → `E` has no escape path | 1 | E = (call (E)) | ^ @@ -64,21 +64,21 @@ fn recursion_in_tree_child() { fn recursion_in_field() { let query = Query::try_from("E = (call body: (E))").unwrap(); assert!(!query.is_valid()); - assert!(query.dump_diagnostics().contains("recursive pattern")); + assert!(query.dump_diagnostics().contains("infinite recursion")); } #[test] fn recursion_in_capture() { let query = Query::try_from("E = (call (E) @inner)").unwrap(); assert!(!query.is_valid()); - assert!(query.dump_diagnostics().contains("recursive pattern")); + assert!(query.dump_diagnostics().contains("infinite recursion")); } #[test] fn recursion_in_sequence() { let query = Query::try_from("E = (call {(a) (E)})").unwrap(); assert!(!query.is_valid()); - assert!(query.dump_diagnostics().contains("recursive pattern")); + assert!(query.dump_diagnostics().contains("infinite recursion")); } #[test] @@ -96,7 +96,7 @@ fn mutual_recursion_no_escape() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: recursive pattern can never match; cycle `B` → `A` → `B` has no escape path + error: infinite recursion: cycle `B` → `A` → `B` has no escape path | 1 | A = (foo (B)) | - `A` references `B` (completing cycle) @@ -126,7 +126,7 @@ fn three_way_cycle_no_escape() { "#}; let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); - assert!(query.dump_diagnostics().contains("recursive pattern")); + assert!(query.dump_diagnostics().contains("infinite recursion")); } #[test] @@ -150,7 +150,7 @@ fn diamond_dependency() { "#}; let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); - assert!(query.dump_diagnostics().contains("recursive pattern")); + assert!(query.dump_diagnostics().contains("infinite recursion")); } #[test] @@ -162,7 +162,7 @@ fn cycle_ref_in_field() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: recursive pattern can never match; cycle `B` → `A` → `B` has no escape path + error: infinite recursion: cycle `B` → `A` → `B` has no escape path | 1 | A = (foo body: (B)) | - `A` references `B` (completing cycle) @@ -182,7 +182,7 @@ fn cycle_ref_in_capture() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: recursive pattern can never match; cycle `B` → `A` → `B` has no escape path + error: infinite recursion: cycle `B` → `A` → `B` has no escape path | 1 | A = (foo (B) @cap) | - `A` references `B` (completing cycle) @@ -202,7 +202,7 @@ fn cycle_ref_in_sequence() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: recursive pattern can never match; cycle `B` → `A` → `B` has no escape path + error: infinite recursion: cycle `B` → `A` → `B` has no escape path | 1 | A = (foo {(x) (B)}) | - `A` references `B` (completing cycle) @@ -231,7 +231,7 @@ fn cycle_with_plus_no_escape() { "#}; let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); - assert!(query.dump_diagnostics().contains("recursive pattern")); + assert!(query.dump_diagnostics().contains("infinite recursion")); } #[test] @@ -303,7 +303,7 @@ fn no_escape_tree_all_recursive() { "#}; let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); - assert!(query.dump_diagnostics().contains("recursive pattern")); + assert!(query.dump_diagnostics().contains("infinite recursion")); } #[test] diff --git a/crates/plotnik-lib/src/query/shapes_tests.rs b/crates/plotnik-lib/src/query/shapes_tests.rs index 36357dc4..9c132b68 100644 --- a/crates/plotnik-lib/src/query/shapes_tests.rs +++ b/crates/plotnik-lib/src/query/shapes_tests.rs @@ -168,7 +168,7 @@ fn field_with_seq_error() { NamedNode¹ b "); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: field `name` value must be a single node + error: field `name` must match exactly one node, not a sequence | 1 | (call name: {(a) (b)}) | ^^^^^^^^^ @@ -195,7 +195,7 @@ fn field_with_ref_to_seq_error() { Ref⁺ X "); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: field `name` value must be a single node + error: field `name` must match exactly one node, not a sequence | 2 | (call name: (X)) | ^^^ diff --git a/crates/plotnik-lib/src/query/symbol_table_tests.rs b/crates/plotnik-lib/src/query/symbol_table_tests.rs index 9fcbbf76..d127c3a0 100644 --- a/crates/plotnik-lib/src/query/symbol_table_tests.rs +++ b/crates/plotnik-lib/src/query/symbol_table_tests.rs @@ -49,7 +49,7 @@ fn undefined_reference() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: undefined reference; `Undefined` + error: `Undefined` is not defined | 1 | Call = (call_expression function: (Undefined)) | ^^^^^^^^^ @@ -78,7 +78,7 @@ fn mutual_recursion() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: recursive pattern can never match; cycle `B` → `A` → `B` has no escape path + error: infinite recursion: cycle `B` → `A` → `B` has no escape path | 1 | A = (foo (B)) | - `A` references `B` (completing cycle) @@ -99,7 +99,7 @@ fn duplicate_definition() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: duplicate definition; `Expr` + error: `Expr` is already defined | 2 | Expr = (other) | ^^^^ @@ -189,7 +189,7 @@ fn entry_point_undefined_reference() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: undefined reference; `Unknown` + error: `Unknown` is not defined | 1 | (call function: (Unknown)) | ^^^^^^^ @@ -237,17 +237,17 @@ fn multiple_undefined() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: undefined reference; `X` + error: `X` is not defined | 1 | (foo (X) (Y) (Z)) | ^ - error: undefined reference; `Y` + error: `Y` is not defined | 1 | (foo (X) (Y) (Z)) | ^ - error: undefined reference; `Z` + error: `Z` is not defined | 1 | (foo (X) (Y) (Z)) | ^ From 8192f89b5615770c12b6ebeadf8cfde1cc3d473a Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 6 Dec 2025 12:19:34 -0300 Subject: [PATCH 14/15] Update query tests to use insta snapshots for diagnostics --- crates/plotnik-lib/src/query/mod_tests.rs | 23 +++- .../plotnik-lib/src/query/recursion_tests.rs | 117 ++++++++++++++++-- 2 files changed, 130 insertions(+), 10 deletions(-) diff --git a/crates/plotnik-lib/src/query/mod_tests.rs b/crates/plotnik-lib/src/query/mod_tests.rs index ed72754e..fa8c4f79 100644 --- a/crates/plotnik-lib/src/query/mod_tests.rs +++ b/crates/plotnik-lib/src/query/mod_tests.rs @@ -10,19 +10,36 @@ fn valid_query() { fn parse_error() { let q = Query::try_from("(unclosed").unwrap(); assert!(!q.is_valid()); - assert!(q.dump_diagnostics().contains("expected")); + insta::assert_snapshot!(q.dump_diagnostics(), @r" + error: missing closing `)`; expected `)` + | + 1 | (unclosed + | -^^^^^^^^ + | | + | tree started here + "); } #[test] fn resolution_error() { let q = Query::try_from("(call (Undefined))").unwrap(); assert!(!q.is_valid()); - assert!(q.dump_diagnostics().contains("is not defined")); + insta::assert_snapshot!(q.dump_diagnostics(), @r" + error: `Undefined` is not defined + | + 1 | (call (Undefined)) + | ^^^^^^^^^ + "); } #[test] fn combined_errors() { let q = Query::try_from("(call (Undefined) extra)").unwrap(); assert!(!q.is_valid()); - assert!(!q.diagnostics().is_empty()); + insta::assert_snapshot!(q.dump_diagnostics(), @r" + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` + | + 1 | (call (Undefined) extra) + | ^^^^^ + "); } diff --git a/crates/plotnik-lib/src/query/recursion_tests.rs b/crates/plotnik-lib/src/query/recursion_tests.rs index 54204720..81fadca3 100644 --- a/crates/plotnik-lib/src/query/recursion_tests.rs +++ b/crates/plotnik-lib/src/query/recursion_tests.rs @@ -4,25 +4,30 @@ use indoc::indoc; #[test] fn escape_via_alternation() { let query = Query::try_from("E = [(x) (call (E))]").unwrap(); + assert!(query.is_valid()); } #[test] fn escape_via_optional() { let query = Query::try_from("E = (call (E)?)").unwrap(); + assert!(query.is_valid()); } #[test] fn escape_via_star() { let query = Query::try_from("E = (call (E)*)").unwrap(); + assert!(query.is_valid()); } #[test] fn no_escape_via_plus() { let query = Query::try_from("E = (call (E)+)").unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_diagnostics(), @r" error: infinite recursion: cycle `E` → `E` has no escape path | @@ -36,6 +41,7 @@ fn no_escape_via_plus() { #[test] fn escape_via_empty_tree() { let query = Query::try_from("E = [(call) (E)]").unwrap(); + assert!(query.is_valid()); } @@ -49,7 +55,9 @@ fn lazy_quantifiers_same_as_greedy() { #[test] fn recursion_in_tree_child() { let query = Query::try_from("E = (call (E))").unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_diagnostics(), @r" error: infinite recursion: cycle `E` → `E` has no escape path | @@ -63,27 +71,55 @@ fn recursion_in_tree_child() { #[test] fn recursion_in_field() { let query = Query::try_from("E = (call body: (E))").unwrap(); + assert!(!query.is_valid()); - assert!(query.dump_diagnostics().contains("infinite recursion")); + + insta::assert_snapshot!(query.dump_diagnostics(), @r" + error: infinite recursion: cycle `E` → `E` has no escape path + | + 1 | E = (call body: (E)) + | ^ + | | + | `E` references itself + "); } #[test] fn recursion_in_capture() { let query = Query::try_from("E = (call (E) @inner)").unwrap(); + assert!(!query.is_valid()); - assert!(query.dump_diagnostics().contains("infinite recursion")); + + insta::assert_snapshot!(query.dump_diagnostics(), @r" + error: infinite recursion: cycle `E` → `E` has no escape path + | + 1 | E = (call (E) @inner) + | ^ + | | + | `E` references itself + "); } #[test] fn recursion_in_sequence() { let query = Query::try_from("E = (call {(a) (E)})").unwrap(); + assert!(!query.is_valid()); - assert!(query.dump_diagnostics().contains("infinite recursion")); + + insta::assert_snapshot!(query.dump_diagnostics(), @r" + error: infinite recursion: cycle `E` → `E` has no escape path + | + 1 | E = (call {(a) (E)}) + | ^ + | | + | `E` references itself + "); } #[test] fn recursion_through_multiple_children() { let query = Query::try_from("E = [(x) (call (a) (E))]").unwrap(); + assert!(query.is_valid()); } @@ -94,7 +130,9 @@ fn mutual_recursion_no_escape() { B = (bar (A)) "#}; let query = Query::try_from(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_diagnostics(), @r" error: infinite recursion: cycle `B` → `A` → `B` has no escape path | @@ -114,6 +152,7 @@ fn mutual_recursion_one_has_escape() { B = (bar (A)) "#}; let query = Query::try_from(input).unwrap(); + assert!(query.is_valid()); } @@ -125,8 +164,21 @@ fn three_way_cycle_no_escape() { C = (c (A)) "#}; let query = Query::try_from(input).unwrap(); + assert!(!query.is_valid()); - assert!(query.dump_diagnostics().contains("infinite recursion")); + + insta::assert_snapshot!(query.dump_diagnostics(), @r" + error: infinite recursion: cycle `C` → `B` → `A` → `C` has no escape path + | + 1 | A = (a (B)) + | - `A` references `B` + 2 | B = (b (C)) + | - `B` references `C` (completing cycle) + 3 | C = (c (A)) + | ^ + | | + | `C` references `A` + "); } #[test] @@ -137,6 +189,7 @@ fn three_way_cycle_one_has_escape() { C = (c (A)) "#}; let query = Query::try_from(input).unwrap(); + assert!(query.is_valid()); } @@ -149,8 +202,22 @@ fn diamond_dependency() { D = (d (A)) "#}; let query = Query::try_from(input).unwrap(); + assert!(!query.is_valid()); - assert!(query.dump_diagnostics().contains("infinite recursion")); + + insta::assert_snapshot!(query.dump_diagnostics(), @r" + error: infinite recursion: cycle `C` → `D` → `B` → `A` → `C` has no escape path + | + 1 | A = (a [(B) (C)]) + | - `A` references `C` (completing cycle) + 2 | B = (b (D)) + 3 | C = (c (D)) + | ^ + | | + | `C` references `D` + 4 | D = (d (A)) + | - `D` references `A` + "); } #[test] @@ -160,7 +227,9 @@ fn cycle_ref_in_field() { B = (bar (A)) "#}; let query = Query::try_from(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_diagnostics(), @r" error: infinite recursion: cycle `B` → `A` → `B` has no escape path | @@ -180,7 +249,9 @@ fn cycle_ref_in_capture() { B = (bar (A)) "#}; let query = Query::try_from(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_diagnostics(), @r" error: infinite recursion: cycle `B` → `A` → `B` has no escape path | @@ -200,7 +271,9 @@ fn cycle_ref_in_sequence() { B = (bar (A)) "#}; let query = Query::try_from(input).unwrap(); + assert!(!query.is_valid()); + insta::assert_snapshot!(query.dump_diagnostics(), @r" error: infinite recursion: cycle `B` → `A` → `B` has no escape path | @@ -220,6 +293,7 @@ fn cycle_with_quantifier_escape() { B = (bar (A)) "#}; let query = Query::try_from(input).unwrap(); + assert!(query.is_valid()); } @@ -230,8 +304,19 @@ fn cycle_with_plus_no_escape() { B = (bar (A)) "#}; let query = Query::try_from(input).unwrap(); + assert!(!query.is_valid()); - assert!(query.dump_diagnostics().contains("infinite recursion")); + + insta::assert_snapshot!(query.dump_diagnostics(), @r" + error: infinite recursion: cycle `B` → `A` → `B` has no escape path + | + 1 | A = (foo (B)+) + | - `A` references `B` (completing cycle) + 2 | B = (bar (A)) + | ^ + | | + | `B` references `A` + "); } #[test] @@ -241,6 +326,7 @@ fn non_recursive_reference() { Tree = (call (Leaf)) "#}; let query = Query::try_from(input).unwrap(); + assert!(query.is_valid()); } @@ -251,12 +337,14 @@ fn entry_point_uses_recursive_def() { (program (E)) "#}; let query = Query::try_from(input).unwrap(); + assert!(query.is_valid()); } #[test] fn direct_self_ref_in_alternation() { let query = Query::try_from("E = [(E) (x)]").unwrap(); + assert!(query.is_valid()); } @@ -266,6 +354,7 @@ fn escape_via_literal_string() { A = [(A) "escape"] "#}; let query = Query::try_from(input).unwrap(); + assert!(query.is_valid()); } @@ -275,6 +364,7 @@ fn escape_via_wildcard() { A = [(A) _] "#}; let query = Query::try_from(input).unwrap(); + assert!(query.is_valid()); } @@ -284,6 +374,7 @@ fn escape_via_childless_tree() { A = [(A) (leaf)] "#}; let query = Query::try_from(input).unwrap(); + assert!(query.is_valid()); } @@ -293,6 +384,7 @@ fn escape_via_anchor() { A = (foo . [(A) (x)]) "#}; let query = Query::try_from(input).unwrap(); + assert!(query.is_valid()); } @@ -302,8 +394,17 @@ fn no_escape_tree_all_recursive() { A = (foo (A)) "#}; let query = Query::try_from(input).unwrap(); + assert!(!query.is_valid()); - assert!(query.dump_diagnostics().contains("infinite recursion")); + + insta::assert_snapshot!(query.dump_diagnostics(), @r" + error: infinite recursion: cycle `A` → `A` has no escape path + | + 1 | A = (foo (A)) + | ^ + | | + | `A` references itself + "); } #[test] @@ -312,6 +413,7 @@ fn escape_in_capture_inner() { A = [(x)@cap (foo (A))] "#}; let query = Query::try_from(input).unwrap(); + assert!(query.is_valid()); } @@ -321,5 +423,6 @@ fn ref_in_quantifier_plus_no_escape() { A = (foo (A)+) "#}; let query = Query::try_from(input).unwrap(); + assert!(!query.is_valid()); } From f148a78e805112c884cbc8fded6156bb75279ec7 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 6 Dec 2025 12:39:54 -0300 Subject: [PATCH 15/15] Refine suppression mechanism --- crates/plotnik-lib/src/diagnostics/mod.rs | 36 ++++++++++--------- .../parser/tests/recovery/incomplete_tests.rs | 31 ++++++++++++---- .../parser/tests/recovery/unexpected_tests.rs | 32 +++++++++++++---- .../parser/tests/recovery/validation_tests.rs | 26 ++++++++++---- crates/plotnik-lib/src/query/mod_tests.rs | 5 +++ 5 files changed, 93 insertions(+), 37 deletions(-) diff --git a/crates/plotnik-lib/src/diagnostics/mod.rs b/crates/plotnik-lib/src/diagnostics/mod.rs index 5365d755..eee2f302 100644 --- a/crates/plotnik-lib/src/diagnostics/mod.rs +++ b/crates/plotnik-lib/src/diagnostics/mod.rs @@ -83,9 +83,10 @@ impl Diagnostics { /// /// Suppression rules: /// 1. Containment: when error A's suppression_range contains error B's display range, - /// and A has higher priority, suppress B + /// and A has higher priority, suppress B (only for structural errors) /// 2. Same position: when spans start at the same position, root-cause errors suppress structural ones /// 3. Consequence errors (UnnamedDefNotLast) suppressed when any other error exists + /// 4. Adjacent: when error A ends exactly where error B starts, A suppresses B pub(crate) fn filtered(&self) -> Vec { if self.messages.is_empty() { return Vec::new(); @@ -110,11 +111,12 @@ impl Diagnostics { continue; } - // Rule 1: Suppression range containment - // If A's suppression_range contains B's display range, A can suppress B - if suppression_range_contains(a.suppression_range, b.range) - && a.kind.suppresses(&b.kind) - { + // Rule 1: Structural error containment + // Only unclosed delimiters can suppress distant errors, because they cause + // cascading parse failures throughout the tree + let contains = a.suppression_range.start() <= b.range.start() + && b.range.end() <= a.suppression_range.end(); + if contains && a.kind.is_structural_error() && a.kind.suppresses(&b.kind) { suppressed[j] = true; continue; } @@ -122,15 +124,25 @@ impl Diagnostics { // Rule 2: Same start position if a.range.start() == b.range.start() { // Root cause errors (Expected*) suppress structural errors (Unclosed*) + // even though structural errors have higher enum priority. This is because + // ExpectedExpression is the actual mistake; UnclosedTree is a consequence. if a.kind.is_root_cause_error() && b.kind.is_structural_error() { suppressed[j] = true; continue; } - // Otherwise, fall back to normal priority (lower discriminant wins) if a.kind.suppresses(&b.kind) { suppressed[j] = true; + continue; } } + + // Rule 4: Adjacent position - when A ends exactly where B starts, + // B is likely a consequence of A (e.g., `@x` where `@` is unexpected + // and `x` would be reported as bare identifier). + // Priority doesn't matter here - position determines causality. + if a.range.end() == b.range.start() { + suppressed[j] = true; + } } } @@ -212,13 +224,3 @@ impl<'a> DiagnosticBuilder<'a> { self.diagnostics.messages.push(self.message); } } - -/// Check if a suppression range contains a display range. -/// -/// For suppression purposes, we use non-strict containment: the inner range -/// can start at the same position as the outer range. This allows errors -/// reported at the same position but with different suppression contexts -/// to properly suppress each other. -fn suppression_range_contains(suppression: TextRange, display: TextRange) -> bool { - suppression.start() <= display.start() && display.end() <= suppression.end() -} 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 ea7bc7a9..9bf981a2 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs @@ -214,11 +214,6 @@ fn bare_capture_at_root() { | 1 | @name | ^ - - error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` - | - 1 | @name - | ^^^^ "); } @@ -231,10 +226,10 @@ fn capture_at_start_of_alternation() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` + error: unexpected token; not valid inside alternation — try `(node)` or close with `]` | 1 | [@x (a)] - | ^ + | ^ "); } @@ -268,6 +263,17 @@ fn field_equals_typo_missing_value() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" + error: use `:` for field constraints, not `=`; this isn't a definition + | + 1 | (call name = ) + | ^ + | + help: use `:` + | + 1 - (call name = ) + 1 + (call name : ) + | + error: expected an expression; after `field =` | 1 | (call name = ) @@ -282,6 +288,17 @@ fn lowercase_branch_label_missing_expression() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" + error: branch labels must be capitalized; branch labels map to enum variants + | + 1 | [label:] + | ^^^^^ + | + help: use `Label` + | + 1 - [label:] + 1 + [Label:] + | + error: expected an expression; after `label:` | 1 | [label:] diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs index eb38bd33..540d8a31 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs @@ -143,10 +143,15 @@ fn predicate_unsupported() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` + error: predicates like `#match?` are not supported | 1 | (a (#eq? @x "foo") b) - | ^ + | ^^^^ + + error: unexpected token; not valid inside a node — try `(child)` or close with `)` + | + 1 | (a (#eq? @x "foo") b) + | ^ error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | @@ -183,10 +188,15 @@ fn predicate_in_tree() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" - error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` + error: predicates like `#match?` are not supported + | + 1 | (function #eq? @name "test") + | ^^^^ + + error: unexpected token; not valid inside a node — try `(child)` or close with `)` | 1 | (function #eq? @name "test") - | ^^^^ + | ^ "#); } @@ -233,6 +243,11 @@ fn multiline_garbage_recovery() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" + error: unexpected token; not valid inside a node — try `(child)` or close with `)` + | + 2 | ^^^ + | ^^^ + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 3 | b) @@ -290,10 +305,15 @@ fn alternation_recovery_to_capture() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` + error: unexpected token; not valid inside alternation — try `(node)` or close with `]` + | + 1 | [^^^ @name] + | ^^^ + + error: unexpected token; not valid inside alternation — try `(node)` or close with `]` | 1 | [^^^ @name] - | ^^^^ + | ^ "); } 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 5121c7be..daa40531 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs @@ -1097,6 +1097,17 @@ fn pipe_in_tree() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" + error: separators are not needed; plotnik uses whitespace, not `|` + | + 1 | (a | b) + | ^ + | + help: remove + | + 1 - (a | b) + 1 + (a b) + | + error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | (a | b) @@ -1171,10 +1182,16 @@ fn field_equals_typo_no_expression() { let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" - error: expected an expression; after `field =` + error: use `:` for field constraints, not `=`; this isn't a definition | 1 | (call name=) - | ^ + | ^ + | + help: use `:` + | + 1 - (call name=) + 1 + (call name:) + | "); } @@ -1276,11 +1293,6 @@ fn single_colon_primitive_type() { 1 | @val : string | ^ - error: expected an expression; after `field:` - | - 1 | @val : string - | ^ - error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)` | 1 | @val : string diff --git a/crates/plotnik-lib/src/query/mod_tests.rs b/crates/plotnik-lib/src/query/mod_tests.rs index fa8c4f79..883e1299 100644 --- a/crates/plotnik-lib/src/query/mod_tests.rs +++ b/crates/plotnik-lib/src/query/mod_tests.rs @@ -41,5 +41,10 @@ fn combined_errors() { | 1 | (call (Undefined) extra) | ^^^^^ + + error: `Undefined` is not defined + | + 1 | (call (Undefined) extra) + | ^^^^^^^^^ "); }