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.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| {
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::new(id.max(1)).unwrap(),
233 path: std::path::PathBuf::from(path),
234 language: LangId::Rust,
235 snapshot_id: crate::model::ids::SnapshotId::new(1).unwrap(),
236 })
237 }
238
239 fn make_symbol_node(id: u32, name: &str, file_id: u32) -> NodeData {
240 NodeData::Symbol(SymbolNode {
241 id: SymbolId::new(id).unwrap(),
242 name: name.to_string(),
243 kind: crate::model::SymbolKind::Function,
244 file_id: FileId::new(file_id.max(1)).unwrap(),
245 visibility: Some(Visibility::Public),
246 source_range: make_source_range(),
247 })
248 }
249
250 fn make_edge(kind: EdgeKind) -> EdgeData {
251 EdgeData::new(kind)
252 }
253
254 #[test]
255 fn scc_single_node_no_edges() {
256 let mut graph = DiGraph::new();
257 let node = graph.add_node(make_symbol_node(1, "foo", 0));
258
259 let analysis = SccAnalysis::analyze(&graph);
260
261 assert_eq!(analysis.components.len(), 1);
262 assert_eq!(analysis.components[0].nodes, vec![node]);
263 assert!(!analysis.components[0].is_cyclic);
264 assert_eq!(analysis.components[0].hint, DeployabilityHint::Independent);
265 }
266
267 #[test]
268 fn scc_linear_chain() {
269 let mut graph = DiGraph::new();
270 let a = graph.add_node(make_symbol_node(1, "a", 0));
271 let b = graph.add_node(make_symbol_node(2, "b", 0));
272 let c = graph.add_node(make_symbol_node(3, "c", 0));
273
274 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
276 graph.add_edge(b, c, make_edge(EdgeKind::Reference));
277
278 let analysis = SccAnalysis::analyze(&graph);
279
280 assert_eq!(analysis.components.len(), 3);
281 assert!(!analysis.has_cycles());
282 assert!(analysis.cyclic_components().next().is_none());
283 }
284
285 #[test]
286 fn scc_simple_cycle() {
287 let mut graph = DiGraph::new();
288 let a = graph.add_node(make_symbol_node(1, "a", 0));
289 let b = graph.add_node(make_symbol_node(2, "b", 0));
290 let c = graph.add_node(make_symbol_node(3, "c", 0));
291
292 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
294 graph.add_edge(b, c, make_edge(EdgeKind::Reference));
295 graph.add_edge(c, a, make_edge(EdgeKind::Reference));
296
297 let analysis = SccAnalysis::analyze(&graph);
298
299 assert!(analysis.has_cycles());
300 let cyclic: Vec<_> = analysis.cyclic_components().collect();
301 assert_eq!(cyclic.len(), 1);
302 assert_eq!(cyclic[0].nodes.len(), 3);
303 assert_eq!(cyclic[0].hint, DeployabilityHint::CyclicCluster);
304 }
305
306 #[test]
307 fn scc_ownership_edges_excluded() {
308 let mut graph = DiGraph::new();
309 let file = graph.add_node(make_file_node(0, "test.rs"));
310 let sym = graph.add_node(make_symbol_node(1, "func", 0));
311
312 graph.add_edge(file, sym, make_edge(EdgeKind::Ownership));
314
315 let analysis = SccAnalysis::analyze(&graph);
316
317 assert_eq!(analysis.components.len(), 2);
319 assert!(!analysis.has_cycles());
320 }
321
322 #[test]
323 fn scc_self_loop_detected() {
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::Reference));
329
330 let analysis = SccAnalysis::analyze(&graph);
331
332 assert!(analysis.has_cycles());
333 let comp = &analysis.components[0];
334 assert!(comp.is_cyclic);
335 assert_eq!(comp.hint, DeployabilityHint::SelfLoop);
336 }
337
338 #[test]
339 fn scc_multiple_cycles() {
340 let mut graph = DiGraph::new();
341 let a = graph.add_node(make_symbol_node(1, "a", 0));
342 let b = graph.add_node(make_symbol_node(2, "b", 0));
343 let c = graph.add_node(make_symbol_node(3, "c", 0));
344 let d = graph.add_node(make_symbol_node(4, "d", 0));
345
346 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
348 graph.add_edge(b, a, make_edge(EdgeKind::Reference));
349
350 graph.add_edge(c, d, make_edge(EdgeKind::Reference));
352 graph.add_edge(d, c, make_edge(EdgeKind::Reference));
353
354 let analysis = SccAnalysis::analyze(&graph);
355
356 assert!(analysis.has_cycles());
357 let cyclic: Vec<_> = analysis.cyclic_components().collect();
358 assert_eq!(cyclic.len(), 2); let counts = analysis.hint_counts();
361 assert_eq!(counts.get(&DeployabilityHint::CyclicCluster), Some(&2));
362 }
363
364 #[test]
365 fn scc_mutual_dependence_check() {
366 let mut graph = DiGraph::new();
367 let a = graph.add_node(make_symbol_node(1, "a", 0));
368 let b = graph.add_node(make_symbol_node(2, "b", 0));
369 let c = graph.add_node(make_symbol_node(3, "c", 0));
370
371 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
373 graph.add_edge(b, a, make_edge(EdgeKind::Reference));
374
375 let analysis = SccAnalysis::analyze(&graph);
376
377 assert!(analysis.mutually_dependent(a, b));
378 assert!(!analysis.mutually_dependent(a, c));
379 assert!(!analysis.mutually_dependent(b, c));
380 }
381
382 #[test]
383 fn scc_component_lookup() {
384 let mut graph = DiGraph::new();
385 let a = graph.add_node(make_symbol_node(1, "a", 0));
386 let b = graph.add_node(make_symbol_node(2, "b", 0));
387
388 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
389
390 let analysis = SccAnalysis::analyze(&graph);
391
392 let comp_a = analysis.component_of(a);
393 let comp_b = analysis.component_of(b);
394 assert!(comp_a.is_some());
395 assert!(comp_b.is_some());
396 }
397
398 #[test]
399 fn scc_topological_order_dependencies_first() {
400 let mut graph = DiGraph::new();
401 let a = graph.add_node(make_symbol_node(1, "a", 0));
402 let b = graph.add_node(make_symbol_node(2, "b", 0));
403 let c = graph.add_node(make_symbol_node(3, "c", 0));
404
405 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
407 graph.add_edge(b, c, make_edge(EdgeKind::Reference));
408
409 let analysis = SccAnalysis::analyze(&graph);
410
411 let indices: Vec<_> = analysis
415 .components
416 .iter()
417 .map(|c| {
418 c.nodes
419 .first()
420 .map(|n| n.index())
421 .expect("Component has nodes")
422 })
423 .collect();
424
425 assert_eq!(indices.len(), 3);
427 }
428
429 #[test]
430 fn scc_diamond_structure() {
431 let mut graph = DiGraph::new();
432 let a = graph.add_node(make_symbol_node(1, "a", 0));
433 let b = graph.add_node(make_symbol_node(2, "b", 0));
434 let c = graph.add_node(make_symbol_node(3, "c", 0));
435 let d = graph.add_node(make_symbol_node(4, "d", 0));
436
437 graph.add_edge(a, b, make_edge(EdgeKind::Reference));
439 graph.add_edge(a, c, make_edge(EdgeKind::Reference));
440 graph.add_edge(b, d, make_edge(EdgeKind::Reference));
441 graph.add_edge(c, d, make_edge(EdgeKind::Reference));
442
443 let analysis = SccAnalysis::analyze(&graph);
444
445 assert!(!analysis.has_cycles());
446 assert_eq!(analysis.components.len(), 4);
447 }
448
449 #[test]
450 fn deployability_hint_display() {
451 assert_eq!(format!("{}", DeployabilityHint::Independent), "independent");
452 assert_eq!(
453 format!("{}", DeployabilityHint::CyclicCluster),
454 "cyclic_cluster"
455 );
456 assert_eq!(format!("{}", DeployabilityHint::SelfLoop), "self_loop");
457 assert_eq!(
458 format!("{}", DeployabilityHint::AcyclicDependency),
459 "acyclic_dependency"
460 );
461 }
462}