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;
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            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        // One walk over the dependency edges answers both questions the hints
107        // need: a self-loop makes a single node cyclic, and an edge that leaves
108        // its component makes that component dependent on another one.
109        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    /// Get the component index for a specific node.
160    pub fn component_of(&self, node: NodeIndex) -> Option<usize> {
161        self.node_to_component.get(&node).copied()
162    }
163
164    /// Check if two nodes are in the same SCC (mutually dependent).
165    pub fn mutually_dependent(&self, a: NodeIndex, b: NodeIndex) -> bool {
166        self.component_of(a) == self.component_of(b)
167    }
168
169    /// Returns true if any cycles exist in the graph.
170    pub fn has_cycles(&self) -> bool {
171        self.components.iter().any(|c| c.is_cyclic)
172    }
173
174    /// Get all cyclic components.
175    pub fn cyclic_components(&self) -> impl Iterator<Item = &Scc> {
176        self.components.iter().filter(|c| c.is_cyclic)
177    }
178
179    /// Get all acyclic (independent/dependency) components.
180    pub fn acyclic_components(&self) -> impl Iterator<Item = &Scc> {
181        self.components.iter().filter(|c| !c.is_cyclic)
182    }
183
184    /// Count of components by hint type.
185    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        // a -> b -> c (acyclic chain)
259        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        // a -> b -> c -> a (cycle)
277        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        // Ownership edge should not create cycle even if self-referential structure
297        graph.add_edge(file, sym, make_edge(EdgeKind::Ownership));
298
299        let analysis = SccAnalysis::analyze(&graph);
300
301        // Should have 2 components, not 1
302        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        // Self-loop via reference edge
312        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        // Only dependency between the two nodes is a Flow edge.
344        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        // Cycle 1: a -> b -> a
364        graph.add_edge(a, b, make_edge(EdgeKind::Reference));
365        graph.add_edge(b, a, make_edge(EdgeKind::Reference));
366
367        // Cycle 2: c -> d -> c (independent)
368        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); // Two separate cycles
376
377        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        // a <-> b are mutually dependent, c is independent
389        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        // c depends on b, b depends on a: a -> b -> c
423        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        // Components should be in reverse topological order
429        // That is: c (dependent) comes before a (dependency)
430        // Or more accurately: dependencies should be processed first
431        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        // Just verify we have 3 components
443        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        // Diamond: a -> b, a -> c, b -> d, c -> d
455        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    /// One component per hint kind, plus an ownership edge that must not create
481    /// deployment coupling: the counts pin the classification of a mixed graph.
482    #[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}