Skip to main content

meta_ast/graph/
scc.rs

1//! SCC (Strongly Connected Components) analysis for dependency graphs.
2//!
3//! This module implements Tarjan's SCC algorithm on the dependency subgraph
4//! (Import and Reference edges only; Ownership edges are excluded via
5//! `EdgeFiltered`).
6//!
7//! ## SCC as atomic deployment unit
8//!
9//! An SCC entirely within one language is never subdivided regardless of
10//! size. Cross-language SCCs may be split at the lowest-confidence edge
11//! (see `deploy::cut`), but same-language SCCs are always kept together.
12//! This guarantees that cycles - a known source of tight coupling - are
13//! preserved as a single deployment unit whenever possible.
14use 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/// A single strongly connected component.
23#[derive(Debug, Clone)]
24pub struct Scc {
25    /// Index of this component in topological order (dependencies first)
26    pub index: usize,
27    /// Node indices in this component
28    pub nodes: Vec<NodeIndex>,
29    /// Whether this component is cyclic (size > 1 or self-loop)
30    pub is_cyclic: bool,
31    /// Deployability recommendation
32    pub hint: DeployabilityHint,
33}
34
35/// Deployability classification for an SCC.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37#[non_exhaustive]
38pub enum DeployabilityHint {
39    /// Single node, no self-loop, no dependencies - can deploy independently
40    Independent,
41    /// Single node, no self-loop, but has dependencies
42    AcyclicDependency,
43    /// Part of a cycle (size > 1) - requires grouped deployment
44    CyclicCluster,
45    /// Single node with self-loop - deploy with caution
46    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/// Complete SCC analysis results for a dependency graph.
61#[derive(Debug, Clone)]
62pub struct SccAnalysis {
63    /// SCCs in reverse topological order (dependencies before dependents)
64    pub components: Vec<Scc>,
65    /// Map from node index to its component index
66    pub node_to_component: HashMap<NodeIndex, usize>,
67}
68
69impl SccAnalysis {
70    /// Analyze a graph and compute SCCs on the dependency subgraph.
71    ///
72    /// Ownership edges are excluded from SCC computation per graph-model.md.
73    /// The dependency subgraph includes Import and Reference edge kinds.
74    ///
75    /// Uses an `EdgeFiltered` view instead of cloning the graph - zero-cost,
76    /// no allocation for the subgraph.
77    pub fn analyze(graph: &DiGraph<NodeData, EdgeData>) -> Self {
78        // Zero-cost view that excludes non-dependency edges (Ownership, Flow).
79        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        // Run Tarjan SCC algorithm on the view.
87        // The returned NodeIndex values ARE the original graph's indices.
88        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            // Self-loop detection must use the dependency subgraph too:
95            // a Flow or Ownership self-loop does not make the component
96            // cyclic because those edges are excluded from SCC analysis.
97            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    /// Classify components as Independent if they have no outgoing dependencies
136    /// to other components.
137    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            // Independence follows the dependency subgraph: Ownership and
149            // Flow edges do not create deployment coupling between units.
150            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        // Update hints for components with no external dependencies
167        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    /// Get the component index for a specific node.
182    pub fn component_of(&self, node: NodeIndex) -> Option<usize> {
183        self.node_to_component.get(&node).copied()
184    }
185
186    /// Check if two nodes are in the same SCC (mutually dependent).
187    pub fn mutually_dependent(&self, a: NodeIndex, b: NodeIndex) -> bool {
188        self.component_of(a) == self.component_of(b)
189    }
190
191    /// Returns true if any cycles exist in the graph.
192    pub fn has_cycles(&self) -> bool {
193        self.components.iter().any(|c| c.is_cyclic)
194    }
195
196    /// Get all cyclic components.
197    pub fn cyclic_components(&self) -> impl Iterator<Item = &Scc> {
198        self.components.iter().filter(|c| c.is_cyclic)
199    }
200
201    /// Get all acyclic (independent/dependency) components.
202    pub fn acyclic_components(&self) -> impl Iterator<Item = &Scc> {
203        self.components.iter().filter(|c| !c.is_cyclic)
204    }
205
206    /// Count of components by hint type.
207    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        // a -> b -> c (acyclic chain)
281        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        // a -> b -> c -> a (cycle)
299        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        // Ownership edge should not create cycle even if self-referential structure
319        graph.add_edge(file, sym, make_edge(EdgeKind::Ownership));
320
321        let analysis = SccAnalysis::analyze(&graph);
322
323        // Should have 2 components, not 1
324        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        // Self-loop via reference edge
334        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        // Only dependency between the two nodes is a Flow edge.
366        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        // Cycle 1: a -> b -> a
386        graph.add_edge(a, b, make_edge(EdgeKind::Reference));
387        graph.add_edge(b, a, make_edge(EdgeKind::Reference));
388
389        // Cycle 2: c -> d -> c (independent)
390        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); // Two separate cycles
398
399        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        // a <-> b are mutually dependent, c is independent
411        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        // c depends on b, b depends on a: a -> b -> c
445        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        // Components should be in reverse topological order
451        // That is: c (dependent) comes before a (dependency)
452        // Or more accurately: dependencies should be processed first
453        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        // Just verify we have 3 components
465        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        // Diamond: a -> b, a -> c, b -> d, c -> d
477        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}