plotnik_lib/diagnostics/
message.rs

1use rowan::TextRange;
2
3/// Diagnostic kinds ordered by priority (highest priority first).
4///
5/// When two diagnostics have overlapping spans, the higher-priority one
6/// suppresses the lower-priority one. This prevents cascading error noise.
7///
8/// Priority rationale:
9/// - Unclosed delimiters cause massive cascading errors downstream
10/// - Expected token errors are root causes the user should fix first
11/// - Invalid syntax usage is a specific mistake at a location
12/// - Naming validation errors are convention violations
13/// - Semantic errors assume valid syntax
14/// - Structural observations are often consequences of earlier errors
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum DiagnosticKind {
17    // These cause cascading errors throughout the rest of the file
18    UnclosedTree,
19    UnclosedSequence,
20    UnclosedAlternation,
21
22    // User omitted something required - root cause errors
23    ExpectedExpression,
24    ExpectedTypeName,
25    ExpectedCaptureName,
26    ExpectedFieldName,
27    ExpectedSubtype,
28
29    // User wrote something that doesn't belong
30    EmptyTree,
31    BareIdentifier,
32    InvalidSeparator,
33    InvalidFieldEquals,
34    InvalidSupertypeSyntax,
35    InvalidTypeAnnotationSyntax,
36    ErrorTakesNoArguments,
37    RefCannotHaveChildren,
38    ErrorMissingOutsideParens,
39    UnsupportedPredicate,
40    UnexpectedToken,
41    CaptureWithoutTarget,
42    LowercaseBranchLabel,
43
44    // Convention violations - fixable with suggestions
45    CaptureNameHasDots,
46    CaptureNameHasHyphens,
47    CaptureNameUppercase,
48    DefNameLowercase,
49    DefNameHasSeparators,
50    BranchLabelHasSeparators,
51    FieldNameHasDots,
52    FieldNameHasHyphens,
53    FieldNameUppercase,
54    TypeNameInvalidChars,
55
56    // Valid syntax, invalid semantics
57    DuplicateDefinition,
58    UndefinedReference,
59    MixedAltBranches,
60    RecursionNoEscape,
61    DirectRecursion,
62    FieldSequenceValue,
63
64    // Type inference errors
65    IncompatibleTypes,
66    MultiCaptureQuantifierNoName,
67    UnusedBranchLabels,
68
69    // Link pass - grammar validation
70    UnknownNodeType,
71    UnknownField,
72    FieldNotOnNodeType,
73    InvalidFieldChildType,
74    InvalidChildType,
75
76    // Often consequences of earlier errors
77    UnnamedDefNotLast,
78}
79
80impl DiagnosticKind {
81    /// Default severity for this kind. Can be overridden by policy.
82    pub fn default_severity(&self) -> Severity {
83        match self {
84            Self::UnusedBranchLabels => Severity::Warning,
85            _ => Severity::Error,
86        }
87    }
88
89    /// Whether this kind suppresses `other` when spans overlap.
90    ///
91    /// Uses enum discriminant ordering: lower position = higher priority.
92    /// A higher-priority diagnostic suppresses lower-priority ones in the same span.
93    pub fn suppresses(&self, other: &DiagnosticKind) -> bool {
94        self < other
95    }
96
97    /// Structural errors are Unclosed* - they cause cascading errors but
98    /// should be suppressed by root-cause errors at the same position.
99    pub fn is_structural_error(&self) -> bool {
100        matches!(
101            self,
102            Self::UnclosedTree | Self::UnclosedSequence | Self::UnclosedAlternation
103        )
104    }
105
106    /// Root cause errors - user omitted something required.
107    /// These suppress structural errors at the same position.
108    pub fn is_root_cause_error(&self) -> bool {
109        matches!(
110            self,
111            Self::ExpectedExpression
112                | Self::ExpectedTypeName
113                | Self::ExpectedCaptureName
114                | Self::ExpectedFieldName
115                | Self::ExpectedSubtype
116        )
117    }
118
119    /// Consequence errors - often caused by earlier parse errors.
120    /// These get suppressed when any root-cause or structural error exists.
121    pub fn is_consequence_error(&self) -> bool {
122        matches!(self, Self::UnnamedDefNotLast)
123    }
124
125    /// Base message for this diagnostic kind, used when no custom message is provided.
126    pub fn fallback_message(&self) -> &'static str {
127        match self {
128            // Unclosed delimiters - clear about what's missing
129            Self::UnclosedTree => "missing closing `)`",
130            Self::UnclosedSequence => "missing closing `}`",
131            Self::UnclosedAlternation => "missing closing `]`",
132
133            // Expected token errors - specific about what's needed
134            Self::ExpectedExpression => "expected an expression",
135            Self::ExpectedTypeName => "expected type name after `::`",
136            Self::ExpectedCaptureName => "expected name after `@`",
137            Self::ExpectedFieldName => "expected field name",
138            Self::ExpectedSubtype => "expected subtype after `/`",
139
140            // Invalid syntax - explain what's wrong
141            Self::EmptyTree => "empty parentheses are not allowed",
142            Self::BareIdentifier => "bare identifier is not a valid expression",
143            Self::InvalidSeparator => "separators are not needed",
144            Self::InvalidFieldEquals => "use `:` for field constraints, not `=`",
145            Self::InvalidSupertypeSyntax => "supertype syntax not allowed on references",
146            Self::InvalidTypeAnnotationSyntax => "use `::` for type annotations, not `:`",
147            Self::ErrorTakesNoArguments => "`(ERROR)` cannot have child nodes",
148            Self::RefCannotHaveChildren => "references cannot have children",
149            Self::ErrorMissingOutsideParens => {
150                "`ERROR` and `MISSING` must be wrapped in parentheses"
151            }
152            Self::UnsupportedPredicate => "predicates like `#match?` are not supported",
153            Self::UnexpectedToken => "unexpected token",
154            Self::CaptureWithoutTarget => "`@` must follow an expression to capture",
155            Self::LowercaseBranchLabel => "branch labels must be capitalized",
156
157            // Naming convention violations
158            Self::CaptureNameHasDots => "capture names cannot contain `.`",
159            Self::CaptureNameHasHyphens => "capture names cannot contain `-`",
160            Self::CaptureNameUppercase => "capture names must be lowercase",
161            Self::DefNameLowercase => "definition names must start uppercase",
162            Self::DefNameHasSeparators => "definition names must be PascalCase",
163            Self::BranchLabelHasSeparators => "branch labels must be PascalCase",
164            Self::FieldNameHasDots => "field names cannot contain `.`",
165            Self::FieldNameHasHyphens => "field names cannot contain `-`",
166            Self::FieldNameUppercase => "field names must be lowercase",
167            Self::TypeNameInvalidChars => "type names cannot contain `.` or `-`",
168
169            // Semantic errors
170            Self::DuplicateDefinition => "name already defined",
171            Self::UndefinedReference => "undefined reference",
172            Self::MixedAltBranches => "cannot mix labeled and unlabeled branches",
173            Self::RecursionNoEscape => "infinite recursion: cycle has no escape path",
174            Self::DirectRecursion => "infinite recursion: cycle consumes no input",
175            Self::FieldSequenceValue => "field must match exactly one node",
176
177            // Type inference
178            Self::IncompatibleTypes => "incompatible types in alternation branches",
179            Self::MultiCaptureQuantifierNoName => {
180                "quantified expression with multiple captures requires `@name`"
181            }
182            Self::UnusedBranchLabels => "branch labels have no effect without capture",
183
184            // Link pass - grammar validation
185            Self::UnknownNodeType => "unknown node type",
186            Self::UnknownField => "unknown field",
187            Self::FieldNotOnNodeType => "field not valid on this node type",
188            Self::InvalidFieldChildType => "node type not valid for this field",
189            Self::InvalidChildType => "node type not valid as child",
190
191            // Structural
192            Self::UnnamedDefNotLast => "only the last definition can be unnamed",
193        }
194    }
195
196    /// Template for custom messages. Contains `{}` placeholder for caller-provided detail.
197    pub fn custom_message(&self) -> String {
198        match self {
199            // Special formatting for references
200            Self::RefCannotHaveChildren => {
201                "`{}` is a reference and cannot have children".to_string()
202            }
203            Self::FieldSequenceValue => {
204                "field `{}` must match exactly one node, not a sequence".to_string()
205            }
206
207            // Semantic errors with name context
208            Self::DuplicateDefinition => "`{}` is already defined".to_string(),
209            Self::UndefinedReference => "`{}` is not defined".to_string(),
210            Self::IncompatibleTypes => "incompatible types: {}".to_string(),
211
212            // Link pass errors with context
213            Self::UnknownNodeType => "`{}` is not a valid node type".to_string(),
214            Self::UnknownField => "`{}` is not a valid field".to_string(),
215            Self::FieldNotOnNodeType => "field `{}` is not valid on this node type".to_string(),
216            Self::InvalidFieldChildType => "node type `{}` is not valid for this field".to_string(),
217            Self::InvalidChildType => "`{}` cannot be a child of this node".to_string(),
218
219            // Alternation mixing
220            Self::MixedAltBranches => "cannot mix labeled and unlabeled branches: {}".to_string(),
221
222            // Unclosed with context
223            Self::UnclosedTree | Self::UnclosedSequence | Self::UnclosedAlternation => {
224                format!("{}; {{}}", self.fallback_message())
225            }
226
227            // Type annotation specifics
228            Self::InvalidTypeAnnotationSyntax => {
229                "type annotations use `::`, not `:` — {}".to_string()
230            }
231
232            // Named def ordering
233            Self::UnnamedDefNotLast => "only the last definition can be unnamed — {}".to_string(),
234
235            // Standard pattern: fallback + context
236            _ => format!("{}; {{}}", self.fallback_message()),
237        }
238    }
239
240    /// Render the final message.
241    ///
242    /// - `None` → returns `fallback_message()`
243    /// - `Some(detail)` → returns `custom_message()` with `{}` replaced by detail
244    pub fn message(&self, msg: Option<&str>) -> String {
245        match msg {
246            None => self.fallback_message().to_string(),
247            Some(detail) => self.custom_message().replace("{}", detail),
248        }
249    }
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
253pub enum Severity {
254    #[default]
255    Error,
256    Warning,
257}
258
259impl std::fmt::Display for Severity {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        match self {
262            Severity::Error => write!(f, "error"),
263            Severity::Warning => write!(f, "warning"),
264        }
265    }
266}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
269pub struct Fix {
270    pub(crate) replacement: String,
271    pub(crate) description: String,
272}
273
274impl Fix {
275    pub fn new(replacement: impl Into<String>, description: impl Into<String>) -> Self {
276        Self {
277            replacement: replacement.into(),
278            description: description.into(),
279        }
280    }
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct RelatedInfo {
285    pub(crate) range: TextRange,
286    pub(crate) message: String,
287}
288
289impl RelatedInfo {
290    pub fn new(range: TextRange, message: impl Into<String>) -> Self {
291        Self {
292            range,
293            message: message.into(),
294        }
295    }
296}
297
298#[derive(Debug, Clone, PartialEq, Eq)]
299pub(crate) struct DiagnosticMessage {
300    pub(crate) kind: DiagnosticKind,
301    /// The range shown to the user (underlined in output).
302    pub(crate) range: TextRange,
303    /// The range used for suppression logic. Errors within another error's
304    /// suppression_range may be suppressed. Defaults to `range` but can be
305    /// set to a parent context (e.g., enclosing tree span) for better cascading
306    /// error suppression.
307    pub(crate) suppression_range: TextRange,
308    pub(crate) message: String,
309    pub(crate) fix: Option<Fix>,
310    pub(crate) related: Vec<RelatedInfo>,
311    pub(crate) hints: Vec<String>,
312}
313
314impl DiagnosticMessage {
315    pub(crate) fn new(kind: DiagnosticKind, range: TextRange, message: impl Into<String>) -> Self {
316        Self {
317            kind,
318            range,
319            suppression_range: range,
320            message: message.into(),
321            fix: None,
322            related: Vec::new(),
323            hints: Vec::new(),
324        }
325    }
326
327    pub(crate) fn with_default_message(kind: DiagnosticKind, range: TextRange) -> Self {
328        Self::new(kind, range, kind.fallback_message())
329    }
330
331    pub(crate) fn severity(&self) -> Severity {
332        self.kind.default_severity()
333    }
334
335    pub(crate) fn is_error(&self) -> bool {
336        self.severity() == Severity::Error
337    }
338
339    pub(crate) fn is_warning(&self) -> bool {
340        self.severity() == Severity::Warning
341    }
342}
343
344impl std::fmt::Display for DiagnosticMessage {
345    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346        write!(
347            f,
348            "{} at {}..{}: {}",
349            self.severity(),
350            u32::from(self.range.start()),
351            u32::from(self.range.end()),
352            self.message
353        )?;
354        if let Some(fix) = &self.fix {
355            write!(f, " (fix: {})", fix.description)?;
356        }
357        for related in &self.related {
358            write!(
359                f,
360                " (related: {} at {}..{})",
361                related.message,
362                u32::from(related.range.start()),
363                u32::from(related.range.end())
364            )?;
365        }
366        for hint in &self.hints {
367            write!(f, " (hint: {})", hint)?;
368        }
369        Ok(())
370    }
371}