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