diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 05270e54..49d849b4 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "_rustgrimp" diff --git a/rust/src/graph/cycles.rs b/rust/src/graph/cycles.rs new file mode 100644 index 00000000..87bbc747 --- /dev/null +++ b/rust/src/graph/cycles.rs @@ -0,0 +1,138 @@ +/* +TODO K4liber: description + +Finds all cycles using Johnson's algorithm. +*/ +use std::collections::{HashSet, HashMap}; + +use crate::graph::Graph; + +struct _DirectedGraph { + adj_list: HashMap>, +} + +impl _DirectedGraph { + + fn new() -> Self { + _DirectedGraph { + adj_list: HashMap::new() + } + } + + fn add_edge(&mut self, u: &str, v: &str) { + self.adj_list + .entry(u.to_string()) + .or_insert_with(Vec::new) + .push(v.to_string()); + } + + fn find_cycles(&self) -> Vec> { + let mut cycles = Vec::new(); + let mut blocked = HashSet::new(); + let mut stack = Vec::new(); + let mut b_sets: HashMap> = HashMap::new(); + + for start in self.adj_list.keys() { + let mut subgraph = self.build_subgraph(start); + self.find_cycles_from( + start, + start, + &mut blocked, + &mut stack, + &mut b_sets, + &mut cycles, + &mut subgraph, + ); + } + + cycles + } + + fn build_subgraph(&self, start: &String) -> HashMap> { + self.adj_list + .iter() + .filter(|(node, _)| node >= &start) + .map(|(node, neighbors)| { + let filtered_neighbors: Vec = + neighbors.iter().filter(|n| n >= &start).cloned().collect(); + (node.clone(), filtered_neighbors) + }) + .collect() + } + + fn find_cycles_from( + &self, + start: &String, + v: &String, + blocked: &mut HashSet, + stack: &mut Vec, + b_sets: &mut HashMap>, + cycles: &mut Vec>, + subgraph: &mut HashMap>, + ) -> bool { + let mut found_cycle = false; + stack.push(v.clone()); + blocked.insert(v.clone()); + + let neighbors = subgraph.get(v).cloned().unwrap_or_default(); + for w in neighbors { + if &w == start { + cycles.push(stack.clone()); + found_cycle = true; + } else if !blocked.contains(&w) { + if self.find_cycles_from(start, &w, blocked, stack, b_sets, cycles, subgraph) { + found_cycle = true; + } + } + } + + if found_cycle { + self.unblock(v, blocked, b_sets); + } else { + if let Some(neighbors) = subgraph.get(v) { + for w in neighbors { + b_sets.entry(w.clone()).or_default().insert(v.clone()); + } + } + } + + stack.pop(); + found_cycle + } + + fn unblock(&self, node: &String, blocked: &mut HashSet, b_sets: &mut HashMap>) { + blocked.remove(node); + let mut stack = vec![node.clone()]; + + while let Some(n) = stack.pop() { + if let Some(dependent_nodes) = b_sets.remove(&n) { + for w in dependent_nodes { + if blocked.remove(&w) { + stack.push(w); + } + } + } + } + } +} + +impl Graph { + pub fn find_cycles( + &self + ) -> Vec> { + let mut directed_graph = _DirectedGraph::new(); + + for module in self.all_modules() { + let module_token = module.token(); + let imports_modules_tokens = self.imports.get(module_token).unwrap().iter(); + + for next_module in imports_modules_tokens { + let imported_module = self.get_module(*next_module); + directed_graph.add_edge(module.name().as_str(), imported_module.unwrap().name().as_str()); + } + } + + let cycles = directed_graph.find_cycles(); + return cycles; + } +} diff --git a/rust/src/graph/mod.rs b/rust/src/graph/mod.rs index 8a97b2bd..49ce0974 100644 --- a/rust/src/graph/mod.rs +++ b/rust/src/graph/mod.rs @@ -13,6 +13,7 @@ pub mod graph_manipulation; pub mod hierarchy_queries; pub mod higher_order_queries; pub mod import_chain_queries; +pub mod cycles; pub(crate) mod pathfinding; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 49a97a5b..2dd21f27 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -420,6 +420,12 @@ impl GraphWrapper { self.convert_package_dependencies_to_python(py, illegal_dependencies) } + pub fn find_cycles( + &self + ) -> PyResult>> { + Ok(self._graph.find_cycles()) + } + pub fn clone(&self) -> GraphWrapper { GraphWrapper { _graph: self._graph.clone(), diff --git a/rust/tests/test_cycles.rs b/rust/tests/test_cycles.rs new file mode 100644 index 00000000..774a940a --- /dev/null +++ b/rust/tests/test_cycles.rs @@ -0,0 +1,28 @@ +use _rustgrimp::graph::Graph; + +#[test] +fn test_find_cycles() { + // Given + let mut graph = Graph::default(); + let dependencies: Vec> = vec![ + vec!["A".to_string(), "B".to_string()], + vec!["B".to_string(), "C".to_string()], + vec!["C".to_string(), "A".to_string()], + ]; + + for modules in dependencies { + let importer_name = &modules[0]; + let imported_name = &modules[1]; + let importer = graph.get_or_add_module(importer_name).token(); + let imported = graph.get_or_add_module(imported_name).token(); + graph.add_import(importer, imported) + } + + let expected_cycles: Vec> = vec![ + vec!["A".to_string(), "B".to_string(), "C".to_string()], + ]; + // When + let cycles = graph.find_cycles(); + // Then + assert_eq!(cycles, expected_cycles); +} diff --git a/src/grimp/adaptors/graph.py b/src/grimp/adaptors/graph.py index 4730ef45..cb164774 100644 --- a/src/grimp/adaptors/graph.py +++ b/src/grimp/adaptors/graph.py @@ -145,6 +145,12 @@ def find_illegal_dependencies_for_layers( return _dependencies_from_tuple(result) + def find_cycles( + self + ) -> list[list[str]]: + result: list[list[str]] = self._rustgraph.find_cycles() + return result + # Dunder methods # -------------- diff --git a/src/grimp/application/ports/graph.py b/src/grimp/application/ports/graph.py index acf39a84..f3e91963 100644 --- a/src/grimp/application/ports/graph.py +++ b/src/grimp/application/ports/graph.py @@ -325,6 +325,15 @@ def find_illegal_dependencies_for_layers( """ raise NotImplementedError + @abc.abstractmethod + def find_cycles(self) -> list[list[str]]: + """ + TODO K4liber: + - docs + + """ + raise NotImplementedError + def __repr__(self) -> str: """ Display the instance in one of the following ways: diff --git a/tests/unit/test_cycles.py b/tests/unit/test_cycles.py new file mode 100644 index 00000000..e50e97fc --- /dev/null +++ b/tests/unit/test_cycles.py @@ -0,0 +1,26 @@ +""" +TODO K4liber: +- remove this notes +- add more test cases (real-life libraries?) + +To rebuild the rust binary run the following command from the root directory: + +python -m pip install -e . + +""" + +from grimp.adaptors.graph import ImportGraph + + +class TestFindCycles: + + def test_empty_graph(self) -> None: + graph = ImportGraph() + graph.add_module("A") + graph.add_module("B") + graph.add_module("C") + graph.add_import(importer="A", imported="B") + graph.add_import(importer="B", imported="C") + graph.add_import(importer="C", imported="A") + cycles = graph.find_cycles() + assert cycles == [["A", "B", "C"]]