use std::collections::HashSet;
use crate::analyses::function_xray::{fetch_hunks_for_path, rev_to_function_sets};
use crate::facts::FactsDb;
use crate::repo::Repo;
use crate::stats::fisher_two_tail_pvalue;
use crate::{Options, Result};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FunctionCouplingRow {
pub a: String,
pub b: String,
pub co_changes: u32,
pub a_changes: u32,
pub b_changes: u32,
pub confidence: f64,
pub p_value: Option<f64>,
}
#[tracing::instrument(name = "function-coupling", skip_all, fields(target = target))]
pub fn run_function_coupling<R: Repo>(
db: &FactsDb,
repo: &R,
opts: &Options,
target: &str,
) -> Result<Vec<FunctionCouplingRow>> {
let hunk_rows = fetch_hunks_for_path(db, target)?;
if hunk_rows.is_empty() {
return Ok(Vec::new());
}
let n_revs: HashSet<&str> = hunk_rows
.iter()
.map(|(rev, _, _, _)| rev.as_str())
.collect();
let n = u32::try_from(n_revs.len()).unwrap_or(u32::MAX);
let rev_sets = rev_to_function_sets(db, repo, target)?;
if rev_sets.is_empty() {
return Ok(Vec::new());
}
let all_fns: HashSet<&str> = rev_sets
.values()
.flat_map(|s| s.iter().map(String::as_str))
.collect();
let mut all_fns: Vec<&str> = all_fns.into_iter().collect();
all_fns.sort_unstable();
let fn_count = all_fns.len();
let mut fn_changes: Vec<u32> = vec![0u32; fn_count];
let mut co_matrix: Vec<u32> = vec![0u32; fn_count * fn_count];
for set in rev_sets.values() {
let touched: Vec<usize> = all_fns
.iter()
.enumerate()
.filter_map(|(i, &name)| if set.contains(name) { Some(i) } else { None })
.collect();
for &i in &touched {
fn_changes[i] += 1;
}
for (pos_a, &i) in touched.iter().enumerate() {
for &j in &touched[pos_a + 1..] {
co_matrix[i * fn_count + j] += 1;
}
}
}
let mut rows: Vec<FunctionCouplingRow> = Vec::new();
for i in 0..fn_count {
for j in i + 1..fn_count {
let co = co_matrix[i * fn_count + j];
if co < 2 {
continue;
}
let a_ch = fn_changes[i];
let b_ch = fn_changes[j];
let a_only = a_ch.saturating_sub(co);
let b_only = b_ch.saturating_sub(co);
let neither = n
.saturating_sub(co)
.saturating_sub(a_only)
.saturating_sub(b_only);
let p_value = fisher_two_tail_pvalue(co, a_only, b_only, neither);
let confidence = f64::from(co) / f64::from(a_ch.min(b_ch)).max(1.0);
rows.push(FunctionCouplingRow {
a: all_fns[i].to_string(),
b: all_fns[j].to_string(),
co_changes: co,
a_changes: a_ch,
b_changes: b_ch,
confidence,
p_value,
});
}
}
rows.sort_unstable_by(|x, y| match (x.p_value, y.p_value) {
(Some(px), Some(py)) => px
.partial_cmp(&py)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| {
y.confidence
.partial_cmp(&x.confidence)
.unwrap_or(std::cmp::Ordering::Equal)
})
.then_with(|| x.a.cmp(&y.a))
.then_with(|| x.b.cmp(&y.b)),
(None, Some(_)) => std::cmp::Ordering::Less,
(Some(_), None) => std::cmp::Ordering::Greater,
(None, None) => x.a.cmp(&y.a).then_with(|| x.b.cmp(&y.b)),
});
if let Some(limit) = opts.rows_limit {
rows.truncate(limit as usize);
}
Ok(rows)
}