use super::cognitive::cognitive_per_function;
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CognitiveDelta {
pub name: String,
pub before: u32,
pub after: u32,
}
impl CognitiveDelta {
pub fn increase(&self) -> i64 {
i64::from(self.after) - i64::from(self.before)
}
pub fn crosses_threshold(&self, threshold: u32) -> bool {
self.before <= threshold && self.after > threshold
}
}
pub fn cognitive_delta(old: &str, new: &str, ext: &str) -> Vec<CognitiveDelta> {
let before = map_by_name(old, ext);
let after = map_by_name(new, ext);
let mut out: Vec<CognitiveDelta> = after
.iter()
.filter_map(|(name, &after_cc)| {
let before_cc = before.get(name).copied().unwrap_or(0);
(before_cc != after_cc).then(|| CognitiveDelta {
name: name.clone(),
before: before_cc,
after: after_cc,
})
})
.collect();
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
pub fn worst_regression(deltas: &[CognitiveDelta], threshold: u32) -> Option<&CognitiveDelta> {
deltas
.iter()
.filter(|d| d.after > d.before && d.after > threshold)
.max_by_key(|d| {
(
d.after - d.before,
d.after,
std::cmp::Reverse(d.name.clone()),
)
})
}
pub fn format_gate_notice(delta: &CognitiveDelta, threshold: u32) -> String {
format!(
"[CODE HEALTH] fn {}: cognitive {}->{} (+{}, >{}) — consider extracting helpers",
delta.name,
delta.before,
delta.after,
delta.after.saturating_sub(delta.before),
threshold
)
}
fn map_by_name(source: &str, ext: &str) -> BTreeMap<String, u32> {
let mut map: BTreeMap<String, u32> = BTreeMap::new();
if let Some(fns) = cognitive_per_function(source, ext) {
for f in fns {
let entry = map.entry(f.name).or_insert(0);
*entry = (*entry).max(f.cognitive);
}
}
map
}
#[cfg(all(test, feature = "tree-sitter"))]
pub mod tests {
use super::*;
#[test]
fn reports_increase_for_edited_function() {
let old = "fn f(a: bool) { if a {} }";
let new = "fn f(a: bool, b: bool) { if a { if b {} } }";
let deltas = cognitive_delta(old, new, "rs");
assert_eq!(deltas.len(), 1);
assert_eq!(deltas[0].name, "f");
assert_eq!(deltas[0].before, 1);
assert_eq!(deltas[0].after, 3);
assert_eq!(deltas[0].increase(), 2);
}
#[test]
fn ignores_unchanged_functions() {
let src = "fn stable(a: bool) { if a {} }";
assert!(cognitive_delta(src, src, "rs").is_empty());
}
#[test]
fn new_function_starts_from_zero() {
let old = "fn a() {}";
let new = "fn a() {}\nfn b(x: bool) { if x { if x {} } }";
let deltas = cognitive_delta(old, new, "rs");
let b = deltas.iter().find(|d| d.name == "b").unwrap();
assert_eq!(b.before, 0);
assert_eq!(b.after, 3);
}
#[test]
fn worst_regression_picks_threshold_crosser() {
let deltas = vec![
CognitiveDelta {
name: "small".into(),
before: 2,
after: 5,
},
CognitiveDelta {
name: "big".into(),
before: 10,
after: 20,
},
];
let worst = worst_regression(&deltas, 15).unwrap();
assert_eq!(worst.name, "big");
assert!(deltas[1].crosses_threshold(15));
assert!(!deltas[0].crosses_threshold(15));
}
#[test]
fn notice_is_deterministic() {
let d = CognitiveDelta {
name: "foo".into(),
before: 8,
after: 16,
};
let n1 = format_gate_notice(&d, 15);
let n2 = format_gate_notice(&d, 15);
assert_eq!(n1, n2);
assert!(n1.contains("cognitive 8->16 (+8, >15)"));
}
}