use super::shared::{bounded_findings, count_label, terminal_safe, MAX_REPORTED_CLUSTER_MEMBERS};
use super::ReportContext;
use anyhow::Result;
use std::collections::BTreeMap;
use std::io::Write;
pub fn write_terminal(context: &ReportContext<'_>, writer: &mut dyn Write) -> Result<()> {
write_terminal_with_color(context, writer, false)
}
pub(super) fn write_terminal_with_color(
context: &ReportContext<'_>,
writer: &mut dyn Write,
color: bool,
) -> Result<()> {
let result = context.result;
let root = context.root;
let summary = &context.summary;
writeln!(writer, "CukeDedup v{}", context.tool_version)?;
writeln!(writer)?;
let mut wrote_rule = false;
let mut findings_by_rule: BTreeMap<_, Vec<_>> = BTreeMap::new();
let selected = bounded_findings(result, true);
let findings_truncated = selected.truncated;
for finding in selected.findings {
findings_by_rule
.entry(finding.rule)
.or_default()
.push(finding);
}
for (rule, findings) in findings_by_rule {
if wrote_rule {
writeln!(writer)?;
}
writeln!(writer, "{}", colorize(rule.to_string(), "1;36", color))?;
writeln!(writer)?;
wrote_rule = true;
for (index, finding) in findings.into_iter().enumerate() {
if index > 0 {
writeln!(writer)?;
}
let severity_color = if finding.severity == crate::model::Severity::Error {
"31"
} else {
"33"
};
let severity = colorize(format!("[{}]", finding.severity), severity_color, color);
writeln!(writer, " {severity} {}", terminal_safe(&finding.message))?;
writeln!(
writer,
" --> {}",
colorize(terminal_safe(&finding.primary.display(root)), "94", color)
)?;
let retained_related = MAX_REPORTED_CLUSTER_MEMBERS.saturating_sub(1);
for related in finding.related.iter().take(retained_related) {
writeln!(
writer,
" related: {}",
colorize(terminal_safe(&related.display(root)), "94", color)
)?;
}
if finding.related.len() > retained_related {
writeln!(
writer,
" related: ... {} more locations omitted",
finding.related.len() - retained_related
)?;
}
if !finding.evidence.matcher_difference.is_empty() {
writeln!(
writer,
" evidence: {}",
terminal_safe(&finding.evidence.matcher_difference)
)?;
}
if !finding.evidence.handler_evidence.is_empty() {
writeln!(
writer,
" handler: {}",
terminal_safe(&finding.evidence.handler_evidence)
)?;
}
writeln!(
writer,
" action: {}",
terminal_safe(&finding.suggested_action)
)?;
}
}
if summary.findings > 0 {
writeln!(writer)?;
}
if findings_truncated > 0 {
writeln!(
writer,
"Showing the highest-severity findings; {} additional active findings omitted",
findings_truncated
)?;
}
writeln!(
writer,
"Analyzed {} and {}: {}, {}, {} suppressed",
count_label(summary.definitions_analyzed, "definition"),
count_label(summary.feature_steps_analyzed, "feature step"),
count_label(summary.errors, "error"),
count_label(summary.warnings, "warning"),
summary.suppressed
)?;
writeln!(
writer,
"Duplication: {} of {} definitions ({:.2}%), threshold {:.2}% — {}",
summary.duplication.duplicated_definitions,
summary.duplication.total_definitions,
summary.duplication.percentage,
summary.duplication.threshold,
if summary.duplication.passed {
"PASS"
} else {
"FAIL"
}
)?;
if !summary.duplication.rules.is_empty() {
writeln!(
writer,
"Contributing rules: {}",
summary
.duplication
.rules
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)?;
}
writeln!(writer)?;
writeln!(
writer,
"{}",
colorize(
format!("Found {}.", count_label(summary.findings, "finding")),
"90",
color
)
)?;
if let Some(metrics) = context.metrics {
writeln!(
writer,
"{}",
colorize(
format!(
"Detection time: {:.1} ms",
metrics.discovery_ms + metrics.parsing_ms + metrics.analysis_ms
),
"90",
color
)
)?;
}
Ok(())
}
fn colorize(text: String, code: &str, enabled: bool) -> String {
if enabled {
format!("\u{1b}[{code}m{text}\u{1b}[0m")
} else {
text
}
}