use petgraph::algo::tarjan_scc;
use petgraph::graph::{DiGraph, Graph, NodeIndex};
use petgraph::visit::EdgeRef;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use srcgraph_core::{ClassNode, EdgeKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SccStatus {
Ready,
Partial,
Entangled,
}
#[derive(Debug, Clone)]
pub struct SccInfo {
pub id: usize,
pub members: Vec<NodeIndex>,
pub status: SccStatus,
}
pub struct SccResult {
pub components: Vec<SccInfo>,
pub dag: DiGraph<usize, ()>,
}
pub fn rate_scc(size: usize) -> SccStatus {
match size {
1 => SccStatus::Ready,
2..=3 => SccStatus::Partial,
_ => SccStatus::Entangled,
}
}
pub fn compute_scc<N, E>(graph: &Graph<N, E>) -> SccResult
where
N: ClassNode,
E: EdgeKind,
{
let raw: Vec<Vec<NodeIndex>> = tarjan_scc(graph);
let mut node_to_scc: HashMap<NodeIndex, usize> = HashMap::with_capacity(graph.node_count());
let components: Vec<SccInfo> = raw
.into_iter()
.enumerate()
.map(|(i, members)| {
for &n in &members {
node_to_scc.insert(n, i);
}
SccInfo {
id: i,
status: rate_scc(members.len()),
members,
}
})
.collect();
let mut dag: DiGraph<usize, ()> = DiGraph::with_capacity(components.len(), 0);
let dag_nodes: Vec<_> = (0..components.len()).map(|i| dag.add_node(i)).collect();
let mut seen: HashSet<(usize, usize)> = HashSet::new();
for e in graph.edge_references() {
let s = node_to_scc[&e.source()];
let t = node_to_scc[&e.target()];
if s != t && seen.insert((s, t)) {
dag.add_edge(dag_nodes[s], dag_nodes[t], ());
}
}
SccResult { components, dag }
}
#[cfg(test)]
mod tests {
use super::*;
use srcgraph_core::{EdgeType, OwnedClassNode, OwnedGraph};
use petgraph::Graph;
fn node(id: &str) -> OwnedClassNode {
OwnedClassNode {
id: id.to_owned(),
name: id.split('.').last().unwrap_or(id).to_owned(),
namespace: "test".to_owned(),
line_count: 10,
method_count: 2,
halstead_eta1: 0,
halstead_eta2: 0,
halstead_n1: 0,
halstead_n2: 0,
method_connectivity: None,
method_fingerprints: None,
method_tokens: None,
call_sequences: None,
cyclomatic_complexity: None,
path_conditions: None,
invariants: None,
error_messages: None,
magic_numbers: None,
dead_code: None,
tenant_branches: None,
state_transitions: None,
}
}
#[test]
fn scc_linear_dag_all_ready() {
let mut g: OwnedGraph = Graph::new();
let a = g.add_node(node("A"));
let b = g.add_node(node("B"));
let c = g.add_node(node("C"));
g.add_edge(a, b, EdgeType::MethodCall);
g.add_edge(b, c, EdgeType::MethodCall);
let r = compute_scc(&g);
assert_eq!(r.components.len(), 3);
assert!(r.components.iter().all(|s| s.status == SccStatus::Ready));
assert!(r.components.iter().all(|s| s.members.len() == 1));
assert_eq!(r.dag.node_count(), 3);
assert_eq!(r.dag.edge_count(), 2);
}
#[test]
fn scc_three_cycle_partial() {
let mut g: OwnedGraph = Graph::new();
let a = g.add_node(node("A"));
let b = g.add_node(node("B"));
let c = g.add_node(node("C"));
g.add_edge(a, b, EdgeType::MethodCall);
g.add_edge(b, c, EdgeType::MethodCall);
g.add_edge(c, a, EdgeType::MethodCall);
let r = compute_scc(&g);
assert_eq!(r.components.len(), 1, "entire graph is one SCC");
assert_eq!(r.components[0].status, SccStatus::Partial);
assert_eq!(r.components[0].members.len(), 3);
assert_eq!(r.dag.edge_count(), 0);
}
#[test]
fn scc_large_cycle_entangled() {
let mut g: OwnedGraph = Graph::new();
let nodes: Vec<_> = (0..5).map(|i| g.add_node(node(&format!("N{i}")))).collect();
for i in 0..5 {
g.add_edge(nodes[i], nodes[(i + 1) % 5], EdgeType::MethodCall);
}
let r = compute_scc(&g);
assert_eq!(r.components.len(), 1);
assert_eq!(r.components[0].status, SccStatus::Entangled);
assert_eq!(r.components[0].members.len(), 5);
}
#[test]
fn scc_condensation_dag_shape() {
let mut g: OwnedGraph = Graph::new();
let a = g.add_node(node("A"));
let b = g.add_node(node("B"));
let c = g.add_node(node("C"));
let d = g.add_node(node("D"));
g.add_edge(a, b, EdgeType::MethodCall);
g.add_edge(b, a, EdgeType::MethodCall); g.add_edge(c, a, EdgeType::MethodCall); g.add_edge(d, c, EdgeType::MethodCall);
let r = compute_scc(&g);
assert_eq!(r.components.len(), 3, "3 SCCs: {{A,B}}, {{C}}, {{D}}");
let pair = r.components.iter().find(|s| s.members.len() == 2).expect("2-node SCC");
assert_eq!(pair.status, SccStatus::Partial);
assert_eq!(r.dag.node_count(), 3);
assert_eq!(r.dag.edge_count(), 2, "two inter-SCC edges");
}
#[test]
fn rate_scc_thresholds() {
assert_eq!(rate_scc(1), SccStatus::Ready);
assert_eq!(rate_scc(2), SccStatus::Partial);
assert_eq!(rate_scc(3), SccStatus::Partial);
assert_eq!(rate_scc(4), SccStatus::Entangled);
assert_eq!(rate_scc(100), SccStatus::Entangled);
}
}