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