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 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 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 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 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 let is_root = !self.parent.contains_key(u);
149 if is_root && child_count > 1 {
150 self.cut_vertices.push(u);
151 }
152 if !is_root && self.low[v] >= self.disc[u] {
154 self.cut_vertices.push(u);
155 }
156
157 if self.low[v] > self.disc[u] {
159 self.bridges.push((u, v));
160 }
161 } else if self.parent.get(u) != Some(&v) {
162 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}