use std::path::Path;
use fallow_output::{
AuditFindingAttribution, AuditGateExemption, CoverageSource, DEFAULT_COGNITIVE_CRITICAL,
DEFAULT_COGNITIVE_HIGH, DEFAULT_CYCLOMATIC_CRITICAL, DEFAULT_CYCLOMATIC_HIGH,
ExceededThreshold, FindingSeverity, WorsenedMetric, WorsenedMetricKind,
compute_finding_severity,
};
use fallow_types::output_dead_code::EffectiveSeverity;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::audit_keys::{health_finding_key, remap_key_for_renames};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ComplexityMetrics {
pub line: u32,
pub cyclomatic: u16,
pub cognitive: u16,
pub crap: Option<f64>,
pub line_count: u32,
pub exceeded: ExceededThreshold,
pub severity: FindingSeverity,
pub crap_estimated: bool,
}
impl ComplexityMetrics {
#[must_use]
pub const fn of(finding: &fallow_output::ComplexityViolation) -> Self {
Self {
line: finding.line,
cyclomatic: finding.cyclomatic,
cognitive: finding.cognitive,
crap: finding.crap,
line_count: finding.line_count,
exceeded: finding.exceeded,
severity: finding.severity,
crap_estimated: crap_is_estimated(finding),
}
}
fn same_values(&self, other: &Self) -> bool {
self.cyclomatic == other.cyclomatic
&& self.cognitive == other.cognitive
&& self.crap.map(f64::to_bits) == other.crap.map(f64::to_bits)
}
fn same_values_and_lines(&self, other: &Self) -> bool {
self.same_values(other) && self.line_count == other.line_count
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ComplexityClass {
pub introduced: bool,
pub worsened_metrics: Vec<WorsenedMetric>,
pub exceeded_metric_grew: bool,
}
impl ComplexityClass {
#[must_use]
pub fn is_worsened(&self) -> bool {
!self.introduced && !self.worsened_metrics.is_empty()
}
#[must_use]
pub fn attribution(&self) -> AuditFindingAttribution {
if self.introduced {
AuditFindingAttribution::Introduced
} else if self.worsened_metrics.is_empty() {
AuditFindingAttribution::Inherited
} else {
AuditFindingAttribution::Worsened
}
}
pub fn clear_worsened_for_off(&mut self) {
if !self.introduced && !self.exceeded_metric_grew {
self.worsened_metrics.clear();
}
}
fn of_match(
head: &fallow_output::ComplexityViolation,
base: Option<&ComplexityMetrics>,
) -> Self {
base.map_or(
Self {
introduced: true,
worsened_metrics: Vec::new(),
exceeded_metric_grew: false,
},
|base| {
let exceeded_metric_grew = exceeded_metric_increased(head, base);
Self {
introduced: newly_exceeded_metric_grew(head, base)
|| (exceeded_metric_grew && severity_tier_rose(head, base)),
worsened_metrics: grown_metrics(head, base),
exceeded_metric_grew,
}
},
)
}
}
fn grown_metrics(
head: &fallow_output::ComplexityViolation,
base: &ComplexityMetrics,
) -> Vec<WorsenedMetric> {
let mut grown = Vec::new();
let mut push = |metric, base: f64, head: f64| {
if head > base {
grown.push(WorsenedMetric { metric, base, head });
}
};
push(
WorsenedMetricKind::Cyclomatic,
f64::from(base.cyclomatic),
f64::from(head.cyclomatic),
);
push(
WorsenedMetricKind::Cognitive,
f64::from(base.cognitive),
f64::from(head.cognitive),
);
if let (Some(base_crap), Some(head_crap)) = (base.crap, head.crap) {
push(WorsenedMetricKind::Crap, base_crap, head_crap);
}
if line_count_grew(base.line_count, head.line_count) {
grown.push(WorsenedMetric {
metric: WorsenedMetricKind::LineCount,
base: f64::from(base.line_count),
head: f64::from(head.line_count),
});
}
grown
}
const LINE_COUNT_MIN_GROWTH_LINES: u32 = 5;
fn line_count_grew(base: u32, head: u32) -> bool {
head.saturating_sub(base) >= LINE_COUNT_MIN_GROWTH_LINES
}
pub type ComplexityBaseline = FxHashMap<String, Vec<ComplexityMetrics>>;
#[must_use]
pub fn complexity_baseline(
report: &fallow_output::HealthReport,
root: &Path,
) -> ComplexityBaseline {
let mut baseline = ComplexityBaseline::default();
for finding in &report.findings {
baseline
.entry(health_finding_key(finding, root))
.or_default()
.push(ComplexityMetrics::of(finding));
}
sort_by_line(&mut baseline);
baseline
}
#[must_use]
pub fn complexity_baseline_keys(baseline: &ComplexityBaseline) -> FxHashSet<String> {
baseline.keys().cloned().collect()
}
#[must_use]
#[expect(
clippy::implicit_hasher,
reason = "fallow standardizes on FxHashMap across audit attribution keys"
)]
pub fn remap_complexity_baseline_for_renames(
baseline: ComplexityBaseline,
renames: &FxHashMap<String, String>,
) -> ComplexityBaseline {
let mut remapped = ComplexityBaseline::default();
for (key, findings) in baseline {
remapped
.entry(remap_key_for_renames(&key, renames))
.or_default()
.extend(findings);
}
sort_by_line(&mut remapped);
remapped
}
fn sort_by_line(baseline: &mut ComplexityBaseline) {
for findings in baseline.values_mut() {
findings.sort_by_key(|metrics| metrics.line);
}
}
fn exceeded_metric_increased(
head: &fallow_output::ComplexityViolation,
base: &ComplexityMetrics,
) -> bool {
let exceeded = head.exceeded;
(exceeded.includes_cyclomatic() && head.cyclomatic > base.cyclomatic)
|| (exceeded.includes_cognitive() && head.cognitive > base.cognitive)
|| (exceeded.includes_crap() && measured_crap_grew(head, base))
}
fn newly_exceeded_metric_grew(
head: &fallow_output::ComplexityViolation,
base: &ComplexityMetrics,
) -> bool {
let (now, before) = (head.exceeded, base.exceeded);
(now.includes_cyclomatic()
&& !before.includes_cyclomatic()
&& head.cyclomatic > base.cyclomatic)
|| (now.includes_cognitive()
&& !before.includes_cognitive()
&& head.cognitive > base.cognitive)
|| (now.includes_crap() && !before.includes_crap() && measured_crap_grew(head, base))
}
fn measured_crap_grew(head: &fallow_output::ComplexityViolation, base: &ComplexityMetrics) -> bool {
crap_is_comparable(head, base)
&& matches!((head.crap, base.crap), (Some(head), Some(base)) if head > base)
}
fn crap_is_comparable(head: &fallow_output::ComplexityViolation, base: &ComplexityMetrics) -> bool {
head.crap.is_some() && base.crap.is_some() && !crap_is_estimated(head) && !base.crap_estimated
}
fn severity_tier_rose(head: &fallow_output::ComplexityViolation, base: &ComplexityMetrics) -> bool {
if crap_is_comparable(head, base) {
return head.severity > base.severity;
}
complexity_tier(head.cognitive, head.cyclomatic)
> complexity_tier(base.cognitive, base.cyclomatic)
}
fn complexity_tier(cognitive: u16, cyclomatic: u16) -> FindingSeverity {
compute_finding_severity(
cognitive,
cyclomatic,
None,
DEFAULT_COGNITIVE_HIGH,
DEFAULT_COGNITIVE_CRITICAL,
DEFAULT_CYCLOMATIC_HIGH,
DEFAULT_CYCLOMATIC_CRITICAL,
)
}
#[must_use]
pub const fn crap_is_estimated(finding: &fallow_output::ComplexityViolation) -> bool {
matches!(
finding.coverage_source,
Some(CoverageSource::Estimated | CoverageSource::EstimatedComponentInherited)
)
}
#[must_use]
pub const fn exceeds_only_estimated_crap(finding: &fallow_output::ComplexityViolation) -> bool {
matches!(finding.exceeded, ExceededThreshold::Crap) && crap_is_estimated(finding)
}
pub fn demote_estimated_crap_gates(findings: &mut [fallow_output::HealthFinding]) {
for finding in findings {
if exceeds_only_estimated_crap(finding)
&& finding.effective_severity == Some(EffectiveSeverity::Error)
{
finding.violation.effective_severity = Some(EffectiveSeverity::Warn);
}
}
}
pub fn exempt_test_file_gates(findings: &mut [fallow_output::HealthFinding], root: &Path) {
for finding in findings {
let relative = finding.path.strip_prefix(root).unwrap_or(&finding.path);
if fallow_engine::test_paths::is_test_path(root, relative) {
finding.violation.effective_severity = Some(EffectiveSeverity::Warn);
finding.gate_exemption = Some(AuditGateExemption::TestFile);
}
}
}
#[must_use]
pub fn classify_complexity_findings(
findings: &[fallow_output::HealthFinding],
root: &Path,
base: &ComplexityBaseline,
) -> Vec<bool> {
classify_complexity_attribution(findings, root, base)
.into_iter()
.map(|class| class.introduced)
.collect()
}
#[must_use]
pub fn classify_complexity_attribution(
findings: &[fallow_output::HealthFinding],
root: &Path,
base: &ComplexityBaseline,
) -> Vec<ComplexityClass> {
let mut groups: FxHashMap<String, Vec<usize>> = FxHashMap::default();
for (index, finding) in findings.iter().enumerate() {
groups
.entry(health_finding_key(finding, root))
.or_default()
.push(index);
}
let mut matches: Vec<Option<&ComplexityMetrics>> = vec![None; findings.len()];
for (key, mut heads) in groups {
let Some(bases) = base.get(&key) else {
continue;
};
heads.sort_by_key(|&index| (findings[index].line, findings[index].col));
let mut unmatched_bases: Vec<Option<&ComplexityMetrics>> = bases.iter().map(Some).collect();
let rules: [fn(&ComplexityMetrics, &ComplexityMetrics) -> bool; 2] = [
ComplexityMetrics::same_values_and_lines,
ComplexityMetrics::same_values,
];
for rule in rules {
for &index in &heads {
if matches[index].is_some() {
continue;
}
let head = ComplexityMetrics::of(&findings[index]);
if let Some(slot) = unmatched_bases
.iter_mut()
.find(|slot| slot.is_some_and(|base| rule(base, &head)))
{
matches[index] = slot.take();
}
}
}
let mut remaining_bases = unmatched_bases.into_iter().flatten();
for &index in &heads {
if matches[index].is_none() {
matches[index] = remaining_bases.next();
}
}
}
findings
.iter()
.zip(matches)
.map(|(finding, base)| ComplexityClass::of_match(finding, base))
.collect()
}
#[must_use]
pub fn classify_complexity_findings_against_all<'a>(
findings: &[fallow_output::HealthFinding],
root: &Path,
bases: impl IntoIterator<Item = &'a ComplexityBaseline>,
) -> Vec<bool> {
classify_complexity_attribution_against_all(findings, root, bases)
.into_iter()
.map(|class| class.introduced)
.collect()
}
#[must_use]
pub fn classify_complexity_attribution_against_all<'a>(
findings: &[fallow_output::HealthFinding],
root: &Path,
bases: impl IntoIterator<Item = &'a ComplexityBaseline>,
) -> Vec<ComplexityClass> {
let per_base: Vec<Vec<ComplexityClass>> = bases
.into_iter()
.map(|base| classify_complexity_attribution(findings, root, base))
.collect();
(0..findings.len())
.map(|index| {
let classes = per_base.iter().map(|classes| &classes[index]);
let inherited = classes
.clone()
.filter(|class| !class.introduced)
.min_by_key(|class| class.worsened_metrics.len());
inherited.cloned().unwrap_or_else(|| ComplexityClass {
introduced: true,
exceeded_metric_grew: false,
worsened_metrics: classes
.map(|class| &class.worsened_metrics)
.find(|metrics| !metrics.is_empty())
.cloned()
.unwrap_or_default(),
})
})
.collect()
}
#[must_use]
pub fn complexity_ledger_keys(
findings: &[fallow_output::HealthFinding],
root: &Path,
) -> Vec<String> {
let mut seen: FxHashMap<String, usize> = FxHashMap::default();
findings
.iter()
.map(|finding| {
let key = health_finding_key(finding, root);
let occurrence = seen.entry(key.clone()).or_default();
*occurrence += 1;
if *occurrence == 1 {
key
} else {
format!("{key}#{occurrence}")
}
})
.collect()
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use fallow_output::{
ComplexityViolation, ExceededThreshold, FindingSeverity, HealthFinding, HealthReport,
};
use super::*;
const MAX: u16 = 15;
fn root() -> PathBuf {
PathBuf::from("/project")
}
fn finding(name: &str, line: u32, cyclomatic: u16, cognitive: u16) -> HealthFinding {
HealthFinding::from(ComplexityViolation {
path: root().join("src/index.js"),
name: name.to_string(),
line,
col: 0,
cyclomatic,
cognitive,
line_count: 30,
param_count: 1,
react_hook_count: 0,
react_jsx_max_depth: 0,
react_prop_count: 0,
react_hook_profile: None,
exceeded: ExceededThreshold::from_bools(cyclomatic > MAX, cognitive > MAX, false),
effective_severity: None,
severity: FindingSeverity::High,
crap: None,
mutation: None,
coverage_pct: None,
coverage_tier: None,
coverage_source: None,
inherited_from: None,
component_rollup: None,
contributions: Vec::new(),
effective_thresholds: None,
threshold_source: None,
})
}
fn baseline(findings: Vec<HealthFinding>) -> ComplexityBaseline {
complexity_baseline(
&HealthReport {
findings,
..HealthReport::default()
},
&root(),
)
}
fn classify(base: Vec<HealthFinding>, head: &[HealthFinding]) -> Vec<bool> {
classify_complexity_findings(head, &root(), &baseline(base))
}
#[test]
fn identity_key_excludes_category_and_line() {
let key = health_finding_key(&finding("hotspot", 4, 17, 16), &root());
assert_eq!(key, "complexity:src/index.js:hotspot");
}
#[test]
fn issue_cases() {
assert_eq!(
classify(vec![finding("f", 1, 17, 16)], &[finding("f", 1, 17, 16)]),
[false]
);
assert_eq!(
classify(vec![finding("f", 1, 17, 16)], &[finding("f", 1, 18, 17)]),
[false]
);
assert_eq!(
classify(vec![finding("f", 1, 18, 17)], &[finding("f", 1, 17, 16)]),
[false]
);
assert_eq!(
classify(vec![finding("f", 1, 17, 16)], &[finding("f", 1, 16, 15)]),
[false]
);
assert_eq!(classify(vec![], &[finding("f", 1, 17, 16)]), [true]);
}
#[test]
fn only_exceeded_metrics_can_introduce() {
assert_eq!(
classify(vec![finding("f", 1, 20, 5)], &[finding("f", 1, 20, 9)]),
[false]
);
assert_eq!(
classify(vec![finding("f", 1, 20, 14)], &[finding("f", 1, 19, 16)]),
[true]
);
}
#[test]
fn a_finding_that_any_base_has_is_inherited() {
let primary = baseline(vec![]);
let merged = baseline(vec![finding("f", 1, 17, 16)]);
let head = [finding("f", 1, 17, 16), finding("g", 9, 17, 16)];
assert_eq!(
classify_complexity_findings_against_all(&head, &root(), [&primary, &merged]),
[false, true]
);
assert_eq!(
classify_complexity_findings_against_all(
&[finding("f", 1, 31, 17)],
&root(),
[&baseline(vec![finding("f", 1, 17, 16)]), &merged]
),
[true]
);
}
#[test]
fn line_shift_keeps_the_match() {
assert_eq!(
classify(vec![finding("f", 1, 17, 16)], &[finding("f", 40, 17, 16)]),
[false]
);
}
fn crap_finding(crap: Option<f64>, source: Option<CoverageSource>) -> HealthFinding {
let mut finding = finding("f", 1, 16, 5);
finding.violation.exceeded = ExceededThreshold::Crap;
finding.violation.crap = crap;
finding.violation.coverage_source = source;
finding.violation.effective_severity = Some(EffectiveSeverity::Error);
finding
}
#[test]
fn crap_without_base_value_is_not_an_increase() {
let head = crap_finding(Some(50.0), Some(CoverageSource::Istanbul));
assert_eq!(
classify(vec![crap_finding(None, None)], std::slice::from_ref(&head)),
[false]
);
assert_eq!(
classify(
vec![crap_finding(Some(40.0), Some(CoverageSource::Istanbul))],
std::slice::from_ref(&head)
),
[false]
);
assert_eq!(
classify(
vec![crap_finding(Some(60.0), Some(CoverageSource::Istanbul))],
&[head]
),
[false]
);
}
#[test]
fn estimated_crap_growth_is_worsened_not_introduced() {
let estimated = Some(CoverageSource::Estimated);
let classes = attribution(
vec![crap_finding(Some(40.0), estimated)],
&[crap_finding(Some(50.0), estimated)],
);
assert_eq!(classes[0].attribution(), AuditFindingAttribution::Worsened);
assert_eq!(
classes[0].worsened_metrics,
[metric(WorsenedMetricKind::Crap, 40.0, 50.0)]
);
let mut base = crap_finding(Some(40.0), estimated);
base.violation.exceeded = ExceededThreshold::CognitiveCrap;
base.violation.cognitive = 20;
let mut head = base.clone();
head.violation.cognitive = 21;
head.violation.crap = Some(50.0);
assert_eq!(classify(vec![base.clone()], &[head.clone()]), [false]);
head.violation.cognitive = 25;
assert_eq!(classify(vec![base], &[head]), [true]);
}
#[test]
fn test_file_findings_get_warn_gate_severity_and_the_exemption() {
let project = root().join("tests").join("project");
let at = |relative: &str| {
let mut finding = finding("f", 1, 16, 5);
finding.violation.path = project.join(relative);
finding.violation.effective_severity = Some(EffectiveSeverity::Error);
finding
};
let mut findings = [
at("src/__tests__/app.js"),
at("src/app.spec.ts"),
at("src/app.test.tsx"),
at("src/__mocks__/api.js"),
at("src/app.js"),
at("src/latest.js"),
];
exempt_test_file_gates(&mut findings, &project);
let marks: Vec<_> = findings
.iter()
.map(|finding| (finding.effective_severity, finding.gate_exemption))
.collect();
let exempt = (
Some(EffectiveSeverity::Warn),
Some(AuditGateExemption::TestFile),
);
let gated = (Some(EffectiveSeverity::Error), None);
assert_eq!(marks, [exempt, exempt, exempt, exempt, gated, gated]);
}
#[test]
fn estimated_crap_only_findings_get_warn_gate_severity() {
let mut both = crap_finding(Some(50.0), Some(CoverageSource::Estimated));
both.violation.exceeded = ExceededThreshold::CognitiveCrap;
let mut warn_rule = crap_finding(Some(50.0), Some(CoverageSource::Estimated));
warn_rule.violation.effective_severity = Some(EffectiveSeverity::Warn);
let mut findings = [
crap_finding(Some(50.0), Some(CoverageSource::Estimated)),
crap_finding(
Some(50.0),
Some(CoverageSource::EstimatedComponentInherited),
),
crap_finding(Some(50.0), Some(CoverageSource::Istanbul)),
both,
warn_rule,
];
demote_estimated_crap_gates(&mut findings);
let severities: Vec<_> = findings
.iter()
.map(|finding| finding.effective_severity)
.collect();
assert_eq!(
severities,
[
Some(EffectiveSeverity::Warn),
Some(EffectiveSeverity::Warn),
Some(EffectiveSeverity::Error),
Some(EffectiveSeverity::Error),
Some(EffectiveSeverity::Warn),
]
);
}
#[test]
fn same_named_findings_match_unchanged_values_first() {
let base = vec![finding("run", 1, 17, 16), finding("run", 30, 21, 20)];
assert_eq!(
classify(
base.clone(),
&[finding("run", 1, 17, 16), finding("run", 30, 21, 20)]
),
[false, false]
);
assert_eq!(
classify(base.clone(), &[finding("run", 1, 21, 20)]),
[false]
);
assert_eq!(
classify(
base.clone(),
&[
finding("run", 1, 30, 30),
finding("run", 20, 17, 16),
finding("run", 50, 21, 20)
]
),
[true, false, false]
);
assert_eq!(
classify(
base.clone(),
&[finding("run", 1, 18, 17), finding("run", 30, 21, 20)]
),
[false, false]
);
assert_eq!(
classify(
base,
&[finding("run", 1, 30, 17), finding("run", 30, 21, 20)]
),
[true, false]
);
}
fn with_lines(mut finding: HealthFinding, line_count: u32) -> HealthFinding {
finding.violation.line_count = line_count;
finding
}
fn attribution(base: Vec<HealthFinding>, head: &[HealthFinding]) -> Vec<ComplexityClass> {
classify_complexity_attribution(head, &root(), &baseline(base))
}
fn metric(kind: WorsenedMetricKind, base: f64, head: f64) -> WorsenedMetric {
WorsenedMetric {
metric: kind,
base,
head,
}
}
#[test]
fn grown_line_count_is_worsened_not_introduced() {
let classes = attribution(
vec![with_lines(finding("f", 1, 17, 16), 80)],
&[with_lines(finding("f", 1, 17, 16), 300)],
);
assert!(!classes[0].introduced);
assert!(classes[0].is_worsened());
assert_eq!(
classes[0].worsened_metrics,
[metric(WorsenedMetricKind::LineCount, 80.0, 300.0)]
);
assert_eq!(classes[0].attribution(), AuditFindingAttribution::Worsened);
}
#[test]
fn grown_metric_that_the_finding_does_not_exceed_is_worsened() {
let classes = attribution(vec![finding("f", 1, 20, 5)], &[finding("f", 1, 20, 9)]);
assert_eq!(classes[0].attribution(), AuditFindingAttribution::Worsened);
assert_eq!(
classes[0].worsened_metrics,
[metric(WorsenedMetricKind::Cognitive, 5.0, 9.0)]
);
}
#[test]
fn grown_exceeded_metric_in_the_same_tier_is_worsened() {
let classes = attribution(vec![finding("f", 1, 100, 16)], &[finding("f", 1, 103, 16)]);
assert_eq!(classes[0].attribution(), AuditFindingAttribution::Worsened);
assert!(classes[0].exceeded_metric_grew);
assert_eq!(
classes[0].worsened_metrics,
[metric(WorsenedMetricKind::Cyclomatic, 100.0, 103.0)]
);
let below = attribution(vec![finding("f", 1, 20, 5)], &[finding("f", 1, 20, 9)]);
assert!(!below[0].exceeded_metric_grew);
}
#[test]
fn severity_tier_uses_the_base_values_not_todays_thresholds() {
let mut head = finding("f", 1, 18, 10);
head.violation.exceeded = ExceededThreshold::Both;
assert_eq!(
classify(vec![finding("f", 1, 17, 10)], &[head.clone()]),
[false]
);
head.violation.cognitive = 11;
assert_eq!(classify(vec![finding("f", 1, 17, 10)], &[head]), [true]);
}
#[test]
fn measured_crap_tier_crossing_is_introduced() {
let istanbul = Some(CoverageSource::Istanbul);
let mut base = crap_finding(Some(40.0), istanbul);
base.violation.severity = FindingSeverity::Moderate;
let mut head = crap_finding(Some(120.0), istanbul);
head.violation.severity = FindingSeverity::Critical;
assert_eq!(classify(vec![base.clone()], &[head.clone()]), [true]);
let estimated = Some(CoverageSource::Estimated);
base.violation.coverage_source = estimated;
head.violation.coverage_source = estimated;
assert_eq!(classify(vec![base], &[head]), [false]);
}
#[test]
fn grown_exceeded_metric_is_introduced_with_values() {
let classes = attribution(vec![finding("f", 1, 17, 16)], &[finding("f", 1, 40, 16)]);
assert_eq!(
classes[0].attribution(),
AuditFindingAttribution::Introduced
);
assert_eq!(
classes[0].worsened_metrics,
[metric(WorsenedMetricKind::Cyclomatic, 17.0, 40.0)]
);
assert!(!classes[0].is_worsened());
}
#[test]
fn improved_and_moved_functions_are_inherited() {
let improved = attribution(
vec![with_lines(finding("f", 1, 30, 30), 90)],
&[with_lines(finding("f", 1, 20, 18), 60)],
);
assert_eq!(
improved[0].attribution(),
AuditFindingAttribution::Inherited
);
assert!(improved[0].worsened_metrics.is_empty());
let moved = attribution(vec![finding("f", 1, 17, 16)], &[finding("f", 400, 17, 16)]);
assert_eq!(moved[0].attribution(), AuditFindingAttribution::Inherited);
}
#[test]
fn new_function_is_introduced_without_values() {
let classes = attribution(vec![finding("f", 1, 17, 16)], &[finding("g", 1, 17, 16)]);
assert_eq!(
classes[0].attribution(),
AuditFindingAttribution::Introduced
);
assert!(classes[0].worsened_metrics.is_empty());
}
#[test]
fn crap_compares_only_when_both_sides_have_a_score() {
let mut head = finding("f", 1, 20, 5);
head.violation.crap = Some(50.0);
let classes = attribution(vec![finding("f", 1, 20, 5)], std::slice::from_ref(&head));
assert_eq!(classes[0].attribution(), AuditFindingAttribution::Inherited);
let mut base = finding("f", 1, 20, 5);
base.violation.crap = Some(40.0);
let classes = attribution(vec![base], &[head]);
assert_eq!(
classes[0].worsened_metrics,
[metric(WorsenedMetricKind::Crap, 40.0, 50.0)]
);
}
#[test]
fn same_named_findings_prefer_the_base_with_equal_line_count() {
let base = vec![
with_lines(finding("run", 1, 17, 16), 10),
with_lines(finding("run", 30, 17, 16), 40),
];
let head = [
with_lines(finding("run", 1, 17, 16), 40),
with_lines(finding("run", 50, 17, 16), 25),
];
let classes = attribution(base, &head);
assert_eq!(classes[0].attribution(), AuditFindingAttribution::Inherited);
assert_eq!(classes[1].attribution(), AuditFindingAttribution::Worsened);
}
#[test]
fn an_exact_match_of_a_later_head_is_not_taken_by_an_earlier_head() {
let base = vec![
with_lines(finding("run", 1, 17, 16), 40),
with_lines(finding("run", 50, 17, 16), 10),
];
let head = [
with_lines(finding("run", 1, 17, 16), 30),
with_lines(finding("run", 20, 17, 16), 40),
];
let classes = attribution(base, &head);
assert_eq!(classes[0].attribution(), AuditFindingAttribution::Worsened);
assert_eq!(
classes[0].worsened_metrics,
[metric(WorsenedMetricKind::LineCount, 10.0, 30.0)]
);
assert_eq!(classes[1].attribution(), AuditFindingAttribution::Inherited);
assert!(classes[1].worsened_metrics.is_empty());
}
#[test]
fn small_line_growth_is_not_worsened() {
let classes = attribution(
vec![with_lines(finding("f", 1, 17, 16), 80)],
&[with_lines(finding("f", 1, 17, 16), 84)],
);
assert_eq!(classes[0].attribution(), AuditFindingAttribution::Inherited);
assert!(classes[0].worsened_metrics.is_empty());
}
#[test]
fn line_growth_needs_five_lines_at_any_function_size() {
let short = attribution(
vec![with_lines(finding("f", 1, 17, 16), 20)],
&[with_lines(finding("f", 1, 17, 16), 24)],
);
assert_eq!(short[0].attribution(), AuditFindingAttribution::Inherited);
let grown = attribution(
vec![with_lines(finding("f", 1, 17, 16), 20)],
&[with_lines(finding("f", 1, 17, 16), 25)],
);
assert_eq!(grown[0].attribution(), AuditFindingAttribution::Worsened);
let long = attribution(
vec![with_lines(finding("f", 1, 17, 16), 300)],
&[with_lines(finding("f", 1, 17, 16), 309)],
);
assert_eq!(long[0].attribution(), AuditFindingAttribution::Worsened);
}
#[test]
fn a_base_with_the_same_values_keeps_the_finding_inherited() {
let primary = baseline(vec![with_lines(finding("f", 1, 17, 16), 20)]);
let merged = baseline(vec![with_lines(finding("f", 1, 17, 16), 60)]);
let head = [with_lines(finding("f", 1, 17, 16), 60)];
let classes =
classify_complexity_attribution_against_all(&head, &root(), [&primary, &merged]);
assert_eq!(classes[0].attribution(), AuditFindingAttribution::Inherited);
let classes = classify_complexity_attribution_against_all(&head, &root(), [&primary]);
assert_eq!(classes[0].attribution(), AuditFindingAttribution::Worsened);
}
#[test]
fn rename_moves_the_base_findings() {
let base = baseline(vec![finding("f", 1, 17, 16)]);
let renames =
FxHashMap::from_iter([("src/index.js".to_string(), "src/moved.js".to_string())]);
let remapped = remap_complexity_baseline_for_renames(base, &renames);
assert!(remapped.contains_key("complexity:src/moved.js:f"));
assert!(!remapped.contains_key("complexity:src/index.js:f"));
}
#[test]
fn ledger_keys_count_same_named_findings_separately() {
let keys = complexity_ledger_keys(
&[finding("run", 1, 17, 16), finding("run", 30, 17, 16)],
&root(),
);
assert_eq!(
keys,
[
"complexity:src/index.js:run",
"complexity:src/index.js:run#2"
]
);
}
}