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