use std::collections::BTreeMap;
use std::io::Write;
use anyhow::Result;
use crate::reporter::Reporter;
use crate::rules::{Severity, Violation};
const RULE_DOC_BASE: &str = "https://github.com/jamesmhall/ailint";
#[derive(Debug)]
pub struct MarkdownReporter {
version: &'static str,
}
impl Default for MarkdownReporter {
fn default() -> Self {
Self {
version: crate::VERSION,
}
}
}
impl MarkdownReporter {
pub fn with_version(version: &'static str) -> Self {
Self { version }
}
}
impl Reporter for MarkdownReporter {
fn report(&self, violations: &[Violation], out: &mut dyn Write) -> Result<()> {
writeln!(out, "# ailint report")?;
writeln!(out)?;
if violations.is_empty() {
writeln!(out, "_No violations._")?;
return Ok(());
}
let mut by_file: BTreeMap<&std::path::Path, Vec<&Violation>> = BTreeMap::new();
for v in violations {
by_file.entry(v.file.as_path()).or_default().push(v);
}
let (mut errors, mut warnings, mut infos) = (0usize, 0usize, 0usize);
for v in violations {
match v.severity {
Severity::Error => errors += 1,
Severity::Warning => warnings += 1,
Severity::Info => infos += 1,
}
}
writeln!(
out,
"**{} violations** across {} files.",
violations.len(),
by_file.len()
)?;
writeln!(out)?;
writeln!(out, "| Severity | Count |")?;
writeln!(out, "|----------|------:|")?;
writeln!(out, "| error | {:>5} |", errors)?;
writeln!(out, "| warning | {:>5} |", warnings)?;
writeln!(out, "| info | {:>5} |", infos)?;
writeln!(out)?;
for (path, mut file_violations) in by_file {
file_violations.sort_by_key(|v| v.line.unwrap_or(0));
writeln!(out, "## `{}`", path.display())?;
writeln!(out)?;
writeln!(out, "| Rule | Severity | Line | Message |")?;
writeln!(out, "|------|----------|-----:|---------|")?;
for v in file_violations {
let sev = v.severity.as_str();
let badge_color = match v.severity {
Severity::Error => "red",
Severity::Warning => "yellow",
Severity::Info => "blue",
};
let badge_url = format!("https://img.shields.io/badge/-{}-{}", sev, badge_color);
let doc_url = format!("{}#{}", RULE_DOC_BASE, v.rule_id.slug);
let line = v.line.map(|n| n.to_string()).unwrap_or_default();
writeln!(
out,
"| []({doc_url}) `{code}` | {sev} | {line} | {msg} |",
sev = sev,
badge_url = badge_url,
doc_url = doc_url,
code = v.rule_id.code_str(),
line = line,
msg = escape_message(&v.message),
)?;
}
writeln!(out)?;
}
writeln!(out, "---")?;
writeln!(out, "_Generated by ailint {}._", self.version)?;
Ok(())
}
}
fn escape_message(msg: &str) -> String {
let mut out = String::with_capacity(msg.len());
for ch in msg.chars() {
match ch {
'|' => out.push_str("\\|"),
'\n' | '\r' => out.push(' '),
other => out.push(other),
}
}
out
}