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