use crate::organization::god_object::types::ModuleSplit;
use std::collections::{HashMap, HashSet};
pub fn detect_circular_dependencies(splits: &[ModuleSplit]) -> Vec<Vec<String>> {
let mut graph: HashMap<String, Vec<String>> = HashMap::new();
for split in splits {
graph.insert(split.suggested_name.clone(), split.dependencies_in.clone());
}
let mut cycles = Vec::new();
let mut visited = HashSet::new();
let mut rec_stack = HashSet::new();
let mut current_path = Vec::new();
for node in graph.keys() {
if !visited.contains(node) {
dfs_find_cycles(
node,
&graph,
&mut visited,
&mut rec_stack,
&mut current_path,
&mut cycles,
);
}
}
cycles
}
fn is_duplicate_cycle(cycles: &[Vec<String>], new_cycle_sorted: &[String]) -> bool {
cycles.iter().any(|existing| {
let mut sorted = existing.clone();
sorted.sort();
sorted == new_cycle_sorted
})
}
fn extract_cycle(path: &[String], cycle_start_idx: usize) -> Vec<String> {
path[cycle_start_idx..].to_vec()
}
fn normalize_cycle(cycle: &[String]) -> Vec<String> {
let mut sorted = cycle.to_vec();
sorted.sort();
sorted
}
fn try_record_cycle(cycles: &mut Vec<Vec<String>>, path: &[String], cycle_start_idx: usize) {
let cycle = extract_cycle(path, cycle_start_idx);
let normalized = normalize_cycle(&cycle);
if !is_duplicate_cycle(cycles, &normalized) {
cycles.push(cycle);
}
}
fn enter_node(
node: &str,
visited: &mut HashSet<String>,
rec_stack: &mut HashSet<String>,
path: &mut Vec<String>,
) {
visited.insert(node.to_string());
rec_stack.insert(node.to_string());
path.push(node.to_string());
}
fn leave_node(node: &str, rec_stack: &mut HashSet<String>, path: &mut Vec<String>) {
path.pop();
rec_stack.remove(node);
}
fn process_neighbors(
neighbors: &[String],
visited: &HashSet<String>,
rec_stack: &HashSet<String>,
path: &[String],
) -> (Vec<String>, Vec<usize>) {
let mut to_visit = Vec::new();
let mut cycle_starts = Vec::new();
for neighbor in neighbors.iter().rev() {
if !visited.contains(neighbor) {
to_visit.push(neighbor.clone());
} else if rec_stack.contains(neighbor) {
if let Some(idx) = path.iter().position(|n| n == neighbor) {
cycle_starts.push(idx);
}
}
}
(to_visit, cycle_starts)
}
fn dfs_find_cycles(
start_node: &str,
graph: &HashMap<String, Vec<String>>,
visited: &mut HashSet<String>,
rec_stack: &mut HashSet<String>,
_current_path: &mut [String],
cycles: &mut Vec<Vec<String>>,
) {
let mut stack: Vec<(String, bool)> = vec![(start_node.to_string(), true)];
let mut path: Vec<String> = Vec::new();
while let Some((node, is_entering)) = stack.pop() {
if !is_entering {
leave_node(&node, rec_stack, &mut path);
continue;
}
enter_node(&node, visited, rec_stack, &mut path);
stack.push((node.clone(), false));
let neighbors = graph.get(&node).map(|v| v.as_slice()).unwrap_or(&[]);
let (to_visit, cycle_starts) = process_neighbors(neighbors, visited, rec_stack, &path);
for idx in cycle_starts {
try_record_cycle(cycles, &path, idx);
}
for neighbor in to_visit {
stack.push((neighbor, true));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::organization::god_object::types::{ModuleSplit, Priority};
fn create_split_with_deps(name: &str, deps_in: Vec<&str>, _deps_out: Vec<&str>) -> ModuleSplit {
ModuleSplit {
suggested_name: name.to_string(),
methods_to_move: vec![],
structs_to_move: vec![],
responsibility: "test".to_string(),
estimated_lines: 100,
method_count: 5,
warning: None,
priority: Priority::Medium,
cohesion_score: None,
dependencies_in: deps_in.iter().map(|s| s.to_string()).collect(),
dependencies_out: _deps_out.iter().map(|s| s.to_string()).collect(),
domain: String::new(),
rationale: None,
method: crate::organization::SplitAnalysisMethod::None,
severity: None,
interface_estimate: None,
classification_evidence: None,
representative_methods: vec![],
fields_needed: vec![],
trait_suggestion: None,
behavior_category: None,
..Default::default()
}
}
#[test]
fn test_simple_cycle_detection() {
let splits = vec![
create_split_with_deps("ModuleA", vec!["ModuleB"], vec![]),
create_split_with_deps("ModuleB", vec!["ModuleC"], vec![]),
create_split_with_deps("ModuleC", vec!["ModuleA"], vec![]),
];
let cycles = detect_circular_dependencies(&splits);
assert_eq!(cycles.len(), 1, "Should detect one cycle");
assert!(cycles[0].contains(&"ModuleA".to_string()));
assert!(cycles[0].contains(&"ModuleB".to_string()));
assert!(cycles[0].contains(&"ModuleC".to_string()));
}
#[test]
fn test_no_cycles() {
let splits = vec![
create_split_with_deps("ModuleA", vec!["ModuleB"], vec![]),
create_split_with_deps("ModuleB", vec!["ModuleC"], vec![]),
create_split_with_deps("ModuleC", vec![], vec![]),
];
let cycles = detect_circular_dependencies(&splits);
assert_eq!(cycles.len(), 0, "Should detect no cycles");
}
#[test]
fn test_self_cycle() {
let splits = vec![create_split_with_deps("ModuleA", vec!["ModuleA"], vec![])];
let cycles = detect_circular_dependencies(&splits);
assert_eq!(cycles.len(), 1, "Should detect self-cycle");
}
#[test]
fn test_two_node_cycle() {
let splits = vec![
create_split_with_deps("ModuleA", vec!["ModuleB"], vec![]),
create_split_with_deps("ModuleB", vec!["ModuleA"], vec![]),
];
let cycles = detect_circular_dependencies(&splits);
assert_eq!(cycles.len(), 1, "Should detect two-node cycle");
}
#[test]
fn test_multiple_independent_cycles() {
let splits = vec![
create_split_with_deps("ModuleA", vec!["ModuleB"], vec![]),
create_split_with_deps("ModuleB", vec!["ModuleA"], vec![]),
create_split_with_deps("ModuleC", vec!["ModuleD"], vec![]),
create_split_with_deps("ModuleD", vec!["ModuleC"], vec![]),
];
let cycles = detect_circular_dependencies(&splits);
assert!(cycles.len() >= 2, "Should detect both cycles");
}
#[test]
fn test_is_duplicate_cycle_finds_match() {
let existing = vec![vec!["A".to_string(), "B".to_string(), "C".to_string()]];
let new_cycle = vec!["A".to_string(), "B".to_string(), "C".to_string()];
assert!(is_duplicate_cycle(&existing, &new_cycle));
}
#[test]
fn test_is_duplicate_cycle_different_order_still_matches() {
let existing = vec![vec!["C".to_string(), "A".to_string(), "B".to_string()]];
let new_cycle = vec!["A".to_string(), "B".to_string(), "C".to_string()];
assert!(is_duplicate_cycle(&existing, &new_cycle));
}
#[test]
fn test_is_duplicate_cycle_no_match() {
let existing = vec![vec!["A".to_string(), "B".to_string()]];
let new_cycle = vec!["A".to_string(), "C".to_string()];
assert!(!is_duplicate_cycle(&existing, &new_cycle));
}
#[test]
fn test_extract_cycle_from_middle() {
let path = vec![
"A".to_string(),
"B".to_string(),
"C".to_string(),
"D".to_string(),
];
let cycle = extract_cycle(&path, 1);
assert_eq!(cycle, vec!["B", "C", "D"]);
}
#[test]
fn test_normalize_cycle_sorts_alphabetically() {
let cycle = vec!["C".to_string(), "A".to_string(), "B".to_string()];
let normalized = normalize_cycle(&cycle);
assert_eq!(normalized, vec!["A", "B", "C"]);
}
#[test]
fn test_process_neighbors_categorizes_correctly() {
let neighbors = vec![
"unvisited".to_string(),
"in_stack".to_string(),
"visited_done".to_string(),
];
let mut visited = HashSet::new();
visited.insert("in_stack".to_string());
visited.insert("visited_done".to_string());
let mut rec_stack = HashSet::new();
rec_stack.insert("in_stack".to_string());
let path = vec!["start".to_string(), "in_stack".to_string()];
let (to_visit, cycle_starts) = process_neighbors(&neighbors, &visited, &rec_stack, &path);
assert!(to_visit.contains(&"unvisited".to_string()));
assert_eq!(cycle_starts.len(), 1);
assert_eq!(cycle_starts[0], 1);
}
#[test]
fn test_process_neighbors_empty() {
let neighbors: Vec<String> = vec![];
let visited = HashSet::new();
let rec_stack = HashSet::new();
let path = vec!["start".to_string()];
let (to_visit, cycle_starts) = process_neighbors(&neighbors, &visited, &rec_stack, &path);
assert!(to_visit.is_empty());
assert!(cycle_starts.is_empty());
}
#[test]
fn test_enter_and_leave_node_state_management() {
let mut visited = HashSet::new();
let mut rec_stack = HashSet::new();
let mut path = Vec::new();
enter_node("A", &mut visited, &mut rec_stack, &mut path);
assert!(visited.contains("A"));
assert!(rec_stack.contains("A"));
assert_eq!(path, vec!["A"]);
leave_node("A", &mut rec_stack, &mut path);
assert!(visited.contains("A")); assert!(!rec_stack.contains("A")); assert!(path.is_empty()); }
#[test]
fn test_try_record_cycle_prevents_duplicates() {
let mut cycles = vec![vec!["A".to_string(), "B".to_string()]];
let path = vec!["X".to_string(), "A".to_string(), "B".to_string()];
try_record_cycle(&mut cycles, &path, 1);
assert_eq!(cycles.len(), 1, "Duplicate should not be added");
}
#[test]
fn test_try_record_cycle_adds_new() {
let mut cycles = vec![vec!["A".to_string(), "B".to_string()]];
let path = vec!["X".to_string(), "C".to_string(), "D".to_string()];
try_record_cycle(&mut cycles, &path, 1);
assert_eq!(cycles.len(), 2, "New cycle should be added");
assert!(cycles.iter().any(|c| c.contains(&"C".to_string())));
}
#[test]
fn test_empty_graph() {
let splits: Vec<ModuleSplit> = vec![];
let cycles = detect_circular_dependencies(&splits);
assert!(cycles.is_empty());
}
}