Skip to main content

drft/analyses/
bridges.rs

1use super::{Analysis, AnalysisContext};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, serde::Serialize)]
5pub struct BridgeEdge {
6    pub source: String,
7    pub target: String,
8}
9
10#[derive(Debug, Clone, serde::Serialize)]
11pub struct BridgesResult {
12    pub cut_vertices: Vec<String>,
13    pub bridges: Vec<BridgeEdge>,
14}
15
16pub struct Bridges;
17
18impl Analysis for Bridges {
19    type Output = BridgesResult;
20
21    fn name(&self) -> &str {
22        "bridges"
23    }
24
25    fn run(&self, ctx: &AnalysisContext) -> BridgesResult {
26        let graph = ctx.graph;
27        // Build undirected adjacency among real nodes
28        let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
29        let real_nodes: Vec<&str> = graph
30            .nodes
31            .keys()
32            .filter(|p| graph.is_file_node(p))
33            .map(|s| s.as_str())
34            .collect();
35
36        for node in &real_nodes {
37            adj.entry(node).or_default();
38        }
39
40        for edge in &graph.edges {
41            if graph.is_file_node(&edge.source)
42                && graph.is_file_node(&edge.target)
43                && edge.source != edge.target
44            {
45                adj.entry(edge.source.as_str())
46                    .or_default()
47                    .push(edge.target.as_str());
48                adj.entry(edge.target.as_str())
49                    .or_default()
50                    .push(edge.source.as_str());
51            }
52        }
53
54        // Deduplicate adjacency lists
55        for neighbors in adj.values_mut() {
56            neighbors.sort();
57            neighbors.dedup();
58        }
59
60        let mut state = TarjanBridgeState {
61            adj: &adj,
62            timer: 0,
63            disc: HashMap::new(),
64            low: HashMap::new(),
65            parent: HashMap::new(),
66            cut_vertices: Vec::new(),
67            bridges: Vec::new(),
68        };
69
70        // Sort for deterministic traversal
71        let mut sorted_nodes = real_nodes;
72        sorted_nodes.sort();
73
74        for &node in &sorted_nodes {
75            if !state.disc.contains_key(node) {
76                state.dfs(node);
77            }
78        }
79
80        let mut cut_vertices = state.cut_vertices;
81        cut_vertices.sort();
82        cut_vertices.dedup();
83
84        let mut bridges: Vec<BridgeEdge> = state
85            .bridges
86            .into_iter()
87            .map(|(a, b)| {
88                // Normalize order
89                if a < b {
90                    BridgeEdge {
91                        source: a.to_string(),
92                        target: b.to_string(),
93                    }
94                } else {
95                    BridgeEdge {
96                        source: b.to_string(),
97                        target: a.to_string(),
98                    }
99                }
100            })
101            .collect();
102        bridges.sort_by(|a, b| {
103            a.source
104                .cmp(&b.source)
105                .then_with(|| a.target.cmp(&b.target))
106        });
107        bridges.dedup_by(|a, b| a.source == b.source && a.target == b.target);
108
109        BridgesResult {
110            cut_vertices: cut_vertices.into_iter().map(|s| s.to_string()).collect(),
111            bridges,
112        }
113    }
114}
115
116struct TarjanBridgeState<'a> {
117    adj: &'a HashMap<&'a str, Vec<&'a str>>,
118    timer: usize,
119    disc: HashMap<&'a str, usize>,
120    low: HashMap<&'a str, usize>,
121    parent: HashMap<&'a str, &'a str>,
122    cut_vertices: Vec<&'a str>,
123    bridges: Vec<(&'a str, &'a str)>,
124}
125
126impl<'a> TarjanBridgeState<'a> {
127    fn dfs(&mut self, u: &'a str) {
128        self.disc.insert(u, self.timer);
129        self.low.insert(u, self.timer);
130        self.timer += 1;
131        let mut child_count = 0;
132
133        let neighbors = self.adj.get(u).cloned().unwrap_or_default();
134        for v in neighbors {
135            if !self.disc.contains_key(v) {
136                child_count += 1;
137                self.parent.insert(v, u);
138                self.dfs(v);
139
140                let v_low = self.low[v];
141                let u_low = self.low[u];
142                if v_low < u_low {
143                    self.low.insert(u, v_low);
144                }
145
146                // u is a cut vertex if:
147                // 1) u is root of DFS tree and has two or more children
148                let is_root = !self.parent.contains_key(u);
149                if is_root && child_count > 1 {
150                    self.cut_vertices.push(u);
151                }
152                // 2) u is not root and low[v] >= disc[u]
153                if !is_root && self.low[v] >= self.disc[u] {
154                    self.cut_vertices.push(u);
155                }
156
157                // (u, v) is a bridge if low[v] > disc[u]
158                if self.low[v] > self.disc[u] {
159                    self.bridges.push((u, v));
160                }
161            } else if self.parent.get(u) != Some(&v) {
162                // Back edge
163                let v_disc = self.disc[v];
164                let u_low = self.low[u];
165                if v_disc < u_low {
166                    self.low.insert(u, v_disc);
167                }
168            }
169        }
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use crate::analyses::AnalysisContext;
177    use crate::config::Config;
178    use crate::graph::Graph;
179    use crate::graph::test_helpers::{make_edge, make_node};
180    use std::path::Path;
181
182    fn make_ctx<'a>(graph: &'a Graph, config: &'a Config) -> AnalysisContext<'a> {
183        AnalysisContext {
184            graph,
185            root: Path::new("."),
186            config,
187            lockfile: None,
188        }
189    }
190
191    #[test]
192    fn linear_chain() {
193        let mut graph = Graph::new();
194        graph.add_node(make_node("a.md"));
195        graph.add_node(make_node("b.md"));
196        graph.add_node(make_node("c.md"));
197        graph.add_edge(make_edge("a.md", "b.md"));
198        graph.add_edge(make_edge("b.md", "c.md"));
199
200        let config = Config::defaults();
201        let result = Bridges.run(&make_ctx(&graph, &config));
202        assert_eq!(result.bridges.len(), 2);
203        assert_eq!(result.cut_vertices, vec!["b.md"]);
204    }
205
206    #[test]
207    fn cycle_has_no_bridges() {
208        let mut graph = Graph::new();
209        graph.add_node(make_node("a.md"));
210        graph.add_node(make_node("b.md"));
211        graph.add_node(make_node("c.md"));
212        graph.add_edge(make_edge("a.md", "b.md"));
213        graph.add_edge(make_edge("b.md", "c.md"));
214        graph.add_edge(make_edge("c.md", "a.md"));
215
216        let config = Config::defaults();
217        let result = Bridges.run(&make_ctx(&graph, &config));
218        assert!(result.bridges.is_empty());
219        assert!(result.cut_vertices.is_empty());
220    }
221
222    #[test]
223    fn star_graph() {
224        let mut graph = Graph::new();
225        graph.add_node(make_node("center.md"));
226        graph.add_node(make_node("a.md"));
227        graph.add_node(make_node("b.md"));
228        graph.add_node(make_node("c.md"));
229        graph.add_edge(make_edge("center.md", "a.md"));
230        graph.add_edge(make_edge("center.md", "b.md"));
231        graph.add_edge(make_edge("center.md", "c.md"));
232
233        let config = Config::defaults();
234        let result = Bridges.run(&make_ctx(&graph, &config));
235        assert_eq!(result.cut_vertices, vec!["center.md"]);
236        assert_eq!(result.bridges.len(), 3);
237    }
238
239    #[test]
240    fn empty_graph() {
241        let graph = Graph::new();
242        let config = Config::defaults();
243        let result = Bridges.run(&make_ctx(&graph, &config));
244        assert!(result.cut_vertices.is_empty());
245        assert!(result.bridges.is_empty());
246    }
247
248    #[test]
249    fn single_node() {
250        let mut graph = Graph::new();
251        graph.add_node(make_node("a.md"));
252
253        let config = Config::defaults();
254        let result = Bridges.run(&make_ctx(&graph, &config));
255        assert!(result.cut_vertices.is_empty());
256        assert!(result.bridges.is_empty());
257    }
258
259    #[test]
260    fn cycle_with_tail() {
261        let mut graph = Graph::new();
262        graph.add_node(make_node("a.md"));
263        graph.add_node(make_node("b.md"));
264        graph.add_node(make_node("c.md"));
265        graph.add_node(make_node("d.md"));
266        graph.add_edge(make_edge("a.md", "b.md"));
267        graph.add_edge(make_edge("b.md", "c.md"));
268        graph.add_edge(make_edge("c.md", "a.md"));
269        graph.add_edge(make_edge("c.md", "d.md"));
270
271        let config = Config::defaults();
272        let result = Bridges.run(&make_ctx(&graph, &config));
273        assert_eq!(result.bridges.len(), 1);
274        assert_eq!(result.bridges[0].source, "c.md");
275        assert_eq!(result.bridges[0].target, "d.md");
276        assert!(result.cut_vertices.contains(&"c.md".to_string()));
277    }
278}