use crate::metrics::{score_count_bands, MetricValue, RawValue};
use crate::snapshot::RepoSnapshot;
fn percentile_75<T: Ord + Copy + Default>(mut values: Vec<T>) -> T {
values.sort_unstable();
values
.get(values.len().saturating_sub(1) * 3 / 4)
.copied()
.unwrap_or_default()
}
pub(super) fn complex_hotspots(snapshot: &RepoSnapshot) -> MetricValue {
if snapshot.file_metrics.is_empty() {
return MetricValue {
name: "Complex hotspots".to_string(),
description: "No AST data available".to_string(),
raw_value: RawValue::Count(0),
score: None,
};
}
let cc_p75 = percentile_75(
snapshot
.file_metrics
.values()
.map(|m| m.cyclomatic_complexity)
.collect(),
);
let churn_p75 = percentile_75(snapshot.commits_by_file.values().map(|c| c.len()).collect());
let hotspots: Vec<String> = snapshot
.file_metrics
.iter()
.filter(|(path, m)| {
let churn = snapshot
.commits_by_file
.get(*path)
.map(|c| c.len())
.unwrap_or(0);
m.cyclomatic_complexity > cc_p75 && churn > churn_p75
})
.map(|(p, _)| p.display().to_string())
.collect();
let count = hotspots.len();
let score = score_count_bands(count);
MetricValue {
name: "Complex hotspots".to_string(),
description: format!("{} files with high complexity and high churn", count),
raw_value: RawValue::List(hotspots),
score: Some(score),
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
use crate::snapshot::*;
#[test]
fn percentile_75_empty_returns_default() {
assert_eq!(percentile_75::<u32>(vec![]), 0);
}
#[test]
fn percentile_75_single_element() {
assert_eq!(percentile_75(vec![42u32]), 42);
}
#[test]
fn percentile_75_four_elements() {
assert_eq!(percentile_75(vec![4u32, 1, 3, 2]), 3);
}
#[test]
fn percentile_75_eight_elements() {
assert_eq!(percentile_75(vec![8u32, 3, 1, 6, 2, 7, 4, 5]), 6);
}
#[test]
fn percentile_75_unsorted_input_sorted_before_lookup() {
assert_eq!(
percentile_75(vec![10u32, 8, 6, 4, 2]),
percentile_75(vec![2u32, 4, 6, 8, 10])
);
}
#[test]
fn complex_hotspots_finds_high_cc_high_churn_files() {
let mut snapshot = RepoSnapshot::new(
PathBuf::from("/tmp"),
"test".into(),
"main".into(),
TimeWindow::default(),
);
let files: &[(&str, u32, usize)] = &[
("bad.rs", 20, 20), ("ok1.rs", 2, 1),
("ok2.rs", 3, 2),
("ok3.rs", 4, 3),
];
for (name, cc, churn) in files {
snapshot.file_metrics.insert(
PathBuf::from(name),
FileComplexity {
total_lines: 100,
loc: 80,
cyclomatic_complexity: *cc,
public_methods: 2,
properties: 1,
..Default::default()
},
);
snapshot.commits_by_file.insert(
PathBuf::from(name),
(0..*churn).map(|i| CommitId(i as u32)).collect(),
);
}
let result = complex_hotspots(&snapshot);
assert_eq!(result.score, Some(75)); match &result.raw_value {
RawValue::List(v) => assert_eq!(v.len(), 1),
_ => panic!("Expected List"),
}
}
#[test]
fn complex_hotspots_scores_100_when_none() {
let mut snapshot = RepoSnapshot::new(
PathBuf::from("/tmp"),
"test".into(),
"main".into(),
TimeWindow::default(),
);
for i in 0..4 {
snapshot.file_metrics.insert(
PathBuf::from(format!("f{}.rs", i)),
FileComplexity {
total_lines: 100,
loc: 80,
cyclomatic_complexity: 5,
public_methods: 2,
properties: 1,
..Default::default()
},
);
snapshot.commits_by_file.insert(
PathBuf::from(format!("f{}.rs", i)),
vec![CommitId(i as u32)],
);
}
let result = complex_hotspots(&snapshot);
assert_eq!(result.score, Some(100));
}
#[test]
fn complex_hotspots_ignores_high_cc_low_churn() {
let mut snapshot = RepoSnapshot::new(
PathBuf::from("/tmp"),
"test".into(),
"main".into(),
TimeWindow::default(),
);
let files: &[(&str, u32, usize)] = &[
("complex.rs", 100, 1), ("churny.rs", 1, 50), ("normal1.rs", 2, 2),
("normal2.rs", 3, 3),
];
for (name, cc, churn) in files {
snapshot.file_metrics.insert(
PathBuf::from(name),
FileComplexity {
total_lines: 100,
loc: 80,
cyclomatic_complexity: *cc,
public_methods: 2,
properties: 1,
..Default::default()
},
);
snapshot.commits_by_file.insert(
PathBuf::from(name),
(0..*churn).map(|i| CommitId(i as u32)).collect(),
);
}
let result = complex_hotspots(&snapshot);
assert_eq!(result.score, Some(100)); }
#[test]
fn complex_hotspots_scores_50_with_three_hotspots() {
let mut snapshot = RepoSnapshot::new(
PathBuf::from("/tmp"),
"test".into(),
"main".into(),
TimeWindow::default(),
);
for i in 0..9usize {
snapshot.file_metrics.insert(
PathBuf::from(format!("normal{}.rs", i)),
FileComplexity {
total_lines: 100,
loc: 80,
cyclomatic_complexity: 2,
public_methods: 2,
properties: 1,
..Default::default()
},
);
snapshot.commits_by_file.insert(
PathBuf::from(format!("normal{}.rs", i)),
vec![CommitId(i as u32)],
);
}
for i in 0..3usize {
snapshot.file_metrics.insert(
PathBuf::from(format!("hot{}.rs", i)),
FileComplexity {
total_lines: 200,
loc: 180,
cyclomatic_complexity: 100,
public_methods: 5,
properties: 1,
..Default::default()
},
);
snapshot.commits_by_file.insert(
PathBuf::from(format!("hot{}.rs", i)),
(0..50).map(|j| CommitId((i * 50 + j) as u32)).collect(),
);
}
let result = complex_hotspots(&snapshot);
assert_eq!(result.score, Some(50));
}
#[test]
fn complex_hotspots_boundary_at_p75_not_flagged() {
let mut snapshot = RepoSnapshot::new(
PathBuf::from("/tmp"),
"test".into(),
"main".into(),
TimeWindow::default(),
);
let files: &[(&str, u32, usize)] = &[
("f1.rs", 1, 1),
("f2.rs", 3, 3),
("f3.rs", 5, 5), ("f4.rs", 5, 5), ];
for (name, cc, churn) in files {
snapshot.file_metrics.insert(
PathBuf::from(name),
FileComplexity {
total_lines: 100,
loc: 80,
cyclomatic_complexity: *cc,
public_methods: 2,
properties: 1,
..Default::default()
},
);
snapshot.commits_by_file.insert(
PathBuf::from(name),
(0..*churn).map(|i| CommitId(i as u32)).collect(),
);
}
let result = complex_hotspots(&snapshot);
assert_eq!(result.score, Some(100)); }
}