mod categories;
mod format;
mod views;
use format::{
format_architecture, format_complexity, format_coupling, format_dry, format_iosp, format_srp,
format_tq,
};
use views::{
ListArchRow, ListComplexityRow, ListCouplingRow, ListDryRow, ListIospRow, ListSrpRow, ListTqRow,
};
use crate::domain::analysis_data::{FunctionRecord, ModuleCouplingRecord};
use crate::domain::findings::{
ArchitectureFinding, ComplexityFinding, CouplingFinding, DryFinding, IospFinding,
OrphanSuppression, SrpFinding, TqFinding,
};
use crate::domain::AnalysisData;
use crate::ports::reporter::{ReporterImpl, Snapshot};
use crate::ports::Reporter;
use crate::report::AnalysisResult;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FindingEntry {
pub file: String,
pub line: usize,
pub category: &'static str,
pub detail: String,
pub function_name: String,
}
impl FindingEntry {
pub(crate) fn new(
file: &str,
line: usize,
category: &'static str,
detail: String,
function_name: String,
) -> Self {
Self {
file: file.to_string(),
line,
category,
detail,
function_name,
}
}
}
pub struct FindingsListReporter<'a> {
pub(crate) data: &'a AnalysisData,
}
impl<'a> FindingsListReporter<'a> {
fn function_name_at(&self, file: &str, line: usize) -> String {
self.data
.functions
.iter()
.find(|fr: &&FunctionRecord| fr.file == file && fr.line == line)
.map(|fr| fr.qualified_name.clone())
.unwrap_or_default()
}
}
impl<'a> ReporterImpl for FindingsListReporter<'a> {
type Output = Vec<FindingEntry>;
type IospView = Vec<ListIospRow>;
type ComplexityView = Vec<ListComplexityRow>;
type DryView = Vec<ListDryRow>;
type SrpView = Vec<ListSrpRow>;
type CouplingView = Vec<ListCouplingRow>;
type TestQualityView = Vec<ListTqRow>;
type ArchitectureView = Vec<ListArchRow>;
type OrphanView = Vec<FindingEntry>;
type IospDataView = ();
type ComplexityDataView = ();
type CouplingDataView = ();
fn build_iosp(&self, findings: &[IospFinding]) -> Vec<ListIospRow> {
findings
.iter()
.filter(|f| !f.common.suppressed)
.map(|f| ListIospRow {
file: f.common.file.clone(),
line: f.common.line,
function_name: self.function_name_at(&f.common.file, f.common.line),
})
.collect()
}
fn build_complexity(&self, findings: &[ComplexityFinding]) -> Vec<ListComplexityRow> {
findings
.iter()
.filter(|f| !f.common.suppressed)
.map(|f| ListComplexityRow {
file: f.common.file.clone(),
line: f.common.line,
function_name: self.function_name_at(&f.common.file, f.common.line),
finding: f.clone(),
})
.collect()
}
fn build_dry(&self, findings: &[DryFinding]) -> Vec<ListDryRow> {
findings
.iter()
.filter(|f| !f.common.suppressed)
.map(|f| ListDryRow {
file: f.common.file.clone(),
line: f.common.line,
function_name: self.function_name_at(&f.common.file, f.common.line),
finding: f.clone(),
})
.collect()
}
fn build_srp(&self, findings: &[SrpFinding]) -> Vec<ListSrpRow> {
findings
.iter()
.filter(|f| !f.common.suppressed)
.map(|f| ListSrpRow {
file: f.common.file.clone(),
line: f.common.line,
function_name: self.function_name_at(&f.common.file, f.common.line),
finding: f.clone(),
})
.collect()
}
fn build_coupling(&self, findings: &[CouplingFinding]) -> Vec<ListCouplingRow> {
findings
.iter()
.filter(|f| !f.common.suppressed)
.map(|f| ListCouplingRow {
file: f.common.file.clone(),
line: f.common.line,
function_name: self.function_name_at(&f.common.file, f.common.line),
finding: f.clone(),
})
.collect()
}
fn build_test_quality(&self, findings: &[TqFinding]) -> Vec<ListTqRow> {
findings
.iter()
.filter(|f| !f.common.suppressed)
.map(|f| ListTqRow {
file: f.common.file.clone(),
line: f.common.line,
function_name: f.function_name.clone(),
kind: f.kind,
})
.collect()
}
fn build_architecture(&self, findings: &[ArchitectureFinding]) -> Vec<ListArchRow> {
findings
.iter()
.filter(|f| !f.common.suppressed)
.map(|f| ListArchRow {
file: f.common.file.clone(),
line: f.common.line,
message: f.common.message.clone(),
})
.collect()
}
fn build_orphans(&self, suppressions: &[OrphanSuppression]) -> Vec<FindingEntry> {
suppressions.iter().map(orphan_to_finding_entry).collect()
}
fn build_iosp_data(&self, _: &[FunctionRecord]) {}
fn build_complexity_data(&self, _: &[FunctionRecord]) {}
fn build_coupling_data(&self, _: &[ModuleCouplingRecord]) {}
fn publish(&self, snapshot: Snapshot<Self>) -> Vec<FindingEntry> {
let Snapshot {
iosp,
complexity,
dry,
srp,
coupling,
test_quality,
architecture,
orphans,
iosp_data: (),
complexity_data: (),
coupling_data: (),
} = snapshot;
let cap = iosp.len()
+ complexity.len()
+ dry.len()
+ srp.len()
+ coupling.len()
+ test_quality.len()
+ architecture.len()
+ orphans.len();
let mut entries: Vec<FindingEntry> = Vec::with_capacity(cap);
entries.extend(iosp.into_iter().map(format_iosp));
entries.extend(complexity.into_iter().map(format_complexity));
entries.extend(dry.into_iter().map(format_dry));
entries.extend(srp.into_iter().map(format_srp));
entries.extend(coupling.into_iter().map(format_coupling));
entries.extend(test_quality.into_iter().map(format_tq));
entries.extend(architecture.into_iter().map(format_architecture));
entries.extend(orphans);
entries.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
entries
}
}
pub fn collect_all_findings(analysis: &AnalysisResult) -> Vec<FindingEntry> {
let reporter = FindingsListReporter {
data: &analysis.data,
};
reporter.render(&analysis.findings, &analysis.data)
}
pub(crate) fn orphan_to_finding_entry(w: &OrphanSuppression) -> FindingEntry {
let dims: Vec<String> = w.dimensions.iter().map(|d| d.to_string()).collect();
let scope = if dims.is_empty() {
"<all>".to_string()
} else {
dims.join(",")
};
let detail = match &w.reason {
Some(r) => format!("stale qual:allow({scope}) — {r}"),
None => format!("stale qual:allow({scope})"),
};
FindingEntry::new(&w.file, w.line, "ORPHAN_SUPPRESSION", detail, String::new())
}
pub(crate) fn format_findings(entries: &[FindingEntry]) -> String {
if entries.is_empty() {
return String::new();
}
let n = entries.len();
let heading = format!("═══ {} Finding{} ═══", n, if n == 1 { "" } else { "s" });
let mut out = String::new();
out.push('\n');
out.push_str(&colored::Colorize::bold(heading.as_str()).to_string());
out.push('\n');
entries.iter().for_each(|e| {
let detail = if e.function_name.is_empty() {
e.detail.clone()
} else if e.detail.is_empty() {
format!("in {}", e.function_name)
} else {
format!("{} in {}", e.detail, e.function_name)
};
if e.file.is_empty() {
out.push_str(&format!(" {} {}\n", e.category, detail));
} else {
out.push_str(&format!(
" {}:{} {} {}\n",
e.file, e.line, e.category, detail
));
}
});
out
}
pub fn print_findings(entries: &[FindingEntry]) {
let s = format_findings(entries);
if !s.is_empty() {
print!("{s}");
}
}