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}
17
18impl ReportPrinter {
19 pub fn new(files_scanned: usize) -> Self {
20 Self {
21 files_scanned,
22 threshold: OffenceThreshold::default(),
23 }
24 }
25
26 pub fn with_threshold(self, threshold: OffenceThreshold) -> Self {
27 Self { threshold, ..self }
28 }
29
30 pub fn print(&self, offences: &[Offence]) {
31 println!("{}", self.render(offences));
32 }
33
34 pub fn render(&self, offences: &[Offence]) -> String {
37 let mut report = String::from("stern4rust report\n\n");
38 if offences.is_empty() {
39 report.push_str("All rules are satisfied.\n\n");
40 report.push_str(&self.summary(offences));
41 return report;
42 }
43
44 let shown = self.threshold.kept(offences);
47 let widths = ColumnWidths::of(shown);
48 report.push_str(&Self::heading(&widths));
49 for offence in shown {
50 report.push_str(&Self::row(offence, &widths));
51 report.push_str(&Self::correction_row(offence, &widths));
52 }
53 report.push('\n');
54 report.push_str(&self.omission(offences));
55 report.push_str(&self.summary(offences));
56 report
57 }
58
59 fn omission(&self, offences: &[Offence]) -> String {
63 let omitted = self.threshold.omitted(offences);
64 if omitted == 0 {
65 return String::new();
66 }
67 format!(
68 "... and {omitted} more offences not shown. Raise --offence-threshold \
69 (currently {}, use 0 for all) to see them.\n\n",
70 self.threshold.limit()
71 )
72 }
73
74 fn heading(widths: &ColumnWidths) -> String {
75 format!(
76 "{:<file$} {:>line$} {:<rule$} offence\n{} {} {} {}\n",
77 "file",
78 "line",
79 "rule",
80 "-".repeat(widths.file),
81 "-".repeat(widths.line),
82 "-".repeat(widths.rule),
83 "-".repeat(widths.description),
84 file = widths.file,
85 line = widths.line,
86 rule = widths.rule
87 )
88 }
89
90 fn row(offence: &Offence, widths: &ColumnWidths) -> String {
91 format!(
92 "{:<file$} {:>line$} {:<rule$} {}\n",
93 offence.file,
94 offence.line,
95 offence.rule,
96 offence.description,
97 file = widths.file,
98 line = widths.line,
99 rule = widths.rule
100 )
101 }
102
103 fn correction_row(offence: &Offence, widths: &ColumnWidths) -> String {
108 let indent = widths.file + widths.line + widths.rule + 6;
109 format!("{}fix: {}\n", " ".repeat(indent), offence.correction)
110 }
111
112 fn summary(&self, offences: &[Offence]) -> String {
113 let broken: BTreeSet<&str> = offences.iter().map(|offence| offence.rule).collect();
114 format!(
115 "summary: files_scanned={} offences={} rules_broken={}",
116 self.files_scanned,
117 offences.len(),
118 broken.len()
119 )
120 }
121}
122
123struct ColumnWidths {
124 file: usize,
125 line: usize,
126 rule: usize,
127 description: usize,
128}
129
130impl ColumnWidths {
131 fn of(offences: &[Offence]) -> Self {
132 Self {
133 file: Self::widest(offences.iter().map(|offence| offence.file.len()), "file"),
134 line: Self::widest(
135 offences
136 .iter()
137 .map(|offence| offence.line.to_string().len()),
138 "line",
139 ),
140 rule: Self::widest(offences.iter().map(|offence| offence.rule.len()), "rule"),
141 description: Self::widest(
142 offences.iter().map(|offence| offence.description.len()),
143 "offence",
144 ),
145 }
146 }
147
148 fn widest<I: Iterator<Item = usize>>(lengths: I, heading: &str) -> usize {
149 lengths.max().unwrap_or(0).max(heading.len())
150 }
151}