use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::path::PathBuf;
use annotate_snippets::{Level, Renderer, Snippet};
use crate::text::LineIndex;
use super::diagnostic::{Diagnostic, Severity};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputMode {
#[default]
Pretty,
Concise,
Json,
}
pub fn render_findings(
diagnostics: &[Diagnostic],
mode: OutputMode,
source_for: &dyn Fn(&PathBuf) -> Option<String>,
) -> String {
match mode {
OutputMode::Json => render_json(diagnostics),
OutputMode::Concise => render_concise(diagnostics, source_for),
OutputMode::Pretty => render_pretty(diagnostics, source_for),
}
}
fn render_json(diagnostics: &[Diagnostic]) -> String {
serde_json::to_string_pretty(diagnostics).unwrap_or_else(|_| "[]".to_string())
}
fn render_concise(
diagnostics: &[Diagnostic],
source_for: &dyn Fn(&PathBuf) -> Option<String>,
) -> String {
let mut by_path: BTreeMap<&PathBuf, Vec<&Diagnostic>> = BTreeMap::new();
for d in diagnostics {
by_path.entry(&d.path).or_default().push(d);
}
let mut out = String::new();
for (path, diags) in by_path {
let source = source_for(path);
let line_index = source.as_deref().map(LineIndex::new);
for d in diags {
let (line, col) = match &line_index {
Some(idx) => {
let lc = idx.byte_to_lc(u32::from(d.range.start()) as usize);
(lc.line, lc.column)
}
None => (1, 1),
};
let _ = writeln!(
out,
"{}:{line}:{col}: {} [{}] {}",
path.display(),
severity_word(d.severity),
d.rule,
d.message.body
);
}
}
out
}
fn render_pretty(
diagnostics: &[Diagnostic],
source_for: &dyn Fn(&PathBuf) -> Option<String>,
) -> String {
let renderer = Renderer::plain();
let mut by_path: BTreeMap<&PathBuf, Vec<&Diagnostic>> = BTreeMap::new();
for d in diagnostics {
by_path.entry(&d.path).or_default().push(d);
}
let mut out = String::new();
for (path, diags) in by_path {
let Some(source) = source_for(path) else {
for d in &diags {
let _ = writeln!(
out,
"{}: {} [{}] {}",
path.display(),
severity_word(d.severity),
d.rule,
d.message.body
);
}
continue;
};
let origin = path.display().to_string();
for d in &diags {
let level = severity_level(d.severity);
let start = u32::from(d.range.start()) as usize;
let end = u32::from(d.range.end()) as usize;
let snippet = Snippet::source(&source)
.origin(&origin)
.annotation(level.span(start..end).label(&d.message.body));
let title = level.title(d.rule);
let message = title.snippet(snippet);
let rendered = renderer.render(message);
let _ = writeln!(out, "{rendered}");
if let Some(s) = &d.message.suggestion {
let _ = writeln!(out, " = help: {s}");
}
}
}
out
}
fn severity_word(s: Severity) -> &'static str {
match s {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Info => "info",
Severity::Hint => "hint",
}
}
fn severity_level(s: Severity) -> Level {
match s {
Severity::Error => Level::Error,
Severity::Warning => Level::Warning,
Severity::Info => Level::Info,
Severity::Hint => Level::Help,
}
}