use std::collections::BTreeSet;
use crate::offence::Offence;
use crate::offence_threshold::OffenceThreshold;
pub struct ReportPrinter {
files_scanned: usize,
threshold: OffenceThreshold,
applied: Vec<String>,
skipped: Vec<String>,
unconfigured: Vec<String>,
}
impl ReportPrinter {
pub fn new(files_scanned: usize) -> Self {
Self {
files_scanned,
threshold: OffenceThreshold::default(),
applied: Vec::new(),
skipped: Vec::new(),
unconfigured: Vec::new(),
}
}
pub fn with_rules(
self,
applied: Vec<String>,
skipped: Vec<String>,
unconfigured: Vec<String>,
) -> Self {
Self {
applied,
skipped,
unconfigured,
..self
}
}
pub fn with_threshold(self, threshold: OffenceThreshold) -> Self {
Self { threshold, ..self }
}
pub fn print(&self, offences: &[Offence]) {
println!("{}", self.render(offences));
}
pub fn render(&self, offences: &[Offence]) -> String {
let mut report = String::from("stern4rust report\n\n");
if offences.is_empty() {
report.push_str(self.clean_verdict());
report.push_str("\n\n");
report.push_str(&self.roster());
report.push_str(&self.summary(offences));
return report;
}
let shown = self.threshold.kept(offences);
let widths = ColumnWidths::of(shown);
report.push_str(&Self::heading(&widths));
for offence in shown {
report.push_str(&Self::row(offence, &widths));
report.push_str(&Self::correction_row(offence, &widths));
}
report.push('\n');
report.push_str(&self.omission(offences));
report.push_str(&self.roster());
report.push_str(&self.summary(offences));
report
}
fn clean_verdict(&self) -> &'static str {
if self.everything_ran() {
"All rules are satisfied."
} else {
"All applied rules are satisfied."
}
}
fn everything_ran(&self) -> bool {
self.skipped.is_empty() && self.unconfigured.is_empty()
}
fn roster(&self) -> String {
if self.applied.is_empty() {
return String::new();
}
let mut roster = format!(" applied: {}\n", self.applied.join(", "));
if !self.everything_ran() {
roster.push_str(&format!(" not applied: {}\n", self.absences().join(", ")));
}
roster.push('\n');
roster
}
fn absences(&self) -> Vec<String> {
self.skipped
.iter()
.map(|name| format!("{name} (skipped)"))
.chain(
self.unconfigured
.iter()
.map(|name| format!("{name} (needs --header-file)")),
)
.collect()
}
fn omission(&self, offences: &[Offence]) -> String {
let omitted = self.threshold.omitted(offences);
if omitted == 0 {
return String::new();
}
format!(
"... and {omitted} more offences not shown. Raise --offence-threshold \
(currently {}, use 0 for all) to see them.\n\n",
self.threshold.limit()
)
}
fn heading(widths: &ColumnWidths) -> String {
format!(
"{:<file$} {:>line$} {:<rule$} offence\n{} {} {} {}\n",
"file",
"line",
"rule",
"-".repeat(widths.file),
"-".repeat(widths.line),
"-".repeat(widths.rule),
"-".repeat(widths.description),
file = widths.file,
line = widths.line,
rule = widths.rule
)
}
fn row(offence: &Offence, widths: &ColumnWidths) -> String {
format!(
"{:<file$} {:>line$} {:<rule$} {}\n",
offence.file,
offence.line,
offence.rule,
offence.description,
file = widths.file,
line = widths.line,
rule = widths.rule
)
}
fn correction_row(offence: &Offence, widths: &ColumnWidths) -> String {
let indent = widths.file + widths.line + widths.rule + 6;
format!("{}fix: {}\n", " ".repeat(indent), offence.correction)
}
fn summary(&self, offences: &[Offence]) -> String {
let broken: BTreeSet<&str> = offences.iter().map(|offence| offence.rule).collect();
format!(
"summary: files_scanned={} offences={} rules_broken={} rules_applied={} \
rules_skipped={} rules_unconfigured={}",
self.files_scanned,
offences.len(),
broken.len(),
self.applied.len(),
self.skipped.len(),
self.unconfigured.len()
)
}
}
struct ColumnWidths {
file: usize,
line: usize,
rule: usize,
description: usize,
}
impl ColumnWidths {
fn of(offences: &[Offence]) -> Self {
Self {
file: Self::widest(offences.iter().map(|offence| offence.file.len()), "file"),
line: Self::widest(
offences
.iter()
.map(|offence| offence.line.to_string().len()),
"line",
),
rule: Self::widest(offences.iter().map(|offence| offence.rule.len()), "rule"),
description: Self::widest(
offences.iter().map(|offence| offence.description.len()),
"offence",
),
}
}
fn widest<I: Iterator<Item = usize>>(lengths: I, heading: &str) -> usize {
lengths.max().unwrap_or(0).max(heading.len())
}
}