#[cfg(test)]
mod tests;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
Info,
Warning,
Error,
}
impl Severity {
pub fn label(self) -> &'static str {
match self {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Info => "info",
}
}
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub severity: Severity,
pub summary: String,
pub file: String,
pub line: usize,
pub col: usize,
pub explanation: Option<String>,
pub suggestion: Option<String>,
}
impl Diagnostic {
pub fn error(file: impl Into<String>, summary: impl Into<String>) -> Self {
Self {
severity: Severity::Error,
summary: summary.into(),
file: file.into(),
line: 0,
col: 0,
explanation: None,
suggestion: None,
}
}
pub fn warning(file: impl Into<String>, summary: impl Into<String>) -> Self {
Self {
severity: Severity::Warning,
summary: summary.into(),
file: file.into(),
line: 0,
col: 0,
explanation: None,
suggestion: None,
}
}
pub fn info(file: impl Into<String>, summary: impl Into<String>) -> Self {
Self {
severity: Severity::Info,
summary: summary.into(),
file: file.into(),
line: 0,
col: 0,
explanation: None,
suggestion: None,
}
}
pub fn at(mut self, line: usize, col: usize) -> Self {
self.line = line;
self.col = col;
self
}
pub fn with_explanation(mut self, explanation: impl Into<String>) -> Self {
self.explanation = Some(explanation.into());
self
}
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
pub fn is_error(&self) -> bool {
self.severity == Severity::Error
}
}
impl std::fmt::Display for Diagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}:{}:{}: {}: {}",
self.file, self.line, self.col, self.severity, self.summary
)
}
}
pub fn format_human(diag: &Diagnostic) -> String {
let mut out = String::new();
out.push_str(diag.severity.label());
out.push_str(": ");
out.push_str(&diag.summary);
out.push('\n');
if diag.line > 0 {
out.push_str(&format!(" --> {}:{}:{}\n", diag.file, diag.line, diag.col));
} else {
out.push_str(&format!(" --> {}\n", diag.file));
}
if let Some(ref explanation) = diag.explanation {
out.push_str(&format!(" = note: {explanation}\n"));
}
if let Some(ref suggestion) = diag.suggestion {
out.push_str(&format!(" = help: {suggestion}\n"));
}
out
}
pub fn format_human_all(diags: &[Diagnostic]) -> String {
let mut out = String::new();
for diag in diags {
out.push_str(&format_human(diag));
out.push('\n');
}
out.push_str(&format_summary(diags));
out
}
pub fn format_json(diag: &Diagnostic) -> String {
let mut out = String::from("{");
out.push_str(&format!("\"severity\":\"{}\",", diag.severity.label()));
out.push_str(&format!("\"summary\":{},", json_escape(&diag.summary)));
out.push_str(&format!("\"file\":{},", json_escape(&diag.file)));
out.push_str(&format!("\"line\":{},", diag.line));
out.push_str(&format!("\"col\":{}", diag.col));
if let Some(ref explanation) = diag.explanation {
out.push_str(&format!(",\"explanation\":{}", json_escape(explanation)));
}
if let Some(ref suggestion) = diag.suggestion {
out.push_str(&format!(",\"suggestion\":{}", json_escape(suggestion)));
}
out.push('}');
out
}
pub fn format_json_all(diags: &[Diagnostic]) -> String {
let mut out = String::new();
for diag in diags {
out.push_str(&format_json(diag));
out.push('\n');
}
out
}
pub fn format_summary(diags: &[Diagnostic]) -> String {
let errors = diags
.iter()
.filter(|d| d.severity == Severity::Error)
.count();
let warnings = diags
.iter()
.filter(|d| d.severity == Severity::Warning)
.count();
match (errors, warnings) {
(0, 0) => "no errors".to_string(),
(e, 0) => format!("{e} error{}", plural(e)),
(0, w) => format!("{w} warning{}", plural(w)),
(e, w) => format!("{e} error{}, {w} warning{}", plural(e), plural(w)),
}
}
pub fn error_count(diags: &[Diagnostic]) -> usize {
diags
.iter()
.filter(|d| d.severity == Severity::Error)
.count()
}
pub fn has_errors(diags: &[Diagnostic]) -> bool {
diags.iter().any(|d| d.severity == Severity::Error)
}
fn plural(n: usize) -> &'static str {
if n == 1 { "" } else { "s" }
}
fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c < '\x20' => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}