Skip to main content

drft/analyses/
scc.rs

1use super::{Analysis, AnalysisContext};
2use crate::graph::Graph;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, serde::Serialize)]
6pub struct Scc {
7    pub id: usize,
8    pub members: Vec<String>,
9}
10
11#[derive(Debug, Clone, serde::Serialize)]
12pub struct SccResult {
13    pub scc_count: usize,
14    pub nontrivial_count: usize,
15    pub sccs: Vec<Scc>,
16    pub node_scc: HashMap<String, usize>,
17}
18
19pub struct StronglyConnectedComponents;
20
21impl Analysis for StronglyConnectedComponents {
22    type Output = SccResult;
23
24    fn name(&self) -> &str {
25        "scc"
26    }
27
28    fn run(&self, ctx: &AnalysisContext) -> SccResult {
29        let graph = ctx.graph;
30        let real_nodes: Vec<&str> = graph
31            .nodes
32            .keys()
33            .filter(|p| graph.is_included_node(p))
34            .map(|s| s.as_str())
35            .collect();
36
37        let mut state = TarjanState {
38            graph,
39            index_counter: 0,
40            stack: Vec::new(),
41            on_stack: HashMap::new(),
42            index: HashMap::new(),
43            lowlink: HashMap::new(),
44            components: Vec::new(),
45        };
46
47        // Sort for deterministic output
48        let mut sorted_nodes = real_nodes;
49        sorted_nodes.sort();
50
51        for &node in &sorted_nodes {
52            if !state.index.contains_key(node) {
53                state.strongconnect(node);
54            }
55        }
56
57        // Build result
58        let all_components = state.components;
59        let scc_count = all_components.len();
60
61        // Check for self-loops to determine non-trivial single-node SCCs
62        let mut has_self_loop: HashMap<&str, bool> = HashMap::new();
63        for edge in &graph.edges {
64            if edge.source == edge.target && graph.is_internal_edge(edge) {
65                has_self_loop.insert(edge.source.as_str(), true);
66            }
67        }
68
69        // Non-trivial SCCs: size > 1, or size == 1 with a self-loop
70        let nontrivial: Vec<Vec<String>> = all_components
71            .iter()
72            .filter(|c| c.len() > 1 || (c.len() == 1 && has_self_loop.contains_key(c[0].as_str())))
73            .cloned()
74            .collect();
75
76        let nontrivial_count = nontrivial.len();
77
78        // Assign SCC IDs to all nodes
79        let mut node_scc: HashMap<String, usize> = HashMap::new();
80        for (i, component) in all_components.iter().enumerate() {
81            for member in component {
82                node_scc.insert(member.clone(), i + 1);
83            }
84        }
85
86        // Build output SCCs (non-trivial only, sorted by size desc then first member)
87        let mut output_sccs = nontrivial;
88        output_sccs.sort_by(|a, b| b.len().cmp(&a.len()).then_with(|| a[0].cmp(&b[0])));
89
90        let sccs = output_sccs
91            .into_iter()
92            .enumerate()
93            .map(|(i, members)| Scc { id: i + 1, members })
94            .collect();
95
96        SccResult {
97            scc_count,
98            nontrivial_count,
99            sccs,
100            node_scc,
101        }
102    }
103}
104
105struct TarjanState<'a> {
106    graph: &'a Graph,
107    index_counter: usize,
108    stack: Vec<&'a str>,
109    on_stack: HashMap<&'a str, bool>,
110    index: HashMap<&'a str, usize>,
111    lowlink: HashMap<&'a str, usize>,
112    components: Vec<Vec<String>>,
113}
114
115impl<'a> TarjanState<'a> {
116    fn strongconnect(&mut self, v: &'a str) {
117        self.index.insert(v, self.index_counter);
118        self.lowlink.insert(v, self.index_counter);
119        self.index_counter += 1;
120        self.stack.push(v);
121        self.on_stack.insert(v, true);
122
123        // Visit successors
124        if let Some(edge_indices) = self.graph.forward.get(v) {
125            for &idx in edge_indices {
126                let edge = &self.graph.edges[idx];
127                if !self.graph.is_internal_edge(edge) {
128                    continue;
129                }
130                let w = edge.target.as_str();
131                if !self.index.contains_key(w) {
132                    self.strongconnect(w);
133                    let w_low = self.lowlink[w];
134                    let v_low = self.lowlink[v];
135                    if w_low < v_low {
136                        self.lowlink.insert(v, w_low);
137                    }
138                } else if self.on_stack.get(w) == Some(&true) {
139                    let w_idx = self.index[w];
140                    let v_low = self.lowlink[v];
141                    if w_idx < v_low {
142                        self.lowlink.insert(v, w_idx);
143                    }
144                }
145            }
146        }
147
148        // If v is a root node, pop the stack to form an SCC
149        if self.lowlink[v] == self.index[v] {
150            let mut component = Vec::new();
151            loop {
152                let w = self.stack.pop().unwrap();
153                self.on_stack.insert(w, false);
154                component.push(w.to_string());
155                if w == v {
156                    break;
157                }
158            }
159            component.sort();
160            self.components.push(component);
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::analyses::AnalysisContext;
169    use crate::config::Config;
170    use crate::graph::Graph;
171    use crate::graph::test_helpers::{make_edge, make_node};
172    use std::path::Path;
173
174    fn make_ctx<'a>(graph: &'a Graph, config: &'a Config) -> AnalysisContext<'a> {
175        AnalysisContext {
176            graph,
177            root: Path::new("."),
178            config,
179            lockfile: None,
180        }
181    }
182
183    #[test]
184    fn dag_has_no_nontrivial_sccs() {
185        let mut graph = Graph::new();
186        graph.add_node(make_node("a.md"));
187        graph.add_node(make_node("b.md"));
188        graph.add_node(make_node("c.md"));
189        graph.add_edge(make_edge("a.md", "b.md"));
190        graph.add_edge(make_edge("b.md", "c.md"));
191
192        let config = Config::defaults();
193        let result = StronglyConnectedComponents.run(&make_ctx(&graph, &config));
194        assert_eq!(result.scc_count, 3);
195        assert_eq!(result.nontrivial_count, 0);
196        assert!(result.sccs.is_empty());
197    }
198
199    #[test]
200    fn simple_cycle() {
201        let mut graph = Graph::new();
202        graph.add_node(make_node("a.md"));
203        graph.add_node(make_node("b.md"));
204        graph.add_node(make_node("c.md"));
205        graph.add_edge(make_edge("a.md", "b.md"));
206        graph.add_edge(make_edge("b.md", "c.md"));
207        graph.add_edge(make_edge("c.md", "a.md"));
208
209        let config = Config::defaults();
210        let result = StronglyConnectedComponents.run(&make_ctx(&graph, &config));
211        assert_eq!(result.nontrivial_count, 1);
212        assert_eq!(result.sccs.len(), 1);
213        assert_eq!(result.sccs[0].members, vec!["a.md", "b.md", "c.md"]);
214    }
215
216    #[test]
217    fn two_separate_cycles() {
218        let mut graph = Graph::new();
219        graph.add_node(make_node("a.md"));
220        graph.add_node(make_node("b.md"));
221        graph.add_node(make_node("c.md"));
222        graph.add_node(make_node("d.md"));
223        graph.add_edge(make_edge("a.md", "b.md"));
224        graph.add_edge(make_edge("b.md", "a.md"));
225        graph.add_edge(make_edge("c.md", "d.md"));
226        graph.add_edge(make_edge("d.md", "c.md"));
227
228        let config = Config::defaults();
229        let result = StronglyConnectedComponents.run(&make_ctx(&graph, &config));
230        assert_eq!(result.nontrivial_count, 2);
231        assert_eq!(result.sccs.len(), 2);
232    }
233
234    #[test]
235    fn mixed_cyclic_and_acyclic() {
236        let mut graph = Graph::new();
237        graph.add_node(make_node("a.md"));
238        graph.add_node(make_node("b.md"));
239        graph.add_node(make_node("c.md"));
240        graph.add_edge(make_edge("a.md", "b.md"));
241        graph.add_edge(make_edge("b.md", "c.md"));
242        graph.add_edge(make_edge("c.md", "b.md"));
243
244        let config = Config::defaults();
245        let result = StronglyConnectedComponents.run(&make_ctx(&graph, &config));
246        assert_eq!(result.nontrivial_count, 1);
247        assert_eq!(result.sccs[0].members, vec!["b.md", "c.md"]);
248        assert!(result.node_scc.contains_key("a.md"));
249    }
250
251    #[test]
252    fn self_loop_is_nontrivial() {
253        let mut graph = Graph::new();
254        graph.add_node(make_node("a.md"));
255        graph.add_edge(make_edge("a.md", "a.md"));
256
257        let config = Config::defaults();
258        let result = StronglyConnectedComponents.run(&make_ctx(&graph, &config));
259        assert_eq!(result.nontrivial_count, 1);
260        assert_eq!(result.sccs[0].members, vec!["a.md"]);
261    }
262
263    #[test]
264    fn empty_graph() {
265        let graph = Graph::new();
266        let config = Config::defaults();
267        let result = StronglyConnectedComponents.run(&make_ctx(&graph, &config));
268        assert_eq!(result.scc_count, 0);
269        assert_eq!(result.nontrivial_count, 0);
270    }
271
272    #[test]
273    fn every_node_has_scc_id() {
274        let mut graph = Graph::new();
275        graph.add_node(make_node("a.md"));
276        graph.add_node(make_node("b.md"));
277        graph.add_node(make_node("c.md"));
278        graph.add_edge(make_edge("a.md", "b.md"));
279        graph.add_edge(make_edge("b.md", "c.md"));
280
281        let config = Config::defaults();
282        let result = StronglyConnectedComponents.run(&make_ctx(&graph, &config));
283        assert!(result.node_scc.contains_key("a.md"));
284        assert!(result.node_scc.contains_key("b.md"));
285        assert!(result.node_scc.contains_key("c.md"));
286    }
287}