1use petgraph::algo::tarjan_scc;
15use petgraph::graph::{DiGraph, NodeIndex};
16use petgraph::visit::EdgeFiltered;
17use std::collections::HashMap;
18
19use crate::graph::edge::EdgeData;
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.participates_in_scc()
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 for &node in &nodes {
95 node_to_component.insert(node, index);
96 }
97
98 components.push(Scc {
99 index,
100 nodes,
101 is_cyclic: false,
102 hint: DeployabilityHint::Independent,
103 });
104 }
105
106 let mut self_loops = vec![false; components.len()];
110 let mut has_outer_dependency = vec![false; components.len()];
111
112 for edge_idx in graph.edge_indices() {
113 let Some(weight) = graph.edge_weight(edge_idx) else {
114 continue;
115 };
116 if !weight.kind.participates_in_scc() {
117 continue;
118 }
119 let Some((source, target)) = graph.edge_endpoints(edge_idx) else {
120 continue;
121 };
122 let (Some(&source_component), Some(&target_component)) = (
123 node_to_component.get(&source),
124 node_to_component.get(&target),
125 ) else {
126 continue;
127 };
128
129 if source_component == target_component {
130 if source == target {
131 self_loops[source_component] = true;
132 }
133 } else {
134 has_outer_dependency[source_component] = true;
135 }
136 }
137
138 for component in &mut components {
139 let clustered = component.nodes.len() > 1;
140 let self_loop = self_loops[component.index];
141 component.is_cyclic = clustered || self_loop;
142 component.hint = if clustered {
143 DeployabilityHint::CyclicCluster
144 } else if self_loop {
145 DeployabilityHint::SelfLoop
146 } else if has_outer_dependency[component.index] {
147 DeployabilityHint::AcyclicDependency
148 } else {
149 DeployabilityHint::Independent
150 };
151 }
152
153 Self {
154 components,
155 node_to_component,
156 }
157 }
158
159 pub fn component_of(&self, node: NodeIndex) -> Option<usize> {
161 self.node_to_component.get(&node).copied()
162 }
163
164 pub fn mutually_dependent(&self, a: NodeIndex, b: NodeIndex) -> bool {
166 self.component_of(a) == self.component_of(b)
167 }
168
169 pub fn has_cycles(&self) -> bool {
171 self.components.iter().any(|c| c.is_cyclic)
172 }
173
174 pub fn cyclic_components(&self) -> impl Iterator<Item = &Scc> {
176 self.components.iter().filter(|c| c.is_cyclic)
177 }
178
179 pub fn acyclic_components(&self) -> impl Iterator<Item = &Scc> {
181 self.components.iter().filter(|c| !c.is_cyclic)
182 }
183
184 pub fn hint_counts(&self) -> HashMap<DeployabilityHint, usize> {
186 let mut counts = HashMap::new();
187 for comp in &self.components {
188 *counts.entry(comp.hint).or_insert(0) += 1;
189 }
190 counts
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use crate::graph::edge::EdgeKind;
198 use crate::graph::node::{FileNode, SymbolNode};
199 use crate::language::LangId;
200 use crate::model::{LineColumn, SourceRange, SymbolId, Visibility, ids::FileId};
201
202 fn make_source_range() -> SourceRange {
203 SourceRange {
204 byte_start: 0,
205 byte_end: 10,
206 start: LineColumn { line: 1, column: 0 },
207 end: LineColumn {
208 line: 1,
209 column: 10,
210 },
211 }
212 }
213
214 fn make_file_node(id: u32, path: &str) -> NodeData {
215 NodeData::File(FileNode {
216 id: FileId::new(id.max(1)).unwrap(),
217 path: std::path::PathBuf::from(path),
218 language: LangId::Rust,
219 snapshot_id: crate::model::ids::SnapshotId::new(1).unwrap(),
220 })
221 }
222
223 fn make_symbol_node(id: u32, name: &str, file_id: u32) -> NodeData {
224 NodeData::Symbol(SymbolNode {
225 id: SymbolId::new(id).unwrap(),
226 name: name.to_string(),
227 kind: crate::model::SymbolKind::Function,
228 file_id: FileId::new(file_id.max(1)).unwrap(),
229 visibility: Some(Visibility::Public),
230 source_range: make_source_range(),
231 })
232 }
233
234 fn make_edge(kind: EdgeKind) -> EdgeData {
235 EdgeData::new(kind)
236 }
237
238 #[test]
239 fn scc_single_node_no_edges() {
240 let mut graph = DiGraph::new();
241 let node = graph.add_node(make_symbol_node(1, "foo", 0));
242
243 let analysis = SccAnalysis::analyze(&graph);
244
245 assert_eq!(analysis.components.len(), 1);
246 assert_eq!(analysis.components[0].nodes, vec![node]);
247 assert!(!analysis.components[0].is_cyclic);
248 assert_eq!(analysis.components[0].hint, DeployabilityHint::Independent);
249 }
250
251 #[test]
252 fn scc_linear_chain() {
253 let mut graph = DiGraph::new();
254 let a = graph.add_node(make_symbol_node(1, "a", 0));
255 let b = graph.add_node(make_symbol_node(2, "b", 0));
256 let c = graph.add_node(make_symbol_node(3, "c", 0));
257
258 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
260 graph.add_edge(b, c, make_edge(EdgeKind::Reference));
261
262 let analysis = SccAnalysis::analyze(&graph);
263
264 assert_eq!(analysis.components.len(), 3);
265 assert!(!analysis.has_cycles());
266 assert!(analysis.cyclic_components().next().is_none());
267 }
268
269 #[test]
270 fn scc_simple_cycle() {
271 let mut graph = DiGraph::new();
272 let a = graph.add_node(make_symbol_node(1, "a", 0));
273 let b = graph.add_node(make_symbol_node(2, "b", 0));
274 let c = graph.add_node(make_symbol_node(3, "c", 0));
275
276 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
278 graph.add_edge(b, c, make_edge(EdgeKind::Reference));
279 graph.add_edge(c, a, make_edge(EdgeKind::Reference));
280
281 let analysis = SccAnalysis::analyze(&graph);
282
283 assert!(analysis.has_cycles());
284 let cyclic: Vec<_> = analysis.cyclic_components().collect();
285 assert_eq!(cyclic.len(), 1);
286 assert_eq!(cyclic[0].nodes.len(), 3);
287 assert_eq!(cyclic[0].hint, DeployabilityHint::CyclicCluster);
288 }
289
290 #[test]
291 fn scc_ownership_edges_excluded() {
292 let mut graph = DiGraph::new();
293 let file = graph.add_node(make_file_node(0, "test.rs"));
294 let sym = graph.add_node(make_symbol_node(1, "func", 0));
295
296 graph.add_edge(file, sym, make_edge(EdgeKind::Ownership));
298
299 let analysis = SccAnalysis::analyze(&graph);
300
301 assert_eq!(analysis.components.len(), 2);
303 assert!(!analysis.has_cycles());
304 }
305
306 #[test]
307 fn scc_self_loop_detected() {
308 let mut graph = DiGraph::new();
309 let a = graph.add_node(make_symbol_node(1, "a", 0));
310
311 graph.add_edge(a, a, make_edge(EdgeKind::Reference));
313
314 let analysis = SccAnalysis::analyze(&graph);
315
316 assert!(analysis.has_cycles());
317 let comp = &analysis.components[0];
318 assert!(comp.is_cyclic);
319 assert_eq!(comp.hint, DeployabilityHint::SelfLoop);
320 }
321
322 #[test]
323 fn scc_flow_self_loop_not_cyclic() {
324 let mut graph = DiGraph::new();
325 let a = graph.add_node(make_symbol_node(1, "a", 0));
326
327 graph.add_edge(a, a, make_edge(EdgeKind::Flow));
328
329 let analysis = SccAnalysis::analyze(&graph);
330
331 assert!(!analysis.has_cycles());
332 let comp = &analysis.components[0];
333 assert!(!comp.is_cyclic);
334 assert_eq!(comp.hint, DeployabilityHint::Independent);
335 }
336
337 #[test]
338 fn scc_flow_edge_not_external_dependency() {
339 let mut graph = DiGraph::new();
340 let a = graph.add_node(make_symbol_node(1, "a", 0));
341 let b = graph.add_node(make_symbol_node(2, "b", 0));
342
343 graph.add_edge(a, b, make_edge(EdgeKind::Flow));
345
346 let analysis = SccAnalysis::analyze(&graph);
347
348 assert_eq!(analysis.components.len(), 2);
349 assert!(!analysis.has_cycles());
350 for comp in &analysis.components {
351 assert_eq!(comp.hint, DeployabilityHint::Independent);
352 }
353 }
354
355 #[test]
356 fn scc_multiple_cycles() {
357 let mut graph = DiGraph::new();
358 let a = graph.add_node(make_symbol_node(1, "a", 0));
359 let b = graph.add_node(make_symbol_node(2, "b", 0));
360 let c = graph.add_node(make_symbol_node(3, "c", 0));
361 let d = graph.add_node(make_symbol_node(4, "d", 0));
362
363 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
365 graph.add_edge(b, a, make_edge(EdgeKind::Reference));
366
367 graph.add_edge(c, d, make_edge(EdgeKind::Reference));
369 graph.add_edge(d, c, make_edge(EdgeKind::Reference));
370
371 let analysis = SccAnalysis::analyze(&graph);
372
373 assert!(analysis.has_cycles());
374 let cyclic: Vec<_> = analysis.cyclic_components().collect();
375 assert_eq!(cyclic.len(), 2); let counts = analysis.hint_counts();
378 assert_eq!(counts.get(&DeployabilityHint::CyclicCluster), Some(&2));
379 }
380
381 #[test]
382 fn scc_mutual_dependence_check() {
383 let mut graph = DiGraph::new();
384 let a = graph.add_node(make_symbol_node(1, "a", 0));
385 let b = graph.add_node(make_symbol_node(2, "b", 0));
386 let c = graph.add_node(make_symbol_node(3, "c", 0));
387
388 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
390 graph.add_edge(b, a, make_edge(EdgeKind::Reference));
391
392 let analysis = SccAnalysis::analyze(&graph);
393
394 assert!(analysis.mutually_dependent(a, b));
395 assert!(!analysis.mutually_dependent(a, c));
396 assert!(!analysis.mutually_dependent(b, c));
397 }
398
399 #[test]
400 fn scc_component_lookup() {
401 let mut graph = DiGraph::new();
402 let a = graph.add_node(make_symbol_node(1, "a", 0));
403 let b = graph.add_node(make_symbol_node(2, "b", 0));
404
405 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
406
407 let analysis = SccAnalysis::analyze(&graph);
408
409 let comp_a = analysis.component_of(a);
410 let comp_b = analysis.component_of(b);
411 assert!(comp_a.is_some());
412 assert!(comp_b.is_some());
413 }
414
415 #[test]
416 fn scc_topological_order_dependencies_first() {
417 let mut graph = DiGraph::new();
418 let a = graph.add_node(make_symbol_node(1, "a", 0));
419 let b = graph.add_node(make_symbol_node(2, "b", 0));
420 let c = graph.add_node(make_symbol_node(3, "c", 0));
421
422 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
424 graph.add_edge(b, c, make_edge(EdgeKind::Reference));
425
426 let analysis = SccAnalysis::analyze(&graph);
427
428 let indices: Vec<_> = analysis
432 .components
433 .iter()
434 .map(|c| {
435 c.nodes
436 .first()
437 .map(|n| n.index())
438 .expect("Component has nodes")
439 })
440 .collect();
441
442 assert_eq!(indices.len(), 3);
444 }
445
446 #[test]
447 fn scc_diamond_structure() {
448 let mut graph = DiGraph::new();
449 let a = graph.add_node(make_symbol_node(1, "a", 0));
450 let b = graph.add_node(make_symbol_node(2, "b", 0));
451 let c = graph.add_node(make_symbol_node(3, "c", 0));
452 let d = graph.add_node(make_symbol_node(4, "d", 0));
453
454 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
456 graph.add_edge(a, c, make_edge(EdgeKind::Reference));
457 graph.add_edge(b, d, make_edge(EdgeKind::Reference));
458 graph.add_edge(c, d, make_edge(EdgeKind::Reference));
459
460 let analysis = SccAnalysis::analyze(&graph);
461
462 assert!(!analysis.has_cycles());
463 assert_eq!(analysis.components.len(), 4);
464 }
465
466 #[test]
467 fn deployability_hint_display() {
468 assert_eq!(format!("{}", DeployabilityHint::Independent), "independent");
469 assert_eq!(
470 format!("{}", DeployabilityHint::CyclicCluster),
471 "cyclic_cluster"
472 );
473 assert_eq!(format!("{}", DeployabilityHint::SelfLoop), "self_loop");
474 assert_eq!(
475 format!("{}", DeployabilityHint::AcyclicDependency),
476 "acyclic_dependency"
477 );
478 }
479
480 #[test]
483 fn hint_counts_are_stable_for_a_mixed_graph() {
484 let mut graph = DiGraph::new();
485 let self_loop = graph.add_node(make_symbol_node(1, "self_loop", 0));
486 let cycle_left = graph.add_node(make_symbol_node(2, "cycle_left", 0));
487 let cycle_right = graph.add_node(make_symbol_node(3, "cycle_right", 0));
488 let dependent = graph.add_node(make_symbol_node(4, "dependent", 0));
489 let standalone = graph.add_node(make_symbol_node(5, "standalone", 0));
490
491 graph.add_edge(self_loop, self_loop, make_edge(EdgeKind::Reference));
492 graph.add_edge(cycle_left, cycle_right, make_edge(EdgeKind::Import));
493 graph.add_edge(cycle_right, cycle_left, make_edge(EdgeKind::Import));
494 graph.add_edge(dependent, cycle_left, make_edge(EdgeKind::Reference));
495 graph.add_edge(standalone, self_loop, make_edge(EdgeKind::Ownership));
496
497 let analysis = SccAnalysis::analyze(&graph);
498 let counts = analysis.hint_counts();
499
500 assert_eq!(
501 analysis.components.len(),
502 4,
503 "the cycle merges two nodes, the other three stay single"
504 );
505 assert_eq!(counts.get(&DeployabilityHint::SelfLoop), Some(&1));
506 assert_eq!(counts.get(&DeployabilityHint::CyclicCluster), Some(&1));
507 assert_eq!(counts.get(&DeployabilityHint::AcyclicDependency), Some(&1));
508 assert_eq!(
509 counts.get(&DeployabilityHint::Independent),
510 Some(&1),
511 "the node whose only edge is ownership has no dependency"
512 );
513 }
514}