ailint_core/reporter/
markdown.rs1use std::collections::BTreeMap;
4use std::io::Write;
5
6use anyhow::Result;
7
8use crate::reporter::Reporter;
9use crate::rules::registry::rule_meta;
10use crate::rules::{Severity, Violation};
11
12const RULE_DOC_BASE: &str = "https://github.com/jamesmhall/ailint";
13
14#[derive(Debug)]
16pub struct MarkdownReporter {
17 version: &'static str,
18}
19
20impl Default for MarkdownReporter {
21 fn default() -> Self {
22 Self {
23 version: crate::VERSION,
24 }
25 }
26}
27
28impl MarkdownReporter {
29 pub fn with_version(version: &'static str) -> Self {
31 Self { version }
32 }
33}
34
35impl Reporter for MarkdownReporter {
36 fn report(&self, violations: &[Violation], out: &mut dyn Write) -> Result<()> {
37 writeln!(out, "# ailint report")?;
38 writeln!(out)?;
39
40 if violations.is_empty() {
41 writeln!(out, "_No violations._")?;
42 return Ok(());
43 }
44
45 let mut by_file: BTreeMap<&std::path::Path, Vec<&Violation>> = BTreeMap::new();
46 for v in violations {
47 by_file.entry(v.file.as_path()).or_default().push(v);
48 }
49
50 let (mut errors, mut warnings, mut infos) = (0usize, 0usize, 0usize);
51 for v in violations {
52 match v.severity {
53 Severity::Error => errors += 1,
54 Severity::Warning => warnings += 1,
55 Severity::Info => infos += 1,
56 }
57 }
58
59 writeln!(
60 out,
61 "**{} violations** across {} files.",
62 violations.len(),
63 by_file.len()
64 )?;
65 writeln!(out)?;
66
67 writeln!(out, "| Severity | Count |")?;
68 writeln!(out, "|----------|------:|")?;
69 writeln!(out, "| error | {:>5} |", errors)?;
70 writeln!(out, "| warning | {:>5} |", warnings)?;
71 writeln!(out, "| info | {:>5} |", infos)?;
72 writeln!(out)?;
73
74 for (path, mut file_violations) in by_file {
75 file_violations.sort_by_key(|v| v.line.unwrap_or(0));
76 writeln!(out, "## `{}`", path.display())?;
77 writeln!(out)?;
78 writeln!(out, "| Rule | Severity | Line | Message | Detail | Fix |")?;
79 writeln!(out, "|------|----------|-----:|---------|--------|-----|")?;
80 for v in file_violations {
81 let sev = v.severity.as_str();
82 let badge_color = match v.severity {
83 Severity::Error => "red",
84 Severity::Warning => "yellow",
85 Severity::Info => "blue",
86 };
87 let badge_url = format!("https://img.shields.io/badge/-{}-{}", sev, badge_color);
88 let doc_url = format!("{}#{}", RULE_DOC_BASE, v.rule_id.slug);
89 let line = v.line.map(|n| n.to_string()).unwrap_or_default();
90 let meta = rule_meta(v.rule_id);
91 let fix = meta.map(|m| m.fix_hint).unwrap_or("");
92 let detail = v.detail.as_deref().unwrap_or("");
93 writeln!(
94 out,
95 "| []({doc_url}) `{code}` | {sev} | {line} | {msg} | {detail} | {fix} |",
96 sev = sev,
97 badge_url = badge_url,
98 doc_url = doc_url,
99 code = v.rule_id.code_str(),
100 line = line,
101 msg = escape_message(&v.message),
102 detail = escape_message(detail),
103 fix = escape_message(fix),
104 )?;
105 }
106 writeln!(out)?;
107 }
108
109 writeln!(out, "---")?;
110 writeln!(out, "_Generated by ailint {}._", self.version)?;
111 Ok(())
112 }
113}
114
115fn escape_message(msg: &str) -> String {
116 let mut out = String::with_capacity(msg.len());
117 for ch in msg.chars() {
118 match ch {
119 '|' => out.push_str("\\|"),
120 '\n' | '\r' => out.push(' '),
121 other => out.push(other),
122 }
123 }
124 out
125}