Skip to main content

duck_diag/
lib.rs

1//! Generic diagnostic engine for tools that need rich, rustc-style error
2//! output. Plug in your own error code enum, attach spans + labels +
3//! suggestions, and render in pretty (color), plain, or JSON modes.
4//!
5//! See `examples/` for end-to-end demos.
6
7mod compact;
8mod diagnostic;
9mod formatter;
10#[cfg(feature = "json")]
11mod json;
12mod macros;
13mod smart;
14mod style;
15
16pub use compact::format_compact;
17pub use diagnostic::*;
18pub use formatter::{DiagnosticFormatter, RenderOptions, SourceCache};
19pub use smart::{format_all_smart, print_all_smart};
20
21use crate::style::*;
22use colored::*;
23
24/// Collects diagnostics, tracks per-severity counts, and renders them in
25/// pretty / plain / compact / JSON modes.
26#[derive(Debug)]
27pub struct DiagnosticEngine<C: DiagnosticCode> {
28  diagnostics: Vec<Diagnostic<C>>,
29  bug_count: usize,
30  error_count: usize,
31  warning_count: usize,
32  help_count: usize,
33  note_count: usize,
34}
35
36impl<C: DiagnosticCode> Default for DiagnosticEngine<C> {
37  fn default() -> Self {
38    Self {
39      diagnostics: Vec::new(),
40      bug_count: 0,
41      error_count: 0,
42      warning_count: 0,
43      help_count: 0,
44      note_count: 0,
45    }
46  }
47}
48
49impl<C: DiagnosticCode> DiagnosticEngine<C> {
50  /// New empty engine.
51  pub fn new() -> Self {
52    Self::default()
53  }
54
55  /// Drop every stored diagnostic and reset all counts.
56  pub fn clear(&mut self) {
57    self.diagnostics.clear();
58    self.bug_count = 0;
59    self.error_count = 0;
60    self.warning_count = 0;
61    self.help_count = 0;
62    self.note_count = 0;
63  }
64
65  /// Push a diagnostic and bump its severity bucket.
66  pub fn emit(&mut self, diagnostic: Diagnostic<C>) {
67    match diagnostic.severity {
68      Severity::Bug => self.bug_count += 1,
69      Severity::Error => self.error_count += 1,
70      Severity::Warning => self.warning_count += 1,
71      Severity::Help => self.help_count += 1,
72      Severity::Note => self.note_count += 1,
73    }
74    self.diagnostics.push(diagnostic);
75  }
76
77  /// Batch-emit. Severity is taken from each diagnostic, not the method name.
78  pub fn emit_errors(&mut self, errors: Vec<Diagnostic<C>>) {
79    for d in errors {
80      self.emit(d);
81    }
82  }
83
84  /// Batch-emit. Severity is taken from each diagnostic, not the method name.
85  pub fn emit_warnings(&mut self, warnings: Vec<Diagnostic<C>>) {
86    for d in warnings {
87      self.emit(d);
88    }
89  }
90
91  /// Batch-emit. Severity is taken from each diagnostic, not the method name.
92  pub fn emit_helps(&mut self, helps: Vec<Diagnostic<C>>) {
93    for d in helps {
94      self.emit(d);
95    }
96  }
97
98  /// Batch-emit. Severity is taken from each diagnostic, not the method name.
99  pub fn emit_notes(&mut self, notes: Vec<Diagnostic<C>>) {
100    for d in notes {
101      self.emit(d);
102    }
103  }
104
105  /// Move all diagnostics from `other` into `self` and merge counts.
106  pub fn extend(&mut self, other: DiagnosticEngine<C>) {
107    self.diagnostics.extend(other.diagnostics);
108    self.bug_count += other.bug_count;
109    self.error_count += other.error_count;
110    self.warning_count += other.warning_count;
111    self.help_count += other.help_count;
112    self.note_count += other.note_count;
113  }
114
115  /// Print every diagnostic with source snippets + carets to stdout, then the
116  /// summary line. Single-source convenience; for multi-file engines use
117  /// [`print_all_smart`].
118  pub fn print_all(&self, source_code: &str) {
119    let cache = SourceCache::new(source_code);
120    for d in &self.diagnostics {
121      let f = DiagnosticFormatter::with_cache(d, &cache);
122      print!("{}", f.format());
123    }
124    let summary = self.format_summary();
125    if !summary.is_empty() {
126      println!("\n{}", summary);
127    }
128  }
129
130  /// Pretty (colored) render of every diagnostic + summary into a string.
131  pub fn format_all(&self, source_code: &str) -> String {
132    self.format_all_with(source_code, RenderOptions::default())
133  }
134
135  /// Plain (no-color) variant of [`Self::format_all`]. Deterministic, suited
136  /// for CI logs.
137  pub fn format_all_plain(&self, source_code: &str) -> String {
138    let opts = RenderOptions { color: false, ..Default::default() };
139    self.format_all_with(source_code, opts)
140  }
141
142  /// Render every diagnostic with caller-supplied [`RenderOptions`].
143  pub fn format_all_with(&self, source_code: &str, options: RenderOptions) -> String {
144    let cache = SourceCache::new(source_code);
145    let mut out = String::new();
146    for d in &self.diagnostics {
147      let f = DiagnosticFormatter::with_cache(d, &cache).with_options(options);
148      out.push_str(&f.format());
149    }
150    if options.color {
151      out.push_str(&self.format_summary());
152    } else {
153      out.push_str(&self.format_summary_plain());
154    }
155    out
156  }
157
158  /// Render every diagnostic in compact (source-less) form. Use when the
159  /// caller doesn't have the original source string — log shippers, batch
160  /// CI summaries, LSP tools that already display source themselves.
161  /// Falls back to colored output; pair with [`Self::format_all_compact_plain`]
162  /// for deterministic CI logs.
163  pub fn format_all_compact(&self) -> String {
164    self.format_all_compact_with(true)
165  }
166
167  /// Plain (no-color) variant of [`Self::format_all_compact`].
168  pub fn format_all_compact_plain(&self) -> String {
169    self.format_all_compact_with(false)
170  }
171
172  fn format_all_compact_with(&self, color: bool) -> String {
173    let mut out = String::new();
174    for d in &self.diagnostics {
175      out.push_str(&compact::format_compact(d, color));
176    }
177    if color {
178      out.push_str(&self.format_summary());
179    } else {
180      out.push_str(&self.format_summary_plain());
181    }
182    out
183  }
184
185  /// Print every diagnostic in compact (source-less) form to stdout, then
186  /// the summary line.
187  pub fn print_all_compact(&self) {
188    print!("{}", self.format_all_compact());
189    let summary = self.format_summary();
190    if !summary.is_empty() {
191      println!("\n{}", summary);
192    }
193  }
194
195  /// Render the trailing summary line ("error: could not compile due to N
196  /// previous errors; M warnings emitted"). `color` toggles ANSI styling.
197  /// Returns the empty string when the engine has no errors / warnings.
198  pub fn summary(&self, color: bool) -> String {
199    self.format_summary_with(color)
200  }
201
202  fn format_summary(&self) -> String {
203    self.format_summary_with(true)
204  }
205
206  fn format_summary_plain(&self) -> String {
207    self.format_summary_with(false)
208  }
209
210  fn format_summary_with(&self, color: bool) -> String {
211    if self.error_count + self.warning_count + self.bug_count == 0 {
212      return String::new();
213    }
214    let total_errors = self.error_count + self.bug_count;
215    if total_errors > 0 {
216      let warn_part = if self.warning_count > 0 {
217        format!(
218          "; {} {} emitted",
219          paint(&self.warning_count.to_string(), color, |s| s.yellow().bold()),
220          plural("warning", self.warning_count),
221        )
222      } else {
223        String::new()
224      };
225      format!(
226        "{}: could not compile due to {} previous {}{}",
227        paint("error", color, |s| s.red().bold()),
228        paint(&total_errors.to_string(), color, |s| s.red().bold()),
229        plural("error", total_errors),
230        warn_part,
231      )
232    } else {
233      format!(
234        "{}: {} {} emitted",
235        paint("warning", color, |s| s.yellow().bold()),
236        paint(&self.warning_count.to_string(), color, |s| s.yellow().bold()),
237        plural("warning", self.warning_count),
238      )
239    }
240  }
241
242  // getters
243
244  /// All stored diagnostics in emit order.
245  pub fn get_diagnostics(&self) -> &[Diagnostic<C>] {
246    &self.diagnostics
247  }
248
249  /// Iterate stored diagnostics in emit order.
250  pub fn iter(&self) -> std::slice::Iter<'_, Diagnostic<C>> {
251    self.diagnostics.iter()
252  }
253
254  /// References to diagnostics with `Severity::Error`.
255  pub fn get_errors(&self) -> Vec<&Diagnostic<C>> {
256    self.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect()
257  }
258
259  /// References to diagnostics with `Severity::Warning`.
260  pub fn get_warnings(&self) -> Vec<&Diagnostic<C>> {
261    self.diagnostics.iter().filter(|d| d.severity == Severity::Warning).collect()
262  }
263
264  /// References to diagnostics with `Severity::Note`.
265  pub fn get_notes(&self) -> Vec<&Diagnostic<C>> {
266    self.diagnostics.iter().filter(|d| d.severity == Severity::Note).collect()
267  }
268
269  /// References to diagnostics with `Severity::Help`.
270  pub fn get_helps(&self) -> Vec<&Diagnostic<C>> {
271    self.diagnostics.iter().filter(|d| d.severity == Severity::Help).collect()
272  }
273
274  /// References to diagnostics with `Severity::Bug`.
275  pub fn get_bugs(&self) -> Vec<&Diagnostic<C>> {
276    self.diagnostics.iter().filter(|d| d.severity == Severity::Bug).collect()
277  }
278
279  /// True when no diagnostics have been emitted.
280  pub fn is_empty(&self) -> bool {
281    self.diagnostics.is_empty()
282  }
283
284  /// Total number of stored diagnostics across every severity.
285  pub fn len(&self) -> usize {
286    self.diagnostics.len()
287  }
288
289  /// Any `Severity::Error` emitted.
290  pub fn has_errors(&self) -> bool {
291    self.error_count > 0
292  }
293
294  /// Any `Severity::Warning` emitted.
295  pub fn has_warnings(&self) -> bool {
296    self.warning_count > 0
297  }
298
299  /// Any `Severity::Help` emitted.
300  pub fn has_helps(&self) -> bool {
301    self.help_count > 0
302  }
303
304  /// Any `Severity::Note` emitted.
305  pub fn has_notes(&self) -> bool {
306    self.note_count > 0
307  }
308
309  /// Any `Severity::Bug` (ICE) emitted.
310  pub fn has_bugs(&self) -> bool {
311    self.bug_count > 0
312  }
313
314  /// Count of `Severity::Bug` diagnostics.
315  pub fn bug_count(&self) -> usize {
316    self.bug_count
317  }
318
319  /// Count of `Severity::Error` diagnostics.
320  pub fn error_count(&self) -> usize {
321    self.error_count
322  }
323
324  /// Count of `Severity::Warning` diagnostics.
325  pub fn warning_count(&self) -> usize {
326    self.warning_count
327  }
328
329  /// Count of `Severity::Help` diagnostics.
330  pub fn help_count(&self) -> usize {
331    self.help_count
332  }
333
334  /// Count of `Severity::Note` diagnostics.
335  pub fn note_count(&self) -> usize {
336    self.note_count
337  }
338}
339
340#[cfg(feature = "json")]
341impl<C: DiagnosticCode + serde::Serialize> DiagnosticEngine<C> {
342  /// Render every diagnostic as a JSON array. Schema is stable: see
343  /// [`crate::json`].
344  pub fn format_all_json(&self) -> String {
345    crate::json::format_all_json(&self.diagnostics)
346  }
347}