use fallow_config::{AuditGate, AuditWorsened, OutputFormat};
use crate::report::sink::outln;
use super::AuditResult;
pub(super) fn print_audit_worsened_section(
result: &AuditResult,
health: &crate::health::HealthResult,
show_headers: bool,
) {
let lines = audit_worsened_lines(result, health);
if lines.is_empty() {
return;
}
match result.output {
OutputFormat::Markdown => {
outln!();
outln!("### Worse than base\n");
outln!("| File | Function | Attribution | Grown metrics |");
outln!("|:-----|:---------|:------------|:--------------|");
}
OutputFormat::Compact => {}
_ => super::print_audit_section_header(
show_headers,
"── Worse than base ────────────────────────────────",
),
}
for line in lines {
outln!("{line}");
}
}
struct WorsenedRow<'a> {
path: String,
line: u32,
name: &'a str,
worsened: bool,
metrics: &'a [fallow_output::WorsenedMetric],
}
fn audit_worsened_lines(result: &AuditResult, health: &crate::health::HealthResult) -> Vec<String> {
let root = &health.config.root;
health
.report
.findings
.iter()
.filter(|finding| !finding.worsened_metrics.is_empty())
.map(|finding| {
let row = WorsenedRow {
path: finding
.path
.strip_prefix(root)
.unwrap_or(&finding.path)
.display()
.to_string()
.replace('\\', "/"),
line: finding.line,
name: &finding.name,
worsened: finding.attribution
== Some(fallow_output::AuditFindingAttribution::Worsened),
metrics: &finding.worsened_metrics,
};
render_worsened_row(&row, result.output, result.attribution.worsened)
})
.collect()
}
const fn worsened_metric_token(metric: fallow_output::WorsenedMetricKind) -> &'static str {
match metric {
fallow_output::WorsenedMetricKind::Cyclomatic => "cyclomatic",
fallow_output::WorsenedMetricKind::Cognitive => "cognitive",
fallow_output::WorsenedMetricKind::Crap => "crap",
fallow_output::WorsenedMetricKind::LineCount => "line_count",
}
}
fn render_worsened_row(row: &WorsenedRow<'_>, output: OutputFormat, mode: AuditWorsened) -> String {
let attribution = if row.worsened {
"worsened"
} else {
"introduced"
};
match output {
OutputFormat::Compact => {
let metrics = row
.metrics
.iter()
.map(|metric| {
format!(
"{}={}->{}",
worsened_metric_token(metric.metric),
fallow_output::format_metric_value(metric.base),
fallow_output::format_metric_value(metric.head),
)
})
.collect::<Vec<_>>()
.join(",");
format!(
"worse-than-base:{}:{}:{}:attribution={attribution},{metrics}",
row.path, row.line, row.name
)
}
OutputFormat::Markdown => format!(
"| {} | {} | {attribution} | {} |",
fallow_output::markdown_table_code_span(&format!("{}:{}", row.path, row.line)),
fallow_output::markdown_table_code_span(row.name),
fallow_output::markdown_table_text(&fallow_output::describe_worsened_metrics(
row.metrics
))
),
_ => {
let note = if row.worsened {
worsened_class_label(mode)
} else {
"introduced: a new limit or a higher tier"
};
format!(
" {}:{} {} {} ({note})",
row.path,
row.line,
row.name,
fallow_output::describe_worsened_metrics(row.metrics)
)
}
}
}
pub(super) const fn worsened_class_label(mode: AuditWorsened) -> &'static str {
match mode {
AuditWorsened::Warn => "worsened: warn only, --worsened error gates it",
AuditWorsened::Error => "worsened: gated by --worsened error",
AuditWorsened::Off => "worsened: off",
}
}
pub(super) fn print_audit_test_file_section(
result: &AuditResult,
health: &crate::health::HealthResult,
show_headers: bool,
) {
let root = &health.config.root;
let new_only = matches!(result.attribution.gate, AuditGate::NewOnly);
let lines: Vec<String> = health
.report
.findings
.iter()
.filter(|finding| {
finding.gate_exemption.is_some()
&& (!new_only
|| finding.introduced == Some(true)
|| finding.attribution
== Some(fallow_output::AuditFindingAttribution::Worsened))
})
.filter_map(|finding| {
let exemption = finding.gate_exemption?;
let path = finding
.path
.strip_prefix(root)
.unwrap_or(&finding.path)
.display()
.to_string()
.replace('\\', "/");
Some(render_test_file_row(
&path,
finding.line,
&finding.name,
exemption,
result.output,
))
})
.collect();
if lines.is_empty() {
return;
}
match result.output {
OutputFormat::Markdown => {
outln!();
outln!("### Test files, not gated\n");
outln!("| File | Function | Gate |");
outln!("|:-----|:---------|:-----|");
}
OutputFormat::Compact => {}
_ => super::print_audit_section_header(
show_headers,
"── Test files, not gated ──────────────────────────",
),
}
for line in lines {
outln!("{line}");
}
}
fn render_test_file_row(
path: &str,
line: u32,
name: &str,
exemption: fallow_output::AuditGateExemption,
output: OutputFormat,
) -> String {
match output {
OutputFormat::Compact => {
format!("gate-exemption:{path}:{line}:{name}:{}", exemption.token())
}
OutputFormat::Markdown => format!(
"| {} | {} | warn only: test file |",
fallow_output::markdown_table_code_span(&format!("{path}:{line}")),
fallow_output::markdown_table_code_span(name),
),
_ => format!(" {path}:{line} {name} (test file: warn only, audit does not gate it)"),
}
}