use std::collections::{HashMap, HashSet};
use crate::facts::FactsDb;
use crate::{Options, Result};
pub const DEFAULT_MIN_FAN_IN: u32 = 3;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct UnstableInterfaceRow {
pub path: String,
pub fan_in: u32,
pub revisions: u32,
pub coupled_dependents: u32,
pub instability_score: f64,
}
#[tracing::instrument(name = "unstable-interface", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_unstable_interface(db: &FactsDb, opts: &Options) -> Result<Vec<UnstableInterfaceRow>> {
let import_pairs: Vec<(String, String)> = crate::analyses::query::query_map_collect(
db,
"SELECT target_path, src_path FROM imports WHERE target_path IS NOT NULL",
[],
"unstable-interface imports",
|r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)),
)?;
if import_pairs.is_empty() {
return Ok(Vec::new());
}
let mut importers: HashMap<String, HashSet<String>> = HashMap::new();
for (target, src) in import_pairs {
importers.entry(target).or_default().insert(src);
}
let revisions: HashMap<String, u32> =
crate::analyses::revisions::run_revisions(db, &opts.with_no_row_limit())?
.into_iter()
.collect();
let coupling_rows = crate::analyses::coupling::run_coupling(db, &opts.with_no_row_limit())?;
let partners = crate::analyses::coupling::partner_index(&coupling_rows);
let mut out: Vec<UnstableInterfaceRow> = Vec::new();
for (path, deps) in &importers {
let fan_in = u32::try_from(deps.len()).unwrap_or(u32::MAX);
if fan_in < DEFAULT_MIN_FAN_IN {
continue;
}
let revs = revisions.get(path).copied().unwrap_or(0);
if revs < opts.min_revs {
continue;
}
let coupled_dependents = partners.get(path).map_or(0, |pt| {
u32::try_from(deps.intersection(pt).count()).unwrap_or(u32::MAX)
});
if coupled_dependents == 0 {
continue;
}
out.push(UnstableInterfaceRow {
path: path.clone(),
fan_in,
revisions: revs,
coupled_dependents,
instability_score: f64::from(revs) * f64::from(coupled_dependents),
});
}
out.sort_by(|a, b| {
b.instability_score
.partial_cmp(&a.instability_score)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.path.cmp(&b.path))
});
if let Some(limit) = opts.rows_limit {
out.truncate(limit as usize);
}
Ok(out)
}