plotnik_compiler/diagnostics/
mod.rs

1mod message;
2mod printer;
3
4#[cfg(test)]
5mod diagnostics_tests;
6
7use rowan::TextRange;
8
9pub use message::{DiagnosticKind, Severity};
10pub use printer::DiagnosticsPrinter;
11
12use message::{DiagnosticMessage, Fix, RelatedInfo};
13
14// Re-export from query module
15pub use crate::query::{SourceId, SourceMap};
16
17/// A location that knows which source it belongs to.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct Span {
20    pub source: SourceId,
21    pub range: TextRange,
22}
23
24impl Span {
25    pub fn new(source: SourceId, range: TextRange) -> Self {
26        Self { source, range }
27    }
28}
29
30#[derive(Debug, Clone, Default)]
31pub struct Diagnostics {
32    messages: Vec<DiagnosticMessage>,
33}
34
35#[must_use = "diagnostic not emitted, call .emit()"]
36pub struct DiagnosticBuilder<'d> {
37    diagnostics: &'d mut Diagnostics,
38    message: DiagnosticMessage,
39}
40
41impl Diagnostics {
42    pub fn new() -> Self {
43        Self {
44            messages: Vec::new(),
45        }
46    }
47
48    /// Create a diagnostic with the given kind and span.
49    ///
50    /// Uses the kind's default message. Call `.message()` on the builder to override.
51    pub fn report(
52        &mut self,
53        source: SourceId,
54        kind: DiagnosticKind,
55        range: TextRange,
56    ) -> DiagnosticBuilder<'_> {
57        DiagnosticBuilder {
58            diagnostics: self,
59            message: DiagnosticMessage::with_default_message(source, kind, range),
60        }
61    }
62
63    pub fn is_empty(&self) -> bool {
64        self.messages.is_empty()
65    }
66
67    pub fn len(&self) -> usize {
68        self.messages.len()
69    }
70
71    pub fn has_errors(&self) -> bool {
72        self.messages.iter().any(|d| d.is_error())
73    }
74
75    pub fn has_warnings(&self) -> bool {
76        self.messages.iter().any(|d| d.is_warning())
77    }
78
79    pub fn error_count(&self) -> usize {
80        self.messages.iter().filter(|d| d.is_error()).count()
81    }
82
83    pub fn warning_count(&self) -> usize {
84        self.messages.iter().filter(|d| d.is_warning()).count()
85    }
86
87    /// Returns diagnostics with cascading errors suppressed.
88    ///
89    /// Suppression rules:
90    /// 1. Containment: when error A's suppression_range contains error B's display range,
91    ///    and A has higher priority, suppress B (only for structural errors)
92    /// 2. Same position: when spans start at the same position, root-cause errors suppress structural ones
93    /// 3. Consequence errors (UnnamedDef) suppressed when any other error exists
94    /// 4. Adjacent: when error A ends exactly where error B starts, A suppresses B
95    pub(crate) fn filtered(&self) -> Vec<DiagnosticMessage> {
96        if self.messages.is_empty() {
97            return Vec::new();
98        }
99
100        let mut suppressed = vec![false; self.messages.len()];
101
102        // Rule 3: Suppress consequence errors if any non-consequence error exists
103        let has_primary_error = self.messages.iter().any(|m| !m.kind.is_consequence_error());
104        if has_primary_error {
105            for (i, msg) in self.messages.iter().enumerate() {
106                if msg.kind.is_consequence_error() {
107                    suppressed[i] = true;
108                }
109            }
110        }
111
112        // O(n²) but n is typically small (< 100 diagnostics)
113        for (i, a) in self.messages.iter().enumerate() {
114            for (j, b) in self.messages.iter().enumerate() {
115                if i == j || suppressed[i] || suppressed[j] {
116                    continue;
117                }
118
119                // Rule 1: Structural error containment
120                // Only unclosed delimiters can suppress distant errors, because they cause
121                // cascading parse failures throughout the tree
122                let contains = a.suppression_range.start() <= b.range.start()
123                    && b.range.end() <= a.suppression_range.end();
124                if contains && a.kind.is_structural_error() && a.kind.suppresses(&b.kind) {
125                    suppressed[j] = true;
126                    continue;
127                }
128
129                // Rule 2: Same start position
130                if a.range.start() == b.range.start() {
131                    // Root cause errors (Expected*) suppress structural errors (Unclosed*)
132                    // even though structural errors have higher enum priority. This is because
133                    // ExpectedExpression is the actual mistake; UnclosedTree is a consequence.
134                    if a.kind.is_root_cause_error() && b.kind.is_structural_error() {
135                        suppressed[j] = true;
136                        continue;
137                    }
138                    if a.kind.suppresses(&b.kind) {
139                        suppressed[j] = true;
140                        continue;
141                    }
142                }
143
144                // Rule 4: Adjacent position - when A ends exactly where B starts,
145                // B is likely a consequence of A (e.g., `@x` where `@` is unexpected
146                // and `x` would be reported as bare identifier).
147                // Priority doesn't matter here - position determines causality.
148                if a.range.end() == b.range.start() {
149                    suppressed[j] = true;
150                }
151            }
152        }
153
154        let mut result: Vec<_> = self
155            .messages
156            .iter()
157            .enumerate()
158            .filter(|(i, _)| !suppressed[*i])
159            .map(|(_, m)| m.clone())
160            .collect();
161        result.sort_by_key(|m| m.range.start());
162        result
163    }
164
165    /// Raw access to all diagnostics (for debugging/testing).
166    #[allow(dead_code)]
167    pub(crate) fn raw(&self) -> &[DiagnosticMessage] {
168        &self.messages
169    }
170
171    /// Create a printer with a source map (multi-file support).
172    pub fn printer<'q>(&self, sources: &'q SourceMap) -> DiagnosticsPrinter<'q> {
173        DiagnosticsPrinter::new(self.messages.clone(), sources)
174    }
175
176    /// Filtered printer with source map (cascading errors suppressed).
177    pub fn filtered_printer<'q>(&self, sources: &'q SourceMap) -> DiagnosticsPrinter<'q> {
178        DiagnosticsPrinter::new(self.filtered(), sources)
179    }
180
181    /// Render with source map.
182    pub fn render(&self, sources: &SourceMap) -> String {
183        self.printer(sources).render()
184    }
185
186    /// Render with source map, colored output.
187    pub fn render_colored(&self, sources: &SourceMap, colored: bool) -> String {
188        self.printer(sources).colored(colored).render()
189    }
190
191    /// Render filtered with source map.
192    pub fn render_filtered(&self, sources: &SourceMap) -> String {
193        self.filtered_printer(sources).render()
194    }
195
196    /// Render filtered with source map, colored output.
197    pub fn render_filtered_colored(&self, sources: &SourceMap, colored: bool) -> String {
198        self.filtered_printer(sources).colored(colored).render()
199    }
200
201    pub fn extend(&mut self, other: Diagnostics) {
202        self.messages.extend(other.messages);
203    }
204}
205
206impl<'d> DiagnosticBuilder<'d> {
207    /// Provide custom detail for this diagnostic, rendered using the kind's template.
208    pub fn message(mut self, msg: impl Into<String>) -> Self {
209        let detail = msg.into();
210        self.message.message = self.message.kind.message(Some(&detail));
211        self
212    }
213
214    pub fn related_to(
215        mut self,
216        source: SourceId,
217        range: TextRange,
218        msg: impl Into<String>,
219    ) -> Self {
220        self.message
221            .related
222            .push(RelatedInfo::new(source, range, msg));
223        self
224    }
225
226    /// Set the suppression range for this diagnostic.
227    ///
228    /// The suppression range is used to suppress cascading errors. Errors whose
229    /// display range falls within another error's suppression range may be
230    /// suppressed if the containing error has higher priority.
231    ///
232    /// Typically set to the parent context span (e.g., enclosing tree).
233    pub fn suppression_range(mut self, range: TextRange) -> Self {
234        self.message.suppression_range = range;
235        self
236    }
237
238    pub fn fix(mut self, description: impl Into<String>, replacement: impl Into<String>) -> Self {
239        self.message.fix = Some(Fix::new(replacement, description));
240        self
241    }
242
243    pub fn hint(mut self, hint: impl Into<String>) -> Self {
244        self.message.hints.push(hint.into());
245        self
246    }
247
248    pub fn emit(mut self) {
249        // Prepend default hint if one exists for this kind
250        if let Some(default_hint) = self.message.kind.default_hint() {
251            self.message.hints.insert(0, default_hint.to_string());
252        }
253        self.diagnostics.messages.push(self.message);
254    }
255}