use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use crate::Result;
use crate::facts::FactsDb;
pub struct ImportGraph {
pub id_to_path: Vec<String>,
pub path_to_id: HashMap<String, usize>,
pub adj: Vec<Vec<usize>>,
}
impl ImportGraph {
#[must_use]
pub fn len(&self) -> usize {
self.id_to_path.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.id_to_path.is_empty()
}
#[must_use]
pub fn resolved_edges(&self) -> Vec<(&str, &str)> {
let mut edges = Vec::new();
for (u, neighbors) in self.adj.iter().enumerate() {
let src = self.id_to_path[u].as_str();
for &v in neighbors {
edges.push((src, self.id_to_path[v].as_str()));
}
}
edges
}
}
pub fn build_import_graph(db: &FactsDb) -> Result<Rc<ImportGraph>> {
let memo = db.analysis_memo::<crate::analyses::memo::ImportGraphMemo>();
if let Some(graph) = memo.get() {
return Ok(graph);
}
let nodes: Vec<String> = crate::analyses::query::query_map_collect(
db,
"SELECT DISTINCT path FROM complexity_metrics ORDER BY path",
[],
"import-graph seed nodes",
|r| r.get::<_, String>(0),
)?;
let edges: Vec<(String, String)> = crate::analyses::query::query_map_collect(
db,
"SELECT src_path, target_path FROM imports WHERE target_path IS NOT NULL",
[],
"import-graph edges",
|r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)),
)?;
let graph = Rc::new(build_import_graph_seeded(&nodes, &edges));
memo.put(Rc::clone(&graph));
Ok(graph)
}
#[must_use]
pub fn build_import_graph_from_edges(edges: &[(String, String)]) -> ImportGraph {
build_import_graph_seeded(&[], edges)
}
#[must_use]
pub fn build_import_graph_seeded(seed_nodes: &[String], edges: &[(String, String)]) -> ImportGraph {
let mut path_to_id: HashMap<String, usize> = HashMap::new();
let mut id_to_path: Vec<String> = Vec::new();
for p in seed_nodes {
intern(p, &mut path_to_id, &mut id_to_path);
}
let mut edge_set: HashSet<(usize, usize)> = HashSet::with_capacity(edges.len());
for (src, tgt) in edges {
if src == tgt {
continue; }
let s = intern(src, &mut path_to_id, &mut id_to_path);
let t = intern(tgt, &mut path_to_id, &mut id_to_path);
edge_set.insert((s, t));
}
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); id_to_path.len()];
let mut sorted_edges: Vec<(usize, usize)> = edge_set.into_iter().collect();
sorted_edges.sort_unstable();
for (s, t) in sorted_edges {
adj[s].push(t);
}
ImportGraph {
id_to_path,
path_to_id,
adj,
}
}
fn intern(p: &str, path_to_id: &mut HashMap<String, usize>, id_to_path: &mut Vec<String>) -> usize {
if let Some(&id) = path_to_id.get(p) {
return id;
}
let id = id_to_path.len();
path_to_id.insert(p.to_owned(), id);
id_to_path.push(p.to_owned());
id
}
#[must_use]
pub fn tarjan_scc(adj: &[Vec<usize>]) -> Vec<Vec<usize>> {
const UNSET: usize = usize::MAX;
let n = adj.len();
let mut indices = vec![UNSET; n];
let mut low = vec![0usize; n];
let mut on_stack = vec![false; n];
let mut tstack: Vec<usize> = Vec::new();
let mut sccs: Vec<Vec<usize>> = Vec::new();
let mut idx = 0usize;
let mut call: Vec<(usize, usize)> = Vec::new();
for s in 0..n {
if indices[s] != UNSET {
continue;
}
call.push((s, 0));
while let Some(&(node, start_child)) = call.last() {
if start_child == 0 {
indices[node] = idx;
low[node] = idx;
idx += 1;
tstack.push(node);
on_stack[node] = true;
}
let mut recursed = false;
let mut j = start_child;
while j < adj[node].len() {
let child = adj[node][j];
if indices[child] == UNSET {
if let Some(top) = call.last_mut() {
top.1 = j + 1;
}
call.push((child, 0));
recursed = true;
break;
} else if on_stack[child] && indices[child] < low[node] {
low[node] = indices[child];
}
j += 1;
}
if recursed {
continue;
}
if low[node] == indices[node] {
let mut comp: Vec<usize> = Vec::new();
while let Some(w) = tstack.pop() {
on_stack[w] = false;
comp.push(w);
if w == node {
break;
}
}
sccs.push(comp);
}
call.pop();
if let Some(&(parent, _)) = call.last()
&& low[node] < low[parent]
{
low[parent] = low[node];
}
}
}
sccs
}
pub struct Reach {
pub scc_of: Vec<usize>,
pub scc_size: Vec<usize>,
pub vfi: Vec<u32>,
pub vfo: Vec<u32>,
}
fn condensation(adj: &[Vec<usize>], sccs: &[Vec<usize>]) -> (Vec<usize>, Vec<HashSet<usize>>) {
let mut scc_of = vec![0usize; adj.len()];
for (cid, comp) in sccs.iter().enumerate() {
for &node in comp {
scc_of[node] = cid;
}
}
let mut cond_fwd: Vec<HashSet<usize>> = vec![HashSet::new(); sccs.len()];
for (u, edges) in adj.iter().enumerate() {
let cu = scc_of[u];
for &v in edges {
let cv = scc_of[v];
if cu != cv {
cond_fwd[cu].insert(cv);
}
}
}
(scc_of, cond_fwd)
}
#[must_use]
pub fn reachability(adj: &[Vec<usize>], sccs: &[Vec<usize>]) -> Reach {
let n = adj.len();
let c = sccs.len();
let scc_size: Vec<usize> = sccs.iter().map(Vec::len).collect();
let (scc_of, cond_fwd) = condensation(adj, sccs);
let mut cond_rev: Vec<HashSet<usize>> = vec![HashSet::new(); c];
for (cu, succs) in cond_fwd.iter().enumerate() {
for &cv in succs {
cond_rev[cv].insert(cu);
}
}
let mut reach_fwd: Vec<HashSet<usize>> = vec![HashSet::new(); c];
for cid in 0..c {
let mut set = HashSet::new();
set.insert(cid);
for &succ in &cond_fwd[cid] {
for &r in &reach_fwd[succ] {
set.insert(r);
}
}
reach_fwd[cid] = set;
}
let mut reach_rev: Vec<HashSet<usize>> = vec![HashSet::new(); c];
for cid in (0..c).rev() {
let mut set = HashSet::new();
set.insert(cid);
for &pred in &cond_rev[cid] {
for &r in &reach_rev[pred] {
set.insert(r);
}
}
reach_rev[cid] = set;
}
let sum_sizes = |set: &HashSet<usize>| -> u32 {
u32::try_from(set.iter().map(|&r| scc_size[r]).sum::<usize>()).unwrap_or(u32::MAX)
};
let mut vfi = vec![0u32; n];
let mut vfo = vec![0u32; n];
for node in 0..n {
let cid = scc_of[node];
vfo[node] = sum_sizes(&reach_fwd[cid]);
vfi[node] = sum_sizes(&reach_rev[cid]);
}
Reach {
scc_of,
scc_size,
vfi,
vfo,
}
}
#[must_use]
pub fn topo_levels(adj: &[Vec<usize>], sccs: &[Vec<usize>]) -> Vec<u32> {
let n = adj.len();
let c = sccs.len();
let (scc_of, cond_fwd) = condensation(adj, sccs);
let mut scc_level = vec![0u32; c];
for cid in (0..c).rev() {
let lvl = scc_level[cid];
for &succ in &cond_fwd[cid] {
if scc_level[succ] < lvl + 1 {
scc_level[succ] = lvl + 1;
}
}
}
(0..n).map(|node| scc_level[scc_of[node]]).collect()
}
pub struct ReachIndex {
scc_of: Vec<usize>,
reach_fwd: Vec<HashSet<usize>>,
}
impl ReachIndex {
#[must_use]
pub fn connected(&self, a: usize, b: usize) -> bool {
let ca = self.scc_of[a];
let cb = self.scc_of[b];
self.reach_fwd[ca].contains(&cb) || self.reach_fwd[cb].contains(&ca)
}
}
#[must_use]
pub fn reach_index(adj: &[Vec<usize>], sccs: &[Vec<usize>]) -> ReachIndex {
let c = sccs.len();
let (scc_of, cond_fwd) = condensation(adj, sccs);
let mut reach_fwd: Vec<HashSet<usize>> = vec![HashSet::new(); c];
for cid in 0..c {
let mut set = HashSet::new();
set.insert(cid);
for &succ in &cond_fwd[cid] {
for &r in &reach_fwd[succ] {
set.insert(r);
}
}
reach_fwd[cid] = set;
}
ReachIndex { scc_of, reach_fwd }
}
pub struct GraphMetrics {
pub n: usize,
pub ccd: f64,
pub propagation_cost: f64,
pub cycle_count: u32,
pub largest_cycle: u32,
pub cyclic_nodes: u32,
}
#[must_use]
pub fn graph_metrics(graph: &ImportGraph) -> GraphMetrics {
let n = graph.len();
if n == 0 {
return GraphMetrics {
n: 0,
ccd: 0.0,
propagation_cost: 0.0,
cycle_count: 0,
largest_cycle: 0,
cyclic_nodes: 0,
};
}
let sccs = tarjan_scc(&graph.adj);
let reach = reachability(&graph.adj, &sccs);
let ccd: f64 = reach.vfo.iter().map(|&v| f64::from(v)).sum();
let n_f = f64::from(u32::try_from(n).unwrap_or(u32::MAX));
let propagation_cost = ccd / (n_f * n_f);
let mut cycle_count = 0u32;
let mut largest = 0usize;
let mut cyclic = 0usize;
for comp in &sccs {
if comp.len() >= 2 {
cycle_count += 1;
cyclic += comp.len();
largest = largest.max(comp.len());
}
}
GraphMetrics {
n,
ccd,
propagation_cost,
cycle_count,
largest_cycle: u32::try_from(largest).unwrap_or(u32::MAX),
cyclic_nodes: u32::try_from(cyclic).unwrap_or(u32::MAX),
}
}
#[cfg(test)]
mod tests {
use super::{reach_index, reachability, tarjan_scc, topo_levels};
use std::collections::BTreeSet;
fn normalize(sccs: Vec<Vec<usize>>) -> BTreeSet<Vec<usize>> {
sccs.into_iter()
.map(|mut c| {
c.sort_unstable();
c
})
.collect()
}
#[test]
fn empty_graph_has_no_components() {
assert!(tarjan_scc(&[]).is_empty());
}
#[test]
fn dag_yields_only_singletons() {
let adj = vec![vec![1, 2], vec![2], vec![]];
let got = normalize(tarjan_scc(&adj));
let want: BTreeSet<Vec<usize>> = [vec![0], vec![1], vec![2]].into_iter().collect();
assert_eq!(got, want);
}
#[test]
fn three_cycle_is_one_component() {
let adj = vec![vec![1], vec![2], vec![0]];
let got = normalize(tarjan_scc(&adj));
let want: BTreeSet<Vec<usize>> = [vec![0, 1, 2]].into_iter().collect();
assert_eq!(got, want);
}
#[test]
fn two_cycles_joined_by_a_bridge_stay_separate() {
let adj = vec![
vec![1], vec![0, 2], vec![3], vec![4], vec![3], ];
let got = normalize(tarjan_scc(&adj));
let want: BTreeSet<Vec<usize>> = [vec![0, 1], vec![2], vec![3, 4]].into_iter().collect();
assert_eq!(got, want);
}
#[test]
fn every_node_appears_in_exactly_one_component() {
let adj = vec![vec![1], vec![2, 0], vec![3], vec![1], vec![]];
let sccs = tarjan_scc(&adj);
let mut seen = vec![false; adj.len()];
let mut count = 0;
for comp in &sccs {
for &v in comp {
assert!(!seen[v], "node {v} appeared in two components");
seen[v] = true;
count += 1;
}
}
assert_eq!(count, adj.len(), "every node must be covered");
}
#[test]
fn reachability_on_a_chain() {
let adj = vec![vec![1], vec![2], vec![]];
let r = reachability(&adj, &tarjan_scc(&adj));
assert_eq!(r.vfo, vec![3, 2, 1]);
assert_eq!(r.vfi, vec![1, 2, 3]);
assert_eq!(r.vfo.iter().sum::<u32>(), 6);
}
#[test]
fn reachability_on_a_full_cycle_is_total() {
let adj = vec![vec![1], vec![2], vec![0]];
let r = reachability(&adj, &tarjan_scc(&adj));
assert_eq!(r.vfo, vec![3, 3, 3]);
assert_eq!(r.vfi, vec![3, 3, 3]);
assert_eq!(r.vfo.iter().sum::<u32>(), 9);
}
#[test]
fn topo_levels_on_a_chain() {
let adj = vec![vec![1], vec![2], vec![]];
assert_eq!(topo_levels(&adj, &tarjan_scc(&adj)), vec![0, 1, 2]);
}
#[test]
fn topo_levels_share_a_level_within_a_cycle() {
let adj = vec![vec![1], vec![0, 2], vec![3], vec![4], vec![3]];
assert_eq!(topo_levels(&adj, &tarjan_scc(&adj)), vec![0, 0, 1, 2, 2]);
}
#[test]
fn reach_index_pairwise_connectivity() {
let adj = vec![vec![1], vec![2], vec![], vec![]];
let idx = reach_index(&adj, &tarjan_scc(&adj));
assert!(idx.connected(0, 2), "0 reaches 2 transitively");
assert!(
idx.connected(2, 0),
"connected is symmetric (either direction)"
);
assert!(
!idx.connected(0, 3),
"nothing connects 0 and the isolated 3"
);
assert!(!idx.connected(2, 3), "no path between 2 and 3");
}
}