|
1 | 1 | use crate::errors::{GrimpError, GrimpResult}; |
2 | 2 | use crate::graph::{Graph, ModuleToken, EMPTY_MODULE_TOKENS}; |
3 | 3 | use indexmap::{IndexMap, IndexSet}; |
| 4 | +use itertools::Itertools; |
4 | 5 | use rustc_hash::{FxHashMap, FxHashSet, FxHasher}; |
5 | 6 | use slotmap::SecondaryMap; |
6 | 7 | use std::hash::BuildHasherDefault; |
@@ -49,6 +50,23 @@ pub fn find_shortest_path( |
49 | 50 | ) |
50 | 51 | } |
51 | 52 |
|
| 53 | +/// Finds the shortest cycle from `modules` to `modules`, via a bidirectional BFS. |
| 54 | +pub fn find_shortest_cycle( |
| 55 | + graph: &Graph, |
| 56 | + modules: &FxHashSet<ModuleToken>, |
| 57 | + excluded_modules: &FxHashSet<ModuleToken>, |
| 58 | + excluded_imports: &FxHashMap<ModuleToken, FxHashSet<ModuleToken>>, |
| 59 | +) -> GrimpResult<Option<Vec<ModuleToken>>> { |
| 60 | + // Exclude imports internal to `modules` |
| 61 | + let mut excluded_imports = excluded_imports.clone(); |
| 62 | + for (m1, m2) in modules.iter().tuple_combinations() { |
| 63 | + excluded_imports.entry(*m1).or_default().insert(*m2); |
| 64 | + excluded_imports.entry(*m2).or_default().insert(*m1); |
| 65 | + } |
| 66 | + |
| 67 | + _find_shortest_path(graph, modules, modules, excluded_modules, &excluded_imports) |
| 68 | +} |
| 69 | + |
52 | 70 | fn _find_shortest_path( |
53 | 71 | graph: &Graph, |
54 | 72 | from_modules: &FxHashSet<ModuleToken>, |
@@ -140,3 +158,105 @@ fn import_is_excluded( |
140 | 158 | .contains(to_module) |
141 | 159 | } |
142 | 160 | } |
| 161 | + |
| 162 | +#[cfg(test)] |
| 163 | +mod test_find_shortest_cycle { |
| 164 | + use super::*; |
| 165 | + |
| 166 | + #[test] |
| 167 | + fn test_finds_cycle_single_module() -> GrimpResult<()> { |
| 168 | + let mut graph = Graph::default(); |
| 169 | + let foo = graph.get_or_add_module("foo").token; |
| 170 | + let bar = graph.get_or_add_module("bar").token; |
| 171 | + let baz = graph.get_or_add_module("baz").token; |
| 172 | + let x = graph.get_or_add_module("x").token; |
| 173 | + let y = graph.get_or_add_module("y").token; |
| 174 | + let z = graph.get_or_add_module("z").token; |
| 175 | + // Shortest cycle |
| 176 | + graph.add_import(foo, bar); |
| 177 | + graph.add_import(bar, baz); |
| 178 | + graph.add_import(baz, foo); |
| 179 | + // Longer cycle |
| 180 | + graph.add_import(foo, x); |
| 181 | + graph.add_import(x, y); |
| 182 | + graph.add_import(y, z); |
| 183 | + graph.add_import(z, foo); |
| 184 | + |
| 185 | + let path = find_shortest_cycle( |
| 186 | + &graph, |
| 187 | + &foo.into(), |
| 188 | + &FxHashSet::default(), |
| 189 | + &FxHashMap::default(), |
| 190 | + )?; |
| 191 | + assert_eq!(path, Some(vec![foo, bar, baz, foo])); |
| 192 | + |
| 193 | + graph.remove_import(baz, foo); |
| 194 | + |
| 195 | + let path = find_shortest_cycle( |
| 196 | + &graph, |
| 197 | + &foo.into(), |
| 198 | + &FxHashSet::default(), |
| 199 | + &FxHashMap::default(), |
| 200 | + )?; |
| 201 | + assert_eq!(path, Some(vec![foo, x, y, z, foo])); |
| 202 | + |
| 203 | + Ok(()) |
| 204 | + } |
| 205 | + |
| 206 | + #[test] |
| 207 | + fn test_returns_none_if_no_cycle() -> GrimpResult<()> { |
| 208 | + let mut graph = Graph::default(); |
| 209 | + let foo = graph.get_or_add_module("foo").token; |
| 210 | + let bar = graph.get_or_add_module("bar").token; |
| 211 | + let baz = graph.get_or_add_module("baz").token; |
| 212 | + graph.add_import(foo, bar); |
| 213 | + graph.add_import(bar, baz); |
| 214 | + |
| 215 | + let path = find_shortest_cycle( |
| 216 | + &graph, |
| 217 | + &foo.into(), |
| 218 | + &FxHashSet::default(), |
| 219 | + &FxHashMap::default(), |
| 220 | + )?; |
| 221 | + |
| 222 | + assert_eq!(path, None); |
| 223 | + |
| 224 | + Ok(()) |
| 225 | + } |
| 226 | + |
| 227 | + #[test] |
| 228 | + fn test_finds_cycle_multiple_module() -> GrimpResult<()> { |
| 229 | + let mut graph = Graph::default(); |
| 230 | + |
| 231 | + graph.get_or_add_module("colors"); |
| 232 | + let red = graph.get_or_add_module("colors.red").token; |
| 233 | + let blue = graph.get_or_add_module("colors.blue").token; |
| 234 | + let a = graph.get_or_add_module("a").token; |
| 235 | + let b = graph.get_or_add_module("b").token; |
| 236 | + let c = graph.get_or_add_module("c").token; |
| 237 | + let d = graph.get_or_add_module("d").token; |
| 238 | + |
| 239 | + // The computation should not be confused by these two imports internal to `modules`. |
| 240 | + graph.add_import(red, blue); |
| 241 | + graph.add_import(blue, red); |
| 242 | + // This is the part we expect to find. |
| 243 | + graph.add_import(red, a); |
| 244 | + graph.add_import(a, b); |
| 245 | + graph.add_import(b, blue); |
| 246 | + // A longer path. |
| 247 | + graph.add_import(a, c); |
| 248 | + graph.add_import(c, d); |
| 249 | + graph.add_import(d, b); |
| 250 | + |
| 251 | + let path = find_shortest_cycle( |
| 252 | + &graph, |
| 253 | + &FxHashSet::from_iter([red, blue]), |
| 254 | + &FxHashSet::default(), |
| 255 | + &FxHashMap::default(), |
| 256 | + )?; |
| 257 | + |
| 258 | + assert_eq!(path, Some(vec![red, a, b, blue])); |
| 259 | + |
| 260 | + Ok(()) |
| 261 | + } |
| 262 | +} |
0 commit comments