use super::BoundarySignal;
use crate::audits::context::classify::helpers::is_test_file;
use crate::graph::v2::{build_coupling_graph_snapshot, direct_dependents};
use crate::review::diff::ChangedFile;
use crate::review::paths::normalized_review_path;
use crate::scan::types::CouplingGraph;
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
pub fn build_importers_by_target(
graph: &CouplingGraph,
repo_root: &Path,
) -> BTreeMap<PathBuf, BTreeSet<PathBuf>> {
let (snapshot, path_by_id) = build_coupling_graph_snapshot(graph);
let mut importers: BTreeMap<PathBuf, BTreeSet<PathBuf>> = BTreeMap::new();
for (target_id, source_ids) in direct_dependents(&snapshot) {
if source_ids.is_empty() {
continue;
}
let Some(target) = path_by_id.get(&target_id) else {
continue;
};
let entry = importers
.entry(normalized_review_path(target, repo_root))
.or_default();
for source_id in &source_ids {
if let Some(source) = path_by_id.get(source_id) {
entry.insert(normalized_review_path(source, repo_root));
}
}
}
importers
}
pub fn enrich_blast_radius(
signals: &mut [BoundarySignal],
graph: Option<&CouplingGraph>,
repo_root: &Path,
) {
let Some(graph) = graph else {
return;
};
let importers = build_importers_by_target(graph, repo_root);
for signal in signals.iter_mut() {
let normalized = normalized_review_path(Path::new(&signal.path), repo_root);
signal.blast_radius = importers.get(&normalized).map_or(0, BTreeSet::len);
}
}
pub fn any_test_changed(changed_files: &[ChangedFile]) -> bool {
changed_files.iter().any(|file| is_test_file(&file.path))
}
pub fn missing_test_for_code_boundary(
signals: &[BoundarySignal],
changed_files: &[ChangedFile],
) -> bool {
signals
.iter()
.any(|signal| signal.category.is_code_boundary())
&& !any_test_changed(changed_files)
}
#[cfg(test)]
mod tests {
use super::*;
fn coupling_graph(edges: &[(&str, &str)]) -> CouplingGraph {
let mut edge_map: BTreeMap<PathBuf, BTreeSet<PathBuf>> = BTreeMap::new();
let mut nodes: BTreeSet<PathBuf> = BTreeSet::new();
for (src, dst) in edges {
let src = PathBuf::from(src);
let dst = PathBuf::from(dst);
nodes.insert(src.clone());
nodes.insert(dst.clone());
edge_map.entry(src).or_default().insert(dst);
}
CouplingGraph {
edges: edge_map,
nodes,
}
}
#[test]
fn importers_by_target_inverts_edges_with_normalized_paths() {
let root = Path::new("/repo");
let graph = coupling_graph(&[("a.rs", "b.rs"), ("a.rs", "c.rs"), ("b.rs", "c.rs")]);
let importers = build_importers_by_target(&graph, root);
let norm = |path: &str| normalized_review_path(Path::new(path), root);
assert_eq!(
importers.get(&norm("c.rs")),
Some(&BTreeSet::from([norm("a.rs"), norm("b.rs")]))
);
assert_eq!(
importers.get(&norm("b.rs")),
Some(&BTreeSet::from([norm("a.rs")]))
);
assert!(!importers.contains_key(&norm("a.rs")));
}
}