plotnik_compiler/diagnostics/
message.rs

1use rowan::TextRange;
2
3use super::{SourceId, Span};
4
5/// Diagnostic kinds ordered by priority (highest priority first).
6///
7/// When two diagnostics have overlapping spans, the higher-priority one
8/// suppresses the lower-priority one. This prevents cascading error noise.
9///
10/// Priority rationale:
11/// - Unclosed delimiters cause massive cascading errors downstream
12/// - Expected token errors are root causes the user should fix first
13/// - Invalid syntax usage is a specific mistake at a location
14/// - Naming validation errors are convention violations
15/// - Semantic errors assume valid syntax
16/// - Structural observations are often consequences of earlier errors
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub enum DiagnosticKind {
19    // These cause cascading errors throughout the rest of the file
20    UnclosedTree,
21    UnclosedSequence,
22    UnclosedAlternation,
23    UnclosedRegex,
24
25    // User omitted something required - root cause errors
26    ExpectedExpression,
27    ExpectedTypeName,
28    ExpectedCaptureName,
29    ExpectedFieldName,
30    ExpectedSubtype,
31    ExpectedPredicateValue,
32
33    // User wrote something that doesn't belong
34    EmptyTree,
35    EmptyAnonymousNode,
36    EmptySequence,
37    EmptyAlternation,
38    BareIdentifier,
39    InvalidSeparator,
40    AnchorInAlternation,
41    InvalidFieldEquals,
42    InvalidSupertypeSyntax,
43    InvalidTypeAnnotationSyntax,
44    ErrorTakesNoArguments,
45    RefCannotHaveChildren,
46    ErrorMissingOutsideParens,
47    UnsupportedPredicate,
48    UnexpectedToken,
49    CaptureWithoutTarget,
50    LowercaseBranchLabel,
51
52    // Convention violations - fixable with suggestions
53    CaptureNameHasDots,
54    CaptureNameHasHyphens,
55    CaptureNameUppercase,
56    DefNameLowercase,
57    DefNameHasSeparators,
58    BranchLabelHasSeparators,
59    FieldNameHasDots,
60    FieldNameHasHyphens,
61    FieldNameUppercase,
62    TypeNameInvalidChars,
63    TreeSitterSequenceSyntax,
64    NegationSyntaxDeprecated,
65
66    // Valid syntax, invalid semantics
67    DuplicateDefinition,
68    UndefinedReference,
69    MixedAltBranches,
70    RecursionNoEscape,
71    DirectRecursion,
72    FieldSequenceValue,
73    AnchorWithoutContext,
74
75    // Type inference errors
76    IncompatibleTypes,
77    MultiCaptureQuantifierNoName,
78    UnusedBranchLabels,
79    StrictDimensionalityViolation,
80    MultiElementScalarCapture,
81    UncapturedOutputWithCaptures,
82    AmbiguousUncapturedOutputs,
83    DuplicateCaptureInScope,
84    IncompatibleCaptureTypes,
85    IncompatibleStructShapes,
86
87    // Predicate validation
88    PredicateOnNonLeaf,
89    EmptyRegex,
90    RegexBackreference,
91    RegexLookaround,
92    RegexNamedCapture,
93    RegexSyntaxError,
94
95    // Link pass - grammar validation
96    UnknownNodeType,
97    UnknownField,
98    FieldNotOnNodeType,
99    InvalidFieldChildType,
100    InvalidChildType,
101
102    // Often consequences of earlier errors
103    UnnamedDef,
104}
105
106impl DiagnosticKind {
107    /// Default severity for this kind. Can be overridden by policy.
108    pub fn default_severity(&self) -> Severity {
109        match self {
110            Self::UnusedBranchLabels
111            | Self::TreeSitterSequenceSyntax
112            | Self::NegationSyntaxDeprecated => Severity::Warning,
113            _ => Severity::Error,
114        }
115    }
116
117    /// Whether this kind suppresses `other` when spans overlap.
118    ///
119    /// Uses enum discriminant ordering: lower position = higher priority.
120    /// A higher-priority diagnostic suppresses lower-priority ones in the same span.
121    pub fn suppresses(&self, other: &DiagnosticKind) -> bool {
122        self < other
123    }
124
125    /// Structural errors are Unclosed* - they cause cascading errors but
126    /// should be suppressed by root-cause errors at the same position.
127    pub fn is_structural_error(&self) -> bool {
128        matches!(
129            self,
130            Self::UnclosedTree
131                | Self::UnclosedSequence
132                | Self::UnclosedAlternation
133                | Self::UnclosedRegex
134        )
135    }
136
137    /// Root cause errors - user omitted something required.
138    /// These suppress structural errors at the same position.
139    pub fn is_root_cause_error(&self) -> bool {
140        matches!(
141            self,
142            Self::ExpectedExpression
143                | Self::ExpectedTypeName
144                | Self::ExpectedCaptureName
145                | Self::ExpectedFieldName
146                | Self::ExpectedSubtype
147                | Self::ExpectedPredicateValue
148        )
149    }
150
151    /// Consequence errors - often caused by earlier parse errors.
152    /// These get suppressed when any root-cause or structural error exists.
153    pub fn is_consequence_error(&self) -> bool {
154        matches!(self, Self::UnnamedDef)
155    }
156
157    /// Default hint for this kind, automatically included in diagnostics.
158    /// Call sites can add additional hints for context-specific information.
159    pub fn default_hint(&self) -> Option<&'static str> {
160        match self {
161            Self::ExpectedSubtype => Some("e.g., `expression/binary_expression`"),
162            Self::ExpectedTypeName => Some("e.g., `::MyType` or `::string`"),
163            Self::ExpectedFieldName => Some("e.g., `-value`"),
164            Self::EmptyTree => Some("use `(_)` to match any named node, or `_` for any node"),
165            Self::EmptyAnonymousNode => Some("use a valid anonymous node or remove it"),
166            Self::EmptySequence => Some("sequences must contain at least one expression"),
167            Self::EmptyAlternation => Some("alternations must contain at least one branch"),
168            Self::TreeSitterSequenceSyntax => Some("use `{...}` for sequences"),
169            Self::NegationSyntaxDeprecated => Some("use `-field` instead of `!field`"),
170            Self::MixedAltBranches => {
171                Some("use all labels for a tagged union, or none for a merged struct")
172            }
173            Self::RecursionNoEscape => {
174                Some("add a non-recursive branch to terminate: `[Base: ... Rec: (Self)]`")
175            }
176            Self::DirectRecursion => {
177                Some("recursive references must consume input before recursing")
178            }
179            Self::AnchorWithoutContext => Some("wrap in a named node: `(parent . (child))`"),
180            Self::AnchorInAlternation => Some("use `[{(a) . (b)} (c)]` to anchor within a branch"),
181            Self::UncapturedOutputWithCaptures => Some("add `@name` to capture the output"),
182            Self::AmbiguousUncapturedOutputs => {
183                Some("capture each expression explicitly: `(X) @x (Y) @y`")
184            }
185            Self::MultiElementScalarCapture => {
186                Some("add internal captures: `{(a) @a (b) @b}* @items`")
187            }
188            _ => None,
189        }
190    }
191
192    /// Base message for this diagnostic kind, used when no custom message is provided.
193    pub fn fallback_message(&self) -> &'static str {
194        match self {
195            // Unclosed delimiters
196            Self::UnclosedTree => "missing closing `)`",
197            Self::UnclosedSequence => "missing closing `}`",
198            Self::UnclosedAlternation => "missing closing `]`",
199            Self::UnclosedRegex => "missing closing `/` for regex",
200
201            // Expected token errors
202            Self::ExpectedExpression => "expected an expression",
203            Self::ExpectedTypeName => "expected type name",
204            Self::ExpectedCaptureName => "expected capture name",
205            Self::ExpectedFieldName => "expected field name",
206            Self::ExpectedSubtype => "expected subtype name",
207            Self::ExpectedPredicateValue => "expected string or regex after predicate operator",
208
209            // Invalid syntax
210            Self::EmptyTree => "empty `()` is not allowed",
211            Self::EmptyAnonymousNode => "empty anonymous node",
212            Self::EmptySequence => "empty `{}` is not allowed",
213            Self::EmptyAlternation => "empty `[]` is not allowed",
214            Self::BareIdentifier => "bare identifier is not valid",
215            Self::InvalidSeparator => "unexpected separator",
216            Self::AnchorInAlternation => "anchors cannot appear directly in alternations",
217            Self::InvalidFieldEquals => "use `:` instead of `=`",
218            Self::InvalidSupertypeSyntax => "references cannot have supertypes",
219            Self::InvalidTypeAnnotationSyntax => "use `::` for type annotations",
220            Self::ErrorTakesNoArguments => "`(ERROR)` cannot have children",
221            Self::RefCannotHaveChildren => "references cannot have children",
222            Self::ErrorMissingOutsideParens => "special node requires parentheses",
223            Self::UnsupportedPredicate => "predicates are not supported",
224            Self::UnexpectedToken => "unexpected token",
225            Self::CaptureWithoutTarget => "capture has no target",
226            Self::LowercaseBranchLabel => "branch label must start with uppercase",
227
228            // Naming convention violations
229            Self::CaptureNameHasDots => "capture names cannot contain `.`",
230            Self::CaptureNameHasHyphens => "capture names cannot contain `-`",
231            Self::CaptureNameUppercase => "capture names must be lowercase",
232            Self::DefNameLowercase => "definition names must start uppercase",
233            Self::DefNameHasSeparators => "definition names must be PascalCase",
234            Self::BranchLabelHasSeparators => "branch labels must be PascalCase",
235            Self::FieldNameHasDots => "field names cannot contain `.`",
236            Self::FieldNameHasHyphens => "field names cannot contain `-`",
237            Self::FieldNameUppercase => "field names must be lowercase",
238            Self::TypeNameInvalidChars => "type names cannot contain `.` or `-`",
239            Self::TreeSitterSequenceSyntax => "tree-sitter sequence syntax",
240            Self::NegationSyntaxDeprecated => "deprecated negation syntax",
241
242            // Semantic errors
243            Self::DuplicateDefinition => "duplicate definition",
244            Self::UndefinedReference => "undefined reference",
245            Self::MixedAltBranches => "cannot mix labeled and unlabeled branches",
246            Self::RecursionNoEscape => "infinite recursion: no escape path",
247            Self::DirectRecursion => "infinite recursion: cycle consumes no input",
248            Self::FieldSequenceValue => "field cannot match a sequence",
249            Self::AnchorWithoutContext => "boundary anchor requires parent node context",
250
251            // Type inference
252            Self::IncompatibleTypes => "incompatible types",
253            Self::MultiCaptureQuantifierNoName => {
254                "quantified expression with multiple captures requires a struct capture"
255            }
256            Self::UnusedBranchLabels => "branch labels have no effect without capture",
257            Self::StrictDimensionalityViolation => {
258                "quantifier with captures requires a struct capture"
259            }
260            Self::MultiElementScalarCapture => {
261                "cannot capture multi-element pattern as scalar array"
262            }
263            Self::UncapturedOutputWithCaptures => {
264                "output-producing expression requires capture when siblings have captures"
265            }
266            Self::AmbiguousUncapturedOutputs => {
267                "multiple expressions produce output without capture"
268            }
269            Self::DuplicateCaptureInScope => "duplicate capture in scope",
270            Self::IncompatibleCaptureTypes => "incompatible capture types",
271            Self::IncompatibleStructShapes => "incompatible struct shapes",
272
273            // Predicate validation
274            Self::PredicateOnNonLeaf => {
275                "predicates match text content, but this node can contain children"
276            }
277            Self::EmptyRegex => "empty regex pattern",
278            Self::RegexBackreference => "backreferences are not supported in regex",
279            Self::RegexLookaround => "lookahead/lookbehind is not supported in regex",
280            Self::RegexNamedCapture => "named captures are not supported in regex",
281            Self::RegexSyntaxError => "invalid regex syntax",
282
283            // Link pass - grammar validation
284            Self::UnknownNodeType => "unknown node type",
285            Self::UnknownField => "unknown field",
286            Self::FieldNotOnNodeType => "field not valid on this node type",
287            Self::InvalidFieldChildType => "node type not valid for this field",
288            Self::InvalidChildType => "node type not valid as child",
289
290            // Structural
291            Self::UnnamedDef => "definition must be named",
292        }
293    }
294
295    /// Template for custom messages. Contains `{}` placeholder for caller-provided detail.
296    pub fn custom_message(&self) -> String {
297        match self {
298            // Special formatting for references
299            Self::RefCannotHaveChildren => {
300                "`{}` is a reference and cannot have children".to_string()
301            }
302            Self::FieldSequenceValue => "field `{}` cannot match a sequence".to_string(),
303
304            // Semantic errors with name context
305            Self::DuplicateDefinition => "`{}` is already defined".to_string(),
306            Self::UndefinedReference => "`{}` is not defined".to_string(),
307            Self::IncompatibleTypes => "incompatible types: {}".to_string(),
308
309            // Type inference errors with context
310            Self::StrictDimensionalityViolation => "{}".to_string(),
311            Self::MultiElementScalarCapture => "{}".to_string(),
312            Self::DuplicateCaptureInScope => {
313                "capture `@{}` already defined in this scope".to_string()
314            }
315            Self::IncompatibleCaptureTypes => {
316                "capture `@{}` has incompatible types across branches".to_string()
317            }
318            Self::IncompatibleStructShapes => {
319                "capture `@{}` has incompatible struct fields across branches".to_string()
320            }
321
322            // Link pass errors with context
323            Self::UnknownNodeType => "`{}` is not a valid node type".to_string(),
324            Self::UnknownField => "`{}` is not a valid field".to_string(),
325            Self::FieldNotOnNodeType => "field `{}` is not valid on this node type".to_string(),
326            Self::InvalidFieldChildType => "node type `{}` is not valid for this field".to_string(),
327            Self::InvalidChildType => "`{}` cannot be a child of this node".to_string(),
328
329            // Alternation mixing
330            Self::MixedAltBranches => "cannot mix labeled and unlabeled branches: {}".to_string(),
331
332            // Unclosed with context
333            Self::UnclosedTree | Self::UnclosedSequence | Self::UnclosedAlternation => {
334                format!("{}; {{}}", self.fallback_message())
335            }
336
337            // Type annotation specifics
338            Self::InvalidTypeAnnotationSyntax => "use `::` for type annotations: {}".to_string(),
339
340            // Named def (no custom message needed; suggestion goes in hint)
341            Self::UnnamedDef => self.fallback_message().to_string(),
342
343            // Standard pattern: fallback + context
344            _ => format!("{}: {{}}", self.fallback_message()),
345        }
346    }
347
348    /// Render the final message.
349    ///
350    /// - `None` → returns `fallback_message()`
351    /// - `Some(detail)` → returns `custom_message()` with `{}` replaced by detail
352    pub fn message(&self, msg: Option<&str>) -> String {
353        match msg {
354            None => self.fallback_message().to_string(),
355            Some(detail) => self.custom_message().replace("{}", detail),
356        }
357    }
358}
359
360#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
361pub enum Severity {
362    #[default]
363    Error,
364    Warning,
365}
366
367impl std::fmt::Display for Severity {
368    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369        match self {
370            Severity::Error => write!(f, "error"),
371            Severity::Warning => write!(f, "warning"),
372        }
373    }
374}
375
376#[derive(Debug, Clone, PartialEq, Eq)]
377pub struct Fix {
378    pub(crate) replacement: String,
379    pub(crate) description: String,
380}
381
382impl Fix {
383    pub fn new(replacement: impl Into<String>, description: impl Into<String>) -> Self {
384        Self {
385            replacement: replacement.into(),
386            description: description.into(),
387        }
388    }
389}
390
391#[derive(Debug, Clone, PartialEq, Eq)]
392pub struct RelatedInfo {
393    pub(crate) span: Span,
394    pub(crate) message: String,
395}
396
397impl RelatedInfo {
398    pub fn new(source: SourceId, range: TextRange, message: impl Into<String>) -> Self {
399        Self {
400            span: Span::new(source, range),
401            message: message.into(),
402        }
403    }
404}
405
406#[derive(Debug, Clone, PartialEq, Eq)]
407pub(crate) struct DiagnosticMessage {
408    pub(crate) kind: DiagnosticKind,
409    /// Which source file this diagnostic belongs to.
410    pub(crate) source: SourceId,
411    /// The range shown to the user (underlined in output).
412    pub(crate) range: TextRange,
413    /// The range used for suppression logic. Errors within another error's
414    /// suppression_range may be suppressed. Defaults to `range` but can be
415    /// set to a parent context (e.g., enclosing tree span) for better cascading
416    /// error suppression.
417    pub(crate) suppression_range: TextRange,
418    pub(crate) message: String,
419    pub(crate) fix: Option<Fix>,
420    pub(crate) related: Vec<RelatedInfo>,
421    pub(crate) hints: Vec<String>,
422}
423
424impl DiagnosticMessage {
425    pub(crate) fn new(
426        source: SourceId,
427        kind: DiagnosticKind,
428        range: TextRange,
429        message: impl Into<String>,
430    ) -> Self {
431        Self {
432            kind,
433            source,
434            range,
435            suppression_range: range,
436            message: message.into(),
437            fix: None,
438            related: Vec::new(),
439            hints: Vec::new(),
440        }
441    }
442
443    pub(crate) fn with_default_message(
444        source: SourceId,
445        kind: DiagnosticKind,
446        range: TextRange,
447    ) -> Self {
448        Self::new(source, kind, range, kind.fallback_message())
449    }
450
451    pub(crate) fn severity(&self) -> Severity {
452        self.kind.default_severity()
453    }
454
455    pub(crate) fn is_error(&self) -> bool {
456        self.severity() == Severity::Error
457    }
458
459    pub(crate) fn is_warning(&self) -> bool {
460        self.severity() == Severity::Warning
461    }
462}
463
464impl std::fmt::Display for DiagnosticMessage {
465    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
466        write!(
467            f,
468            "{} at {}..{}: {}",
469            self.severity(),
470            u32::from(self.range.start()),
471            u32::from(self.range.end()),
472            self.message
473        )?;
474        if let Some(fix) = &self.fix {
475            write!(f, " (fix: {})", fix.description)?;
476        }
477        for related in &self.related {
478            write!(
479                f,
480                " (related: {} at {}..{})",
481                related.message,
482                u32::from(related.span.range.start()),
483                u32::from(related.span.range.end())
484            )?;
485        }
486        for hint in &self.hints {
487            write!(f, " (hint: {})", hint)?;
488        }
489        Ok(())
490    }
491}