duck-diag 0.8.1

Generic diagnostic engine for building rich error reporting into any tool
Documentation
//! Generic diagnostic engine for tools that need rich, rustc-style error
//! output. Plug in your own error code enum, attach spans + labels +
//! suggestions, and render in pretty (color), plain, or JSON modes.
//!
//! See `examples/` for end-to-end demos.

mod compact;
mod diagnostic;
mod formatter;
#[cfg(feature = "json")]
mod json;
mod macros;
mod smart;
mod style;

pub use compact::format_compact;
pub use diagnostic::*;
pub use formatter::{DiagnosticFormatter, RenderOptions, SourceCache};
pub use smart::{format_all_smart, print_all_smart};

use crate::style::*;
use colored::*;

/// Collects diagnostics, tracks per-severity counts, and renders them in
/// pretty / plain / compact / JSON modes.
#[derive(Debug)]
pub struct DiagnosticEngine<C: DiagnosticCode> {
  diagnostics: Vec<Diagnostic<C>>,
  bug_count: usize,
  error_count: usize,
  warning_count: usize,
  help_count: usize,
  note_count: usize,
}

impl<C: DiagnosticCode> Default for DiagnosticEngine<C> {
  fn default() -> Self {
    Self {
      diagnostics: Vec::new(),
      bug_count: 0,
      error_count: 0,
      warning_count: 0,
      help_count: 0,
      note_count: 0,
    }
  }
}

impl<C: DiagnosticCode> DiagnosticEngine<C> {
  /// New empty engine.
  pub fn new() -> Self {
    Self::default()
  }

  /// Drop every stored diagnostic and reset all counts.
  pub fn clear(&mut self) {
    self.diagnostics.clear();
    self.bug_count = 0;
    self.error_count = 0;
    self.warning_count = 0;
    self.help_count = 0;
    self.note_count = 0;
  }

  /// Push a diagnostic and bump its severity bucket.
  pub fn emit(&mut self, diagnostic: Diagnostic<C>) {
    match diagnostic.severity {
      Severity::Bug => self.bug_count += 1,
      Severity::Error => self.error_count += 1,
      Severity::Warning => self.warning_count += 1,
      Severity::Help => self.help_count += 1,
      Severity::Note => self.note_count += 1,
    }
    self.diagnostics.push(diagnostic);
  }

  /// Batch-emit. Severity is taken from each diagnostic, not the method name.
  pub fn emit_errors(&mut self, errors: Vec<Diagnostic<C>>) {
    for d in errors {
      self.emit(d);
    }
  }

  /// Batch-emit. Severity is taken from each diagnostic, not the method name.
  pub fn emit_warnings(&mut self, warnings: Vec<Diagnostic<C>>) {
    for d in warnings {
      self.emit(d);
    }
  }

  /// Batch-emit. Severity is taken from each diagnostic, not the method name.
  pub fn emit_helps(&mut self, helps: Vec<Diagnostic<C>>) {
    for d in helps {
      self.emit(d);
    }
  }

  /// Batch-emit. Severity is taken from each diagnostic, not the method name.
  pub fn emit_notes(&mut self, notes: Vec<Diagnostic<C>>) {
    for d in notes {
      self.emit(d);
    }
  }

  /// Move all diagnostics from `other` into `self` and merge counts.
  pub fn extend(&mut self, other: DiagnosticEngine<C>) {
    self.diagnostics.extend(other.diagnostics);
    self.bug_count += other.bug_count;
    self.error_count += other.error_count;
    self.warning_count += other.warning_count;
    self.help_count += other.help_count;
    self.note_count += other.note_count;
  }

  /// Print every diagnostic with source snippets + carets to stdout, then the
  /// summary line. Single-source convenience; for multi-file engines use
  /// [`print_all_smart`].
  pub fn print_all(&self, source_code: &str) {
    let cache = SourceCache::new(source_code);
    for d in &self.diagnostics {
      let f = DiagnosticFormatter::with_cache(d, &cache);
      print!("{}", f.format());
    }
    let summary = self.format_summary();
    if !summary.is_empty() {
      println!("\n{}", summary);
    }
  }

  /// Pretty (colored) render of every diagnostic + summary into a string.
  pub fn format_all(&self, source_code: &str) -> String {
    self.format_all_with(source_code, RenderOptions::default())
  }

  /// Plain (no-color) variant of [`Self::format_all`]. Deterministic, suited
  /// for CI logs.
  pub fn format_all_plain(&self, source_code: &str) -> String {
    let opts = RenderOptions { color: false, ..Default::default() };
    self.format_all_with(source_code, opts)
  }

  /// Render every diagnostic with caller-supplied [`RenderOptions`].
  pub fn format_all_with(&self, source_code: &str, options: RenderOptions) -> String {
    let cache = SourceCache::new(source_code);
    let mut out = String::new();
    for d in &self.diagnostics {
      let f = DiagnosticFormatter::with_cache(d, &cache).with_options(options);
      out.push_str(&f.format());
    }
    if options.color {
      out.push_str(&self.format_summary());
    } else {
      out.push_str(&self.format_summary_plain());
    }
    out
  }

  /// Render every diagnostic in compact (source-less) form. Use when the
  /// caller doesn't have the original source string — log shippers, batch
  /// CI summaries, LSP tools that already display source themselves.
  /// Falls back to colored output; pair with [`Self::format_all_compact_plain`]
  /// for deterministic CI logs.
  pub fn format_all_compact(&self) -> String {
    self.format_all_compact_with(true)
  }

  /// Plain (no-color) variant of [`Self::format_all_compact`].
  pub fn format_all_compact_plain(&self) -> String {
    self.format_all_compact_with(false)
  }

  fn format_all_compact_with(&self, color: bool) -> String {
    let mut out = String::new();
    for d in &self.diagnostics {
      out.push_str(&compact::format_compact(d, color));
    }
    if color {
      out.push_str(&self.format_summary());
    } else {
      out.push_str(&self.format_summary_plain());
    }
    out
  }

  /// Print every diagnostic in compact (source-less) form to stdout, then
  /// the summary line.
  pub fn print_all_compact(&self) {
    print!("{}", self.format_all_compact());
    let summary = self.format_summary();
    if !summary.is_empty() {
      println!("\n{}", summary);
    }
  }

  /// Render the trailing summary line ("error: could not compile due to N
  /// previous errors; M warnings emitted"). `color` toggles ANSI styling.
  /// Returns the empty string when the engine has no errors / warnings.
  pub fn summary(&self, color: bool) -> String {
    self.format_summary_with(color)
  }

  fn format_summary(&self) -> String {
    self.format_summary_with(true)
  }

  fn format_summary_plain(&self) -> String {
    self.format_summary_with(false)
  }

  fn format_summary_with(&self, color: bool) -> String {
    if self.error_count + self.warning_count + self.bug_count == 0 {
      return String::new();
    }
    let total_errors = self.error_count + self.bug_count;
    if total_errors > 0 {
      let warn_part = if self.warning_count > 0 {
        format!(
          "; {} {} emitted",
          paint(&self.warning_count.to_string(), color, |s| s.yellow().bold()),
          plural("warning", self.warning_count),
        )
      } else {
        String::new()
      };
      format!(
        "{}: could not compile due to {} previous {}{}",
        paint("error", color, |s| s.red().bold()),
        paint(&total_errors.to_string(), color, |s| s.red().bold()),
        plural("error", total_errors),
        warn_part,
      )
    } else {
      format!(
        "{}: {} {} emitted",
        paint("warning", color, |s| s.yellow().bold()),
        paint(&self.warning_count.to_string(), color, |s| s.yellow().bold()),
        plural("warning", self.warning_count),
      )
    }
  }

  // getters

  /// All stored diagnostics in emit order.
  pub fn get_diagnostics(&self) -> &[Diagnostic<C>] {
    &self.diagnostics
  }

  /// Iterate stored diagnostics in emit order.
  pub fn iter(&self) -> std::slice::Iter<'_, Diagnostic<C>> {
    self.diagnostics.iter()
  }

  /// References to diagnostics with `Severity::Error`.
  pub fn get_errors(&self) -> Vec<&Diagnostic<C>> {
    self.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect()
  }

  /// References to diagnostics with `Severity::Warning`.
  pub fn get_warnings(&self) -> Vec<&Diagnostic<C>> {
    self.diagnostics.iter().filter(|d| d.severity == Severity::Warning).collect()
  }

  /// References to diagnostics with `Severity::Note`.
  pub fn get_notes(&self) -> Vec<&Diagnostic<C>> {
    self.diagnostics.iter().filter(|d| d.severity == Severity::Note).collect()
  }

  /// References to diagnostics with `Severity::Help`.
  pub fn get_helps(&self) -> Vec<&Diagnostic<C>> {
    self.diagnostics.iter().filter(|d| d.severity == Severity::Help).collect()
  }

  /// References to diagnostics with `Severity::Bug`.
  pub fn get_bugs(&self) -> Vec<&Diagnostic<C>> {
    self.diagnostics.iter().filter(|d| d.severity == Severity::Bug).collect()
  }

  /// True when no diagnostics have been emitted.
  pub fn is_empty(&self) -> bool {
    self.diagnostics.is_empty()
  }

  /// Total number of stored diagnostics across every severity.
  pub fn len(&self) -> usize {
    self.diagnostics.len()
  }

  /// Any `Severity::Error` emitted.
  pub fn has_errors(&self) -> bool {
    self.error_count > 0
  }

  /// Any `Severity::Warning` emitted.
  pub fn has_warnings(&self) -> bool {
    self.warning_count > 0
  }

  /// Any `Severity::Help` emitted.
  pub fn has_helps(&self) -> bool {
    self.help_count > 0
  }

  /// Any `Severity::Note` emitted.
  pub fn has_notes(&self) -> bool {
    self.note_count > 0
  }

  /// Any `Severity::Bug` (ICE) emitted.
  pub fn has_bugs(&self) -> bool {
    self.bug_count > 0
  }

  /// Count of `Severity::Bug` diagnostics.
  pub fn bug_count(&self) -> usize {
    self.bug_count
  }

  /// Count of `Severity::Error` diagnostics.
  pub fn error_count(&self) -> usize {
    self.error_count
  }

  /// Count of `Severity::Warning` diagnostics.
  pub fn warning_count(&self) -> usize {
    self.warning_count
  }

  /// Count of `Severity::Help` diagnostics.
  pub fn help_count(&self) -> usize {
    self.help_count
  }

  /// Count of `Severity::Note` diagnostics.
  pub fn note_count(&self) -> usize {
    self.note_count
  }
}

#[cfg(feature = "json")]
impl<C: DiagnosticCode + serde::Serialize> DiagnosticEngine<C> {
  /// Render every diagnostic as a JSON array. Schema is stable: see
  /// [`crate::json`].
  pub fn format_all_json(&self) -> String {
    crate::json::format_all_json(&self.diagnostics)
  }
}