use std::io::{self, Write};
use crate::domain::types::FunctionVerdict;
use crate::domain::view::AnalysisView;
pub fn render_summary(view: &AnalysisView<'_>, sink: &mut impl Write) -> io::Result<()> {
for verdict in &view.shown {
if !verdict.exceeds {
continue;
}
writeln!(sink, "{}", format_line(verdict))?;
}
Ok(())
}
fn format_line(verdict: &FunctionVerdict) -> String {
let identity = &verdict.scored.identity;
let span = &identity.span;
let actions = format_actions(verdict);
format!(
"[crap={crap:.2}] {file}:{start}-{end} {name} [actions: {actions}]",
crap = verdict.scored.crap.value,
file = identity.file_path,
start = span.start_line,
end = span.end_line,
name = identity.qualified_name,
actions = actions,
)
}
fn format_actions(verdict: &FunctionVerdict) -> String {
let Some(diag) = verdict.diagnostic.as_deref() else {
return "none".to_string();
};
if diag.suggested_actions.is_empty() {
return "none".to_string();
}
diag.suggested_actions
.iter()
.map(action_kind_label)
.collect::<Vec<_>>()
.join(",")
}
fn action_kind_label(action: &crate::domain::diagnostic::SuggestedAction) -> &'static str {
use crate::domain::diagnostic::SuggestedAction::*;
match action {
AddTestsForLines { .. } => "add_tests_for_lines",
ExtractFunction { .. } => "extract_function",
SimplifyBranching { .. } => "simplify_branching",
AcceptInherentComplexity { .. } => "accept_inherent_complexity",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::diagnostic::{
Applicability, Diagnostic, LineRange, ProposedSplit, RootCause, SplitKind, SuggestedAction,
};
use crate::domain::types::{
AnalysisResult, AnalysisSummary, ComplexityMetric, CrapScore, FunctionIdentity, RiskLevel,
ScoredFunction, SourceSpan,
};
use crate::domain::view::{self, ViewSpec};
fn make_result(verdicts: Vec<FunctionVerdict>) -> AnalysisResult {
AnalysisResult {
passed: verdicts.iter().all(|v| !v.exceeds),
functions: verdicts,
summary: AnalysisSummary::default(),
}
}
fn make_verdict(name: &str, file: &str, line_start: usize, line_end: usize) -> FunctionVerdict {
FunctionVerdict {
scored: ScoredFunction {
identity: FunctionIdentity {
file_path: file.to_string(),
qualified_name: name.to_string(),
span: SourceSpan {
start_line: line_start,
end_line: line_end,
start_column: 0,
end_column: 0,
},
},
complexity: 5,
complexity_metric: ComplexityMetric::Cognitive,
coverage_percent: 50.0,
crap: CrapScore {
value: 12.34,
risk_level: RiskLevel::Moderate,
},
contributors: vec![],
},
threshold: 5.0,
exceeds: true,
diagnostic: None,
}
}
#[test]
fn render_summary_emits_one_line_per_exceeding_verdict() {
let mut v = make_verdict("foo", "src/lib.rs", 10, 20);
v.diagnostic = Some(Box::new(Diagnostic {
coverage_gaps: vec![LineRange::new(11, 13)],
complexity_drivers: vec![],
suggested_actions: vec![
SuggestedAction::AddTestsForLines {
lines: vec![LineRange::new(11, 13)],
applicability: Applicability::default(),
},
SuggestedAction::ExtractFunction {
candidates: vec![ProposedSplit {
line_range: LineRange::new(11, 19),
complexity_contribution: 4,
branch_path: "if-branch".to_string(),
kind: SplitKind::DeepestNesting,
recommended: true,
}],
applicability: Applicability::default(),
},
],
root_cause: RootCause::Both,
}));
let result = make_result(vec![v]);
let view = view::apply(&result, ViewSpec::default());
let mut sink = Vec::new();
render_summary(&view, &mut sink).unwrap();
let out = String::from_utf8(sink).unwrap();
assert_eq!(
out,
"[crap=12.34] src/lib.rs:10-20 foo [actions: add_tests_for_lines,extract_function]\n"
);
}
#[test]
fn render_summary_skips_passing_verdicts() {
let mut v = make_verdict("under", "src/lib.rs", 1, 5);
v.exceeds = false;
let result = make_result(vec![v]);
let view = view::apply(&result, ViewSpec::default());
let mut sink = Vec::new();
render_summary(&view, &mut sink).unwrap();
assert!(sink.is_empty());
}
#[test]
fn render_summary_emits_none_when_diagnostic_absent() {
let v = make_verdict("naked", "src/lib.rs", 1, 9);
let result = make_result(vec![v]);
let view = view::apply(&result, ViewSpec::default());
let mut sink = Vec::new();
render_summary(&view, &mut sink).unwrap();
let out = String::from_utf8(sink).unwrap();
assert_eq!(out, "[crap=12.34] src/lib.rs:1-9 naked [actions: none]\n");
}
#[test]
fn render_summary_preserves_view_shown_order() {
let mut a = make_verdict("a_first", "src/lib.rs", 1, 5);
a.scored.crap.value = 10.0;
let mut b = make_verdict("b_second", "src/lib.rs", 6, 10);
b.scored.crap.value = 30.0;
let result = make_result(vec![a, b]);
let view = view::apply(&result, ViewSpec::default());
let mut sink = Vec::new();
render_summary(&view, &mut sink).unwrap();
let out = String::from_utf8(sink).unwrap();
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 2);
assert!(lines[0].contains("b_second"));
assert!(lines[1].contains("a_first"));
}
}