plotnik_lib/diagnostics/
mod.rs

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