use std::path::Path;
use fallow_config::{AuditGate, RulesConfig, Severity};
use super::{AuditAnalysesView, AuditKeySnapshot};
use crate::audit_keys::{
AuditComparison, AuditDomainLedger, DeadCodeAuditLedger, dead_code_audit_ledger,
dupe_group_key, health_finding_key, preexisting_dupe_group_keys, styling_finding_key,
};
use crate::{AuditAttribution, AuditSummary, AuditVerdict};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DupeDemotionDiffSource {
Shared(String),
Worktree,
Skipped,
}
impl DupeDemotionDiffSource {
#[must_use]
pub fn label(&self, base_ref: &str) -> String {
match self {
Self::Shared(label) => label.clone(),
Self::Worktree => format!("merge-base worktree diff vs {base_ref}"),
Self::Skipped => "skipped: no diff available".to_string(),
}
}
}
#[derive(Clone, Copy)]
pub struct SharedDiff<'a> {
pub index: &'a fallow_output::DiffIndex,
pub label: &'a str,
}
#[must_use]
pub fn compare(
view: &AuditAnalysesView<'_>,
base: Option<&AuditKeySnapshot>,
syntactic_fallback: bool,
) -> AuditComparison {
let dead_code =
view.dead_code
.as_ref()
.map_or_else(DeadCodeAuditLedger::default, |dead_code| {
let base_keys = base.map(|snapshot| {
if syntactic_fallback {
snapshot
.syntactic_dead_code
.as_ref()
.unwrap_or(&snapshot.dead_code)
} else {
&snapshot.dead_code
}
});
let mut ledger = dead_code_audit_ledger(
dead_code.results,
dead_code.root,
dead_code.config,
base_keys,
);
if syntactic_fallback && let Some(head_syntactic) = dead_code.syntactic_keys {
ledger.demote_unattributable_introductions(head_syntactic);
}
ledger
});
let health = AuditDomainLedger::compare(
view.health.iter().flat_map(|health| {
health
.report
.findings
.iter()
.map(|finding| health_finding_key(finding, health.root))
}),
base.map(|snapshot| &snapshot.health),
);
let dupes = AuditDomainLedger::compare(
view.duplication.iter().flat_map(|duplication| {
duplication
.clone_groups
.iter()
.map(|group| dupe_group_key(group, duplication.root))
}),
base.map(|snapshot| &snapshot.dupes),
);
let styling = AuditDomainLedger::compare(
view.health.iter().flat_map(|health| {
health
.report
.styling_findings
.iter()
.map(|finding| styling_finding_key(finding, health.root))
}),
base.map(|snapshot| &snapshot.styling),
);
AuditComparison {
dead_code,
health,
dupes,
styling,
}
}
pub fn demote_preexisting_dupe_introductions(
comparison: &mut AuditComparison,
view: &AuditAnalysesView<'_>,
root: &Path,
base_ref: &str,
shared: Option<SharedDiff<'_>>,
) -> Option<DupeDemotionDiffSource> {
if comparison.dupes.introduced_count() == 0 {
return None;
}
let duplication = view.duplication.as_ref()?;
let worktree_index;
let (index, source) = if let Some(shared) = shared {
(
shared.index,
DupeDemotionDiffSource::Shared(shared.label.to_owned()),
)
} else if let Ok(diff) = fallow_engine::changed_files::try_get_changed_diff(root, base_ref) {
worktree_index = fallow_output::DiffIndex::from_unified_diff(&diff);
(&worktree_index, DupeDemotionDiffSource::Worktree)
} else {
return Some(DupeDemotionDiffSource::Skipped);
};
let demote = preexisting_dupe_group_keys(
duplication.clone_groups.iter().copied(),
duplication.root,
index,
);
comparison.dupes.demote_introductions(&demote);
Some(source)
}
#[must_use]
pub fn styling_rule_severity(rules: &RulesConfig, code: &str) -> Severity {
match code {
"css-token-drift" => rules.css_token_drift,
"css-duplicate-block" => rules.css_duplicate_block,
"css-selector-complexity" => rules.css_selector_complexity,
"css-dead-surface" => rules.css_dead_surface,
"css-broken-reference" => rules.css_broken_reference,
_ => Severity::Warn,
}
}
#[must_use]
pub fn styling_finding_gates(rules: &RulesConfig, code: &str) -> bool {
styling_rule_severity(rules, code) == Severity::Error
}
#[must_use]
pub fn outcome(
gate: AuditGate,
view: &AuditAnalysesView<'_>,
comparison: &AuditComparison,
has_base: bool,
) -> (AuditAttribution, AuditVerdict, AuditSummary) {
let summary = summary(view, comparison);
(
attribution(gate, comparison, has_base),
verdict(gate, view, comparison, &summary),
summary,
)
}
fn verdict(
gate: AuditGate,
view: &AuditAnalysesView<'_>,
comparison: &AuditComparison,
summary: &AuditSummary,
) -> AuditVerdict {
let new_only = matches!(gate, AuditGate::NewOnly);
let dead_code_errors = if new_only {
comparison.dead_code.has_introduced_errors()
} else {
dead_code_has_errors(view)
};
let dead_code_warnings = if new_only {
comparison.dead_code.has_introduced_warnings()
} else {
comparison
.dead_code
.records()
.iter()
.any(|record| record.effective_severity == Severity::Warn)
};
let (complexity_errors, complexity_warnings) =
view.health.as_ref().map_or((false, false), |health| {
health
.report
.findings
.iter()
.zip(comparison.health.introduced())
.filter(|(_, introduced)| !new_only || *introduced)
.fold((false, false), |(errors, warnings), (finding, _)| {
if finding.blocks() {
(true, warnings)
} else {
(errors, true)
}
})
});
let styling_errors = view.health.as_ref().is_some_and(|health| {
health
.report
.styling_findings
.iter()
.zip(comparison.styling.introduced())
.any(|(finding, introduced)| {
(!new_only || introduced) && styling_finding_gates(health.rules, &finding.code)
})
});
let duplication_findings = if new_only {
comparison.dupes.introduced_count()
} else {
summary.duplication_clone_groups
};
let duplication_errors = view.duplication.as_ref().is_some_and(|duplication| {
duplication_findings > 0
&& duplication.threshold > 0.0
&& duplication.duplication_percentage > duplication.threshold
});
if dead_code_errors || complexity_errors || styling_errors || duplication_errors {
AuditVerdict::Fail
} else if dead_code_warnings || complexity_warnings || duplication_findings > 0 {
AuditVerdict::Warn
} else {
AuditVerdict::Pass
}
}
fn dead_code_has_errors(view: &AuditAnalysesView<'_>) -> bool {
view.dead_code.as_ref().is_some_and(|dead_code| {
fallow_engine::error_severity::has_error_severity_issues(
dead_code.results,
&dead_code.config.rules,
Some(dead_code.config),
false,
)
})
}
fn attribution(gate: AuditGate, comparison: &AuditComparison, has_base: bool) -> AuditAttribution {
if !has_base {
return AuditAttribution {
gate,
..AuditAttribution::default()
};
}
AuditAttribution {
gate,
dead_code_introduced: comparison.dead_code.introduced_count(),
dead_code_inherited: comparison.dead_code.inherited_count(),
complexity_introduced: comparison.health.introduced_count(),
complexity_inherited: comparison.health.inherited_count(),
duplication_introduced: comparison.dupes.introduced_count(),
duplication_inherited: comparison.dupes.inherited_count(),
}
}
fn summary(view: &AuditAnalysesView<'_>, comparison: &AuditComparison) -> AuditSummary {
AuditSummary {
dead_code_issues: comparison.dead_code.visible_count(),
dead_code_has_errors: dead_code_has_errors(view),
complexity_findings: view
.health
.as_ref()
.map_or(0, |health| health.report.findings.len()),
max_cyclomatic: view.health.as_ref().and_then(|health| {
health
.report
.findings
.iter()
.map(|finding| finding.cyclomatic)
.max()
}),
duplication_clone_groups: view
.duplication
.as_ref()
.map_or(0, |duplication| duplication.clone_groups.len()),
}
}