1use petgraph::algo::tarjan_scc;
15use petgraph::graph::{DiGraph, Graph, NodeIndex};
16use petgraph::visit::EdgeRef;
17use serde::{Deserialize, Serialize};
18use std::collections::{HashMap, HashSet};
19
20use srcgraph_core::{ClassNode, EdgeKind};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum SccStatus {
28 Ready,
30 Partial,
32 Entangled,
34}
35
36#[derive(Debug, Clone)]
38pub struct SccInfo {
39 pub id: usize,
41 pub members: Vec<NodeIndex>,
43 pub status: SccStatus,
44}
45
46pub struct SccResult {
48 pub components: Vec<SccInfo>,
50 pub dag: DiGraph<usize, ()>,
53}
54
55pub fn rate_scc(size: usize) -> SccStatus {
57 match size {
58 1 => SccStatus::Ready,
59 2..=3 => SccStatus::Partial,
60 _ => SccStatus::Entangled,
61 }
62}
63
64pub fn compute_scc<N, E>(graph: &Graph<N, E>) -> SccResult
70where
71 N: ClassNode,
72 E: EdgeKind,
73{
74 let raw: Vec<Vec<NodeIndex>> = tarjan_scc(graph);
75
76 let mut node_to_scc: HashMap<NodeIndex, usize> = HashMap::with_capacity(graph.node_count());
78 let components: Vec<SccInfo> = raw
79 .into_iter()
80 .enumerate()
81 .map(|(i, members)| {
82 for &n in &members {
83 node_to_scc.insert(n, i);
84 }
85 SccInfo {
86 id: i,
87 status: rate_scc(members.len()),
88 members,
89 }
90 })
91 .collect();
92
93 let mut dag: DiGraph<usize, ()> = DiGraph::with_capacity(components.len(), 0);
95 let dag_nodes: Vec<_> = (0..components.len()).map(|i| dag.add_node(i)).collect();
96
97 let mut seen: HashSet<(usize, usize)> = HashSet::new();
98 for e in graph.edge_references() {
99 let s = node_to_scc[&e.source()];
100 let t = node_to_scc[&e.target()];
101 if s != t && seen.insert((s, t)) {
102 dag.add_edge(dag_nodes[s], dag_nodes[t], ());
103 }
104 }
105
106 SccResult { components, dag }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use srcgraph_core::{EdgeType, OwnedClassNode, OwnedGraph};
113 use petgraph::Graph;
114
115 fn node(id: &str) -> OwnedClassNode {
116 OwnedClassNode {
117 id: id.to_owned(),
118 name: id.split('.').last().unwrap_or(id).to_owned(),
119 namespace: "test".to_owned(),
120 line_count: 10,
121 method_count: 2,
122 halstead_eta1: 0,
123 halstead_eta2: 0,
124 halstead_n1: 0,
125 halstead_n2: 0,
126 method_connectivity: None,
127 method_fingerprints: None,
128 method_tokens: None,
129 call_sequences: None,
130 cyclomatic_complexity: None,
131 path_conditions: None,
132 invariants: None,
133 error_messages: None,
134 magic_numbers: None,
135 dead_code: None,
136 tenant_branches: None,
137 state_transitions: None,
138 }
139 }
140
141 #[test]
142 fn scc_linear_dag_all_ready() {
143 let mut g: OwnedGraph = Graph::new();
145 let a = g.add_node(node("A"));
146 let b = g.add_node(node("B"));
147 let c = g.add_node(node("C"));
148 g.add_edge(a, b, EdgeType::MethodCall);
149 g.add_edge(b, c, EdgeType::MethodCall);
150
151 let r = compute_scc(&g);
152
153 assert_eq!(r.components.len(), 3);
154 assert!(r.components.iter().all(|s| s.status == SccStatus::Ready));
155 assert!(r.components.iter().all(|s| s.members.len() == 1));
156 assert_eq!(r.dag.node_count(), 3);
158 assert_eq!(r.dag.edge_count(), 2);
159 }
160
161 #[test]
162 fn scc_three_cycle_partial() {
163 let mut g: OwnedGraph = Graph::new();
165 let a = g.add_node(node("A"));
166 let b = g.add_node(node("B"));
167 let c = g.add_node(node("C"));
168 g.add_edge(a, b, EdgeType::MethodCall);
169 g.add_edge(b, c, EdgeType::MethodCall);
170 g.add_edge(c, a, EdgeType::MethodCall);
171
172 let r = compute_scc(&g);
173
174 assert_eq!(r.components.len(), 1, "entire graph is one SCC");
175 assert_eq!(r.components[0].status, SccStatus::Partial);
176 assert_eq!(r.components[0].members.len(), 3);
177 assert_eq!(r.dag.edge_count(), 0);
179 }
180
181 #[test]
182 fn scc_large_cycle_entangled() {
183 let mut g: OwnedGraph = Graph::new();
185 let nodes: Vec<_> = (0..5).map(|i| g.add_node(node(&format!("N{i}")))).collect();
186 for i in 0..5 {
187 g.add_edge(nodes[i], nodes[(i + 1) % 5], EdgeType::MethodCall);
188 }
189
190 let r = compute_scc(&g);
191
192 assert_eq!(r.components.len(), 1);
193 assert_eq!(r.components[0].status, SccStatus::Entangled);
194 assert_eq!(r.components[0].members.len(), 5);
195 }
196
197 #[test]
198 fn scc_condensation_dag_shape() {
199 let mut g: OwnedGraph = Graph::new();
203 let a = g.add_node(node("A"));
204 let b = g.add_node(node("B"));
205 let c = g.add_node(node("C"));
206 let d = g.add_node(node("D"));
207 g.add_edge(a, b, EdgeType::MethodCall);
208 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);
213
214 assert_eq!(r.components.len(), 3, "3 SCCs: {{A,B}}, {{C}}, {{D}}");
215
216 let pair = r.components.iter().find(|s| s.members.len() == 2).expect("2-node SCC");
217 assert_eq!(pair.status, SccStatus::Partial);
218
219 assert_eq!(r.dag.node_count(), 3);
220 assert_eq!(r.dag.edge_count(), 2, "two inter-SCC edges");
221 }
222
223 #[test]
224 fn rate_scc_thresholds() {
225 assert_eq!(rate_scc(1), SccStatus::Ready);
226 assert_eq!(rate_scc(2), SccStatus::Partial);
227 assert_eq!(rate_scc(3), SccStatus::Partial);
228 assert_eq!(rate_scc(4), SccStatus::Entangled);
229 assert_eq!(rate_scc(100), SccStatus::Entangled);
230 }
231}