1use crate::check::{Finding, Severity};
6use crate::registry::Registry;
7
8const HINT_INDENT: &str = " ▶ hint: ";
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum OutputFormat {
12 Text,
14 GithubAnnotations,
16}
17
18pub fn format_github_annotations(findings: &[Finding]) -> String {
22 let mut out = String::new();
23 for f in findings {
24 let level = match f.severity {
25 Severity::Hard => "error",
26 Severity::Advisory => "warning",
27 };
28 out.push_str(&format!(
29 "::{level} file={file},line={line}::[{check}] {kind} — {claim}\n",
30 file = f.file.display(),
31 line = f.line.max(1),
32 check = f.check_id,
33 kind = f.kind.short(),
34 claim = collapse_newlines(&truncate(&f.claim, 200)),
35 ));
36 }
37 out
38}
39
40fn collapse_newlines(s: &str) -> String {
41 s.replace(['\n', '\r'], " ").trim().to_string()
42}
43
44pub fn format_report(reg: &Registry, findings: &[Finding], explain: bool) -> String {
45 let mut out = String::new();
46 if findings.is_empty() {
47 out.push_str(&format!(
48 "drift: no findings ({} check(s) ran)\n",
49 reg.len()
50 ));
51 return out;
52 }
53
54 let mut last_file: Option<&std::path::Path> = None;
55 let mut hard = 0usize;
56 let mut advisory = 0usize;
57 for f in findings {
58 match f.severity {
59 Severity::Hard => hard += 1,
60 Severity::Advisory => advisory += 1,
61 }
62 if last_file != Some(f.file.as_path()) {
63 out.push('\n');
64 out.push_str(&format!("{} {}\n", f.severity.label(), f.file.display()));
65 last_file = Some(f.file.as_path());
66 }
67 out.push_str(&format!(
68 " line {}: [{}] {}\n",
69 f.line,
70 f.check_id,
71 f.kind.short()
72 ));
73 if explain {
74 let intent = reg
75 .checks()
76 .find(|c| c.id() == f.check_id)
77 .map(|c| c.intent())
78 .unwrap_or("");
79 out.push_str(&format!(" intent: {intent}\n"));
80 out.push_str(&format!(" claim : {}\n", f.claim));
81 } else {
82 out.push_str(&format!(" claim : {}\n", truncate(&f.claim, 120)));
83 }
84 if let Some(hint) = &f.fix_hint {
85 out.push_str(&format!("{HINT_INDENT}{hint}\n"));
86 }
87 }
88 out.push_str(&format!(
89 "\nsummary: {hard} hard, {advisory} advisory ({} check(s) ran)\n",
90 reg.len()
91 ));
92 out
93}
94
95fn truncate(s: &str, max: usize) -> String {
96 if s.chars().count() <= max {
97 return s.to_string();
98 }
99 let cut: String = s.chars().take(max).collect();
100 format!("{cut}…")
101}