stern4rust/
report_printer.rs1use std::collections::BTreeSet;
6
7use crate::offence::Offence;
8use crate::offence_threshold::OffenceThreshold;
9
10pub struct ReportPrinter {
14 files_scanned: usize,
15 threshold: OffenceThreshold,
16 applied: Vec<String>,
17 skipped: Vec<String>,
18 unconfigured: Vec<String>,
19}
20
21impl ReportPrinter {
22 pub fn new(files_scanned: usize) -> Self {
23 Self {
24 files_scanned,
25 threshold: OffenceThreshold::default(),
26 applied: Vec::new(),
27 skipped: Vec::new(),
28 unconfigured: Vec::new(),
29 }
30 }
31
32 pub fn with_rules(
33 self,
34 applied: Vec<String>,
35 skipped: Vec<String>,
36 unconfigured: Vec<String>,
37 ) -> Self {
38 Self {
39 applied,
40 skipped,
41 unconfigured,
42 ..self
43 }
44 }
45
46 pub fn with_threshold(self, threshold: OffenceThreshold) -> Self {
47 Self { threshold, ..self }
48 }
49
50 pub fn print(&self, offences: &[Offence]) {
51 println!("{}", self.render(offences));
52 }
53
54 pub fn render(&self, offences: &[Offence]) -> String {
57 let mut report = String::from("stern4rust report\n\n");
58 if offences.is_empty() {
59 report.push_str(self.clean_verdict());
60 report.push_str("\n\n");
61 report.push_str(&self.roster());
62 report.push_str(&self.summary(offences));
63 return report;
64 }
65
66 let shown = self.threshold.kept(offences);
69 let widths = ColumnWidths::of(shown);
70 report.push_str(&Self::heading(&widths));
71 for offence in shown {
72 report.push_str(&Self::row(offence, &widths));
73 report.push_str(&Self::correction_row(offence, &widths));
74 }
75 report.push('\n');
76 report.push_str(&self.omission(offences));
77 report.push_str(&self.roster());
78 report.push_str(&self.summary(offences));
79 report
80 }
81
82 fn clean_verdict(&self) -> &'static str {
87 if self.everything_ran() {
88 "All rules are satisfied."
89 } else {
90 "All applied rules are satisfied."
91 }
92 }
93
94 fn everything_ran(&self) -> bool {
95 self.skipped.is_empty() && self.unconfigured.is_empty()
96 }
97
98 fn roster(&self) -> String {
101 if self.applied.is_empty() {
102 return String::new();
103 }
104 let mut roster = format!(" applied: {}\n", self.applied.join(", "));
105 if !self.everything_ran() {
106 roster.push_str(&format!(" not applied: {}\n", self.absences().join(", ")));
107 }
108 roster.push('\n');
109 roster
110 }
111
112 fn absences(&self) -> Vec<String> {
117 self.skipped
118 .iter()
119 .map(|name| format!("{name} (skipped)"))
120 .chain(
121 self.unconfigured
122 .iter()
123 .map(|name| format!("{name} (needs --header-file)")),
124 )
125 .collect()
126 }
127
128 fn omission(&self, offences: &[Offence]) -> String {
132 let omitted = self.threshold.omitted(offences);
133 if omitted == 0 {
134 return String::new();
135 }
136 format!(
137 "... and {omitted} more offences not shown. Raise --offence-threshold \
138 (currently {}, use 0 for all) to see them.\n\n",
139 self.threshold.limit()
140 )
141 }
142
143 fn heading(widths: &ColumnWidths) -> String {
144 format!(
145 "{:<file$} {:>line$} {:<rule$} offence\n{} {} {} {}\n",
146 "file",
147 "line",
148 "rule",
149 "-".repeat(widths.file),
150 "-".repeat(widths.line),
151 "-".repeat(widths.rule),
152 "-".repeat(widths.description),
153 file = widths.file,
154 line = widths.line,
155 rule = widths.rule
156 )
157 }
158
159 fn row(offence: &Offence, widths: &ColumnWidths) -> String {
160 format!(
161 "{:<file$} {:>line$} {:<rule$} {}\n",
162 offence.file,
163 offence.line,
164 offence.rule,
165 offence.description,
166 file = widths.file,
167 line = widths.line,
168 rule = widths.rule
169 )
170 }
171
172 fn correction_row(offence: &Offence, widths: &ColumnWidths) -> String {
177 let indent = widths.file + widths.line + widths.rule + 6;
178 format!("{}fix: {}\n", " ".repeat(indent), offence.correction)
179 }
180
181 fn summary(&self, offences: &[Offence]) -> String {
182 let broken: BTreeSet<&str> = offences.iter().map(|offence| offence.rule).collect();
183 format!(
184 "summary: files_scanned={} offences={} rules_broken={} rules_applied={} \
185 rules_skipped={} rules_unconfigured={}",
186 self.files_scanned,
187 offences.len(),
188 broken.len(),
189 self.applied.len(),
190 self.skipped.len(),
191 self.unconfigured.len()
192 )
193 }
194}
195
196struct ColumnWidths {
197 file: usize,
198 line: usize,
199 rule: usize,
200 description: usize,
201}
202
203impl ColumnWidths {
204 fn of(offences: &[Offence]) -> Self {
205 Self {
206 file: Self::widest(offences.iter().map(|offence| offence.file.len()), "file"),
207 line: Self::widest(
208 offences
209 .iter()
210 .map(|offence| offence.line.to_string().len()),
211 "line",
212 ),
213 rule: Self::widest(offences.iter().map(|offence| offence.rule.len()), "rule"),
214 description: Self::widest(
215 offences.iter().map(|offence| offence.description.len()),
216 "offence",
217 ),
218 }
219 }
220
221 fn widest<I: Iterator<Item = usize>>(lengths: I, heading: &str) -> usize {
222 lengths.max().unwrap_or(0).max(heading.len())
223 }
224}