1use petgraph::algo::tarjan_scc;
15use petgraph::graph::{DiGraph, NodeIndex};
16use petgraph::visit::EdgeFiltered;
17use std::collections::HashMap;
18
19use crate::graph::edge::{EdgeData, EdgeKind};
20use crate::graph::node::NodeData;
21
22#[derive(Debug, Clone)]
24pub struct Scc {
25 pub index: usize,
27 pub nodes: Vec<NodeIndex>,
29 pub is_cyclic: bool,
31 pub hint: DeployabilityHint,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37#[non_exhaustive]
38pub enum DeployabilityHint {
39 Independent,
41 AcyclicDependency,
43 CyclicCluster,
45 SelfLoop,
47}
48
49impl std::fmt::Display for DeployabilityHint {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 match self {
52 DeployabilityHint::Independent => write!(f, "independent"),
53 DeployabilityHint::AcyclicDependency => write!(f, "acyclic_dependency"),
54 DeployabilityHint::CyclicCluster => write!(f, "cyclic_cluster"),
55 DeployabilityHint::SelfLoop => write!(f, "self_loop"),
56 }
57 }
58}
59
60#[derive(Debug, Clone)]
62pub struct SccAnalysis {
63 pub components: Vec<Scc>,
65 pub node_to_component: HashMap<NodeIndex, usize>,
67}
68
69impl SccAnalysis {
70 pub fn analyze(graph: &DiGraph<NodeData, EdgeData>) -> Self {
78 let dep_view = EdgeFiltered::from_fn(
80 graph,
81 |edge: petgraph::graph::EdgeReference<'_, EdgeData>| {
82 edge.weight().kind != EdgeKind::Ownership
83 },
84 );
85
86 let scc_groups = tarjan_scc(&dep_view);
89
90 let mut components = Vec::with_capacity(scc_groups.len());
91 let mut node_to_component = HashMap::new();
92
93 for (index, nodes) in scc_groups.into_iter().enumerate() {
94 let has_self_loop = nodes.iter().any(|&node| {
95 graph
96 .neighbors_directed(node, petgraph::Direction::Outgoing)
97 .any(|neighbor| neighbor == node)
98 });
99
100 let is_cyclic = nodes.len() > 1 || has_self_loop;
101
102 let hint = if nodes.len() > 1 {
103 DeployabilityHint::CyclicCluster
104 } else if has_self_loop {
105 DeployabilityHint::SelfLoop
106 } else if nodes.len() == 1 {
107 DeployabilityHint::AcyclicDependency
108 } else {
109 DeployabilityHint::Independent
110 };
111
112 for &node in &nodes {
113 node_to_component.insert(node, index);
114 }
115
116 components.push(Scc {
117 index,
118 nodes,
119 is_cyclic,
120 hint,
121 });
122 }
123
124 Self::classify_independence(graph, &mut components, &node_to_component);
125
126 Self {
127 components,
128 node_to_component,
129 }
130 }
131
132 fn classify_independence(
135 graph: &DiGraph<NodeData, EdgeData>,
136 components: &mut [Scc],
137 node_to_component: &HashMap<NodeIndex, usize>,
138 ) {
139 let mut component_deps: HashMap<usize, Vec<usize>> = HashMap::new();
140
141 for edge_idx in graph.edge_indices() {
142 let Some(weight) = graph.edge_weight(edge_idx) else {
143 continue;
144 };
145 if weight.kind == EdgeKind::Ownership {
146 continue;
147 }
148 let Some((source, target)) = graph.edge_endpoints(edge_idx) else {
149 continue;
150 };
151 let source_comp = node_to_component.get(&source);
152 let target_comp = node_to_component.get(&target);
153
154 if let (Some(&s), Some(&t)) = (source_comp, target_comp)
155 && s != t
156 {
157 component_deps.entry(s).or_default().push(t);
158 }
159 }
160
161 for comp in components.iter_mut() {
163 if comp.hint == DeployabilityHint::AcyclicDependency {
164 let has_external_deps = component_deps
165 .get(&comp.index)
166 .map(|deps| !deps.is_empty())
167 .unwrap_or(false);
168
169 if !has_external_deps {
170 comp.hint = DeployabilityHint::Independent;
171 }
172 }
173 }
174 }
175
176 pub fn component_of(&self, node: NodeIndex) -> Option<usize> {
178 self.node_to_component.get(&node).copied()
179 }
180
181 pub fn mutually_dependent(&self, a: NodeIndex, b: NodeIndex) -> bool {
183 self.component_of(a) == self.component_of(b)
184 }
185
186 pub fn has_cycles(&self) -> bool {
188 self.components.iter().any(|c| c.is_cyclic)
189 }
190
191 pub fn cyclic_components(&self) -> impl Iterator<Item = &Scc> {
193 self.components.iter().filter(|c| c.is_cyclic)
194 }
195
196 pub fn acyclic_components(&self) -> impl Iterator<Item = &Scc> {
198 self.components.iter().filter(|c| !c.is_cyclic)
199 }
200
201 pub fn hint_counts(&self) -> HashMap<DeployabilityHint, usize> {
203 let mut counts = HashMap::new();
204 for comp in &self.components {
205 *counts.entry(comp.hint).or_insert(0) += 1;
206 }
207 counts
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use crate::graph::node::{FileNode, SymbolNode};
215 use crate::language::LangId;
216 use crate::model::{LineColumn, SourceRange, SymbolId, Visibility, ids::FileId};
217
218 fn make_source_range() -> SourceRange {
219 SourceRange {
220 byte_start: 0,
221 byte_end: 10,
222 start: LineColumn { line: 1, column: 0 },
223 end: LineColumn {
224 line: 1,
225 column: 10,
226 },
227 }
228 }
229
230 fn make_file_node(id: u32, path: &str) -> NodeData {
231 NodeData::File(FileNode {
232 id: FileId(id),
233 path: std::path::PathBuf::from(path),
234 language: LangId::Rust,
235 snapshot_id: crate::model::ids::SnapshotId(1),
236 })
237 }
238
239 fn make_symbol_node(id: u32, name: &str, file_id: u32) -> NodeData {
240 NodeData::Symbol(SymbolNode {
241 id: SymbolId(id),
242 name: name.to_string(),
243 kind: crate::model::SymbolKind::Function,
244 file_id: FileId(file_id),
245 visibility: Some(Visibility::Public),
246 source_range: make_source_range(),
247 })
248 }
249
250 fn make_edge(kind: EdgeKind) -> EdgeData {
251 EdgeData {
252 kind,
253 confidence: 1.0,
254 }
255 }
256
257 #[test]
258 fn scc_single_node_no_edges() {
259 let mut graph = DiGraph::new();
260 let node = graph.add_node(make_symbol_node(1, "foo", 0));
261
262 let analysis = SccAnalysis::analyze(&graph);
263
264 assert_eq!(analysis.components.len(), 1);
265 assert_eq!(analysis.components[0].nodes, vec![node]);
266 assert!(!analysis.components[0].is_cyclic);
267 assert_eq!(analysis.components[0].hint, DeployabilityHint::Independent);
268 }
269
270 #[test]
271 fn scc_linear_chain() {
272 let mut graph = DiGraph::new();
273 let a = graph.add_node(make_symbol_node(1, "a", 0));
274 let b = graph.add_node(make_symbol_node(2, "b", 0));
275 let c = graph.add_node(make_symbol_node(3, "c", 0));
276
277 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
279 graph.add_edge(b, c, make_edge(EdgeKind::Reference));
280
281 let analysis = SccAnalysis::analyze(&graph);
282
283 assert_eq!(analysis.components.len(), 3);
284 assert!(!analysis.has_cycles());
285 assert!(analysis.cyclic_components().next().is_none());
286 }
287
288 #[test]
289 fn scc_simple_cycle() {
290 let mut graph = DiGraph::new();
291 let a = graph.add_node(make_symbol_node(1, "a", 0));
292 let b = graph.add_node(make_symbol_node(2, "b", 0));
293 let c = graph.add_node(make_symbol_node(3, "c", 0));
294
295 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
297 graph.add_edge(b, c, make_edge(EdgeKind::Reference));
298 graph.add_edge(c, a, make_edge(EdgeKind::Reference));
299
300 let analysis = SccAnalysis::analyze(&graph);
301
302 assert!(analysis.has_cycles());
303 let cyclic: Vec<_> = analysis.cyclic_components().collect();
304 assert_eq!(cyclic.len(), 1);
305 assert_eq!(cyclic[0].nodes.len(), 3);
306 assert_eq!(cyclic[0].hint, DeployabilityHint::CyclicCluster);
307 }
308
309 #[test]
310 fn scc_ownership_edges_excluded() {
311 let mut graph = DiGraph::new();
312 let file = graph.add_node(make_file_node(0, "test.rs"));
313 let sym = graph.add_node(make_symbol_node(1, "func", 0));
314
315 graph.add_edge(file, sym, make_edge(EdgeKind::Ownership));
317
318 let analysis = SccAnalysis::analyze(&graph);
319
320 assert_eq!(analysis.components.len(), 2);
322 assert!(!analysis.has_cycles());
323 }
324
325 #[test]
326 fn scc_self_loop_detected() {
327 let mut graph = DiGraph::new();
328 let a = graph.add_node(make_symbol_node(1, "a", 0));
329
330 graph.add_edge(a, a, make_edge(EdgeKind::Reference));
332
333 let analysis = SccAnalysis::analyze(&graph);
334
335 assert!(analysis.has_cycles());
336 let comp = &analysis.components[0];
337 assert!(comp.is_cyclic);
338 assert_eq!(comp.hint, DeployabilityHint::SelfLoop);
339 }
340
341 #[test]
342 fn scc_multiple_cycles() {
343 let mut graph = DiGraph::new();
344 let a = graph.add_node(make_symbol_node(1, "a", 0));
345 let b = graph.add_node(make_symbol_node(2, "b", 0));
346 let c = graph.add_node(make_symbol_node(3, "c", 0));
347 let d = graph.add_node(make_symbol_node(4, "d", 0));
348
349 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
351 graph.add_edge(b, a, make_edge(EdgeKind::Reference));
352
353 graph.add_edge(c, d, make_edge(EdgeKind::Reference));
355 graph.add_edge(d, c, make_edge(EdgeKind::Reference));
356
357 let analysis = SccAnalysis::analyze(&graph);
358
359 assert!(analysis.has_cycles());
360 let cyclic: Vec<_> = analysis.cyclic_components().collect();
361 assert_eq!(cyclic.len(), 2); let counts = analysis.hint_counts();
364 assert_eq!(counts.get(&DeployabilityHint::CyclicCluster), Some(&2));
365 }
366
367 #[test]
368 fn scc_mutual_dependence_check() {
369 let mut graph = DiGraph::new();
370 let a = graph.add_node(make_symbol_node(1, "a", 0));
371 let b = graph.add_node(make_symbol_node(2, "b", 0));
372 let c = graph.add_node(make_symbol_node(3, "c", 0));
373
374 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
376 graph.add_edge(b, a, make_edge(EdgeKind::Reference));
377
378 let analysis = SccAnalysis::analyze(&graph);
379
380 assert!(analysis.mutually_dependent(a, b));
381 assert!(!analysis.mutually_dependent(a, c));
382 assert!(!analysis.mutually_dependent(b, c));
383 }
384
385 #[test]
386 fn scc_component_lookup() {
387 let mut graph = DiGraph::new();
388 let a = graph.add_node(make_symbol_node(1, "a", 0));
389 let b = graph.add_node(make_symbol_node(2, "b", 0));
390
391 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
392
393 let analysis = SccAnalysis::analyze(&graph);
394
395 let comp_a = analysis.component_of(a);
396 let comp_b = analysis.component_of(b);
397 assert!(comp_a.is_some());
398 assert!(comp_b.is_some());
399 }
400
401 #[test]
402 fn scc_topological_order_dependencies_first() {
403 let mut graph = DiGraph::new();
404 let a = graph.add_node(make_symbol_node(1, "a", 0));
405 let b = graph.add_node(make_symbol_node(2, "b", 0));
406 let c = graph.add_node(make_symbol_node(3, "c", 0));
407
408 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
410 graph.add_edge(b, c, make_edge(EdgeKind::Reference));
411
412 let analysis = SccAnalysis::analyze(&graph);
413
414 let indices: Vec<_> = analysis
418 .components
419 .iter()
420 .map(|c| {
421 c.nodes
422 .first()
423 .map(|n| n.index())
424 .expect("Component has nodes")
425 })
426 .collect();
427
428 assert_eq!(indices.len(), 3);
430 }
431
432 #[test]
433 fn scc_diamond_structure() {
434 let mut graph = DiGraph::new();
435 let a = graph.add_node(make_symbol_node(1, "a", 0));
436 let b = graph.add_node(make_symbol_node(2, "b", 0));
437 let c = graph.add_node(make_symbol_node(3, "c", 0));
438 let d = graph.add_node(make_symbol_node(4, "d", 0));
439
440 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
442 graph.add_edge(a, c, make_edge(EdgeKind::Reference));
443 graph.add_edge(b, d, make_edge(EdgeKind::Reference));
444 graph.add_edge(c, d, make_edge(EdgeKind::Reference));
445
446 let analysis = SccAnalysis::analyze(&graph);
447
448 assert!(!analysis.has_cycles());
449 assert_eq!(analysis.components.len(), 4);
450 }
451
452 #[test]
453 fn deployability_hint_display() {
454 assert_eq!(format!("{}", DeployabilityHint::Independent), "independent");
455 assert_eq!(
456 format!("{}", DeployabilityHint::CyclicCluster),
457 "cyclic_cluster"
458 );
459 assert_eq!(format!("{}", DeployabilityHint::SelfLoop), "self_loop");
460 assert_eq!(
461 format!("{}", DeployabilityHint::AcyclicDependency),
462 "acyclic_dependency"
463 );
464 }
465}