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