use std::fmt;
const RESET: &str = "\x1b[0m";
const BOLD: &str = "\x1b[1m";
const BLUE: &str = "\x1b[1;94m";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
Note,
}
impl Severity {
fn parse(s: &str) -> Option<Self> {
match s.trim() {
"error" | "fatal error" => Some(Self::Error),
"warning" => Some(Self::Warning),
"note" => Some(Self::Note),
_ => None,
}
}
fn color(self) -> &'static str {
match self {
Self::Error => "\x1b[1;91m",
Self::Warning => "\x1b[1;93m",
Self::Note => "\x1b[1;96m",
}
}
fn label(self) -> &'static str {
match self {
Self::Error => "error",
Self::Warning => "warning",
Self::Note => "note",
}
}
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.label())
}
}
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub file: String,
pub line: usize,
pub column: usize,
pub severity: Severity,
pub message: String,
}
impl Diagnostic {
pub fn parse_line(line: &str) -> Option<Self> {
let mut parts = line.splitn(4, ':');
let file = parts.next()?.trim();
let line_num: usize = parts.next()?.trim().parse().ok()?;
let col_num: usize = parts.next()?.trim().parse().ok()?;
let rest = parts.next()?.trim();
let (sev_str, message) = rest.split_once(':')?;
let severity = Severity::parse(sev_str)?;
if file.is_empty() || line_num == 0 || col_num == 0 {
return None;
}
Some(Self {
file: file.to_string(),
line: line_num,
column: col_num,
severity,
message: message.trim().to_string(),
})
}
}
pub fn parse_all(stderr: &str) -> Vec<Diagnostic> {
stderr.lines().filter_map(Diagnostic::parse_line).collect()
}
pub fn print_diagnostic(diag: &Diagnostic) {
let color = diag.severity.color();
let gutter = diag.line.to_string().len().max(2);
eprintln!(
"{color}{BOLD}{}{RESET}{BOLD}: {}{RESET}",
diag.severity, diag.message
);
eprintln!(
"{BLUE}{:>gutter$}{RESET} {BLUE}-->{RESET} {}:{}:{}",
"",
diag.file,
diag.line,
diag.column,
gutter = gutter
);
eprintln!("{BLUE}{:>gutter$} |{RESET}", "", gutter = gutter);
if let Ok(content) = std::fs::read_to_string(&diag.file) {
if let Some(code_line) = content.lines().nth(diag.line.saturating_sub(1)) {
eprintln!(
"{BLUE}{:>gutter$} |{RESET} {}",
diag.line,
code_line,
gutter = gutter
);
let caret_pad = diag.column.saturating_sub(1);
let padding: String = code_line
.chars()
.take(caret_pad)
.map(|c| if c == '\t' { '\t' } else { ' ' })
.collect();
eprintln!(
"{BLUE}{:>gutter$} |{RESET} {}{color}{BOLD}^{RESET}",
"",
padding,
gutter = gutter
);
}
}
eprintln!("{BLUE}{:>gutter$} |{RESET}", "", gutter = gutter);
eprintln!();
}
pub fn print_all(diagnostics: &[Diagnostic]) {
for diag in diagnostics {
print_diagnostic(diag);
}
print_summary(diagnostics);
}
pub fn print_summary(diagnostics: &[Diagnostic]) {
let errors = diagnostics
.iter()
.filter(|d| d.severity == Severity::Error)
.count();
let warnings = diagnostics
.iter()
.filter(|d| d.severity == Severity::Warning)
.count();
if errors > 0 {
let noun = if errors == 1 { "error" } else { "errors" };
eprintln!(
"{}{}error{}: could not compile due to {} previous {}",
BOLD,
Severity::Error.color(),
RESET,
errors,
noun
);
} else if warnings > 0 {
let noun = if warnings == 1 { "warning" } else { "warnings" };
eprintln!(
"{}{}warning{}: {} {} emitted",
BOLD,
Severity::Warning.color(),
RESET,
warnings,
noun
);
}
}