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;
17use std::collections::HashMap;
18
19use crate::graph::edge::{EdgeData, EdgeKind};
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            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    /// Classify components as Independent if they have no outgoing dependencies
133    /// to other components.
134    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        // Update hints for components with no external dependencies
162        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    /// Get the component index for a specific node.
177    pub fn component_of(&self, node: NodeIndex) -> Option<usize> {
178        self.node_to_component.get(&node).copied()
179    }
180
181    /// Check if two nodes are in the same SCC (mutually dependent).
182    pub fn mutually_dependent(&self, a: NodeIndex, b: NodeIndex) -> bool {
183        self.component_of(a) == self.component_of(b)
184    }
185
186    /// Returns true if any cycles exist in the graph.
187    pub fn has_cycles(&self) -> bool {
188        self.components.iter().any(|c| c.is_cyclic)
189    }
190
191    /// Get all cyclic components.
192    pub fn cyclic_components(&self) -> impl Iterator<Item = &Scc> {
193        self.components.iter().filter(|c| c.is_cyclic)
194    }
195
196    /// Get all acyclic (independent/dependency) components.
197    pub fn acyclic_components(&self) -> impl Iterator<Item = &Scc> {
198        self.components.iter().filter(|c| !c.is_cyclic)
199    }
200
201    /// Count of components by hint type.
202    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        // a -> b -> c (acyclic chain)
275        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        // a -> b -> c -> a (cycle)
293        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        // Ownership edge should not create cycle even if self-referential structure
313        graph.add_edge(file, sym, make_edge(EdgeKind::Ownership));
314
315        let analysis = SccAnalysis::analyze(&graph);
316
317        // Should have 2 components, not 1
318        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        // Self-loop via reference edge
328        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        // Cycle 1: a -> b -> a
347        graph.add_edge(a, b, make_edge(EdgeKind::Reference));
348        graph.add_edge(b, a, make_edge(EdgeKind::Reference));
349
350        // Cycle 2: c -> d -> c (independent)
351        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); // Two separate cycles
359
360        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        // a <-> b are mutually dependent, c is independent
372        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        // c depends on b, b depends on a: a -> b -> c
406        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        // Components should be in reverse topological order
412        // That is: c (dependent) comes before a (dependency)
413        // Or more accurately: dependencies should be processed first
414        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        // Just verify we have 3 components
426        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        // Diamond: a -> b, a -> c, b -> d, c -> d
438        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}