use crate::core::{CircularDependency, Dependency, ModuleDependency};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct DependencyGraph {
adjacency: HashMap<String, Vec<String>>,
modules: Vec<String>,
}
impl Default for DependencyGraph {
fn default() -> Self {
Self::new()
}
}
impl DependencyGraph {
pub fn new() -> Self {
Self {
adjacency: HashMap::new(),
modules: Vec::new(),
}
}
pub fn add_module(&mut self, module: String) {
if !self.adjacency.contains_key(&module) {
self.adjacency.insert(module.clone(), Vec::new());
if !self.modules.contains(&module) {
self.modules.push(module);
}
}
}
pub fn add_dependency(&mut self, from: String, to: String) {
self.add_module(from.clone());
self.add_module(to.clone());
self.adjacency.entry(from).or_default().push(to);
}
pub fn detect_circular_dependencies(&self) -> Vec<CircularDependency> {
let mut visited: HashMap<String, bool> =
self.modules.iter().map(|m| (m.clone(), false)).collect();
let mut rec_stack: HashMap<String, bool> =
self.modules.iter().map(|m| (m.clone(), false)).collect();
let mut circular_deps = Vec::new();
for module in &self.modules {
if !visited.get(module).copied().unwrap_or(false) {
self.dfs_detect_cycles_iterative(
module,
&mut visited,
&mut rec_stack,
&mut circular_deps,
);
}
}
circular_deps
}
fn dfs_detect_cycles_iterative(
&self,
start_module: &str,
visited: &mut HashMap<String, bool>,
rec_stack: &mut HashMap<String, bool>,
cycles: &mut Vec<CircularDependency>,
) {
let mut stack: Vec<(String, usize, bool)> = vec![(start_module.to_string(), 0, true)];
let mut path: Vec<String> = Vec::new();
while let Some((module, _dep_idx, is_entering)) = stack.pop() {
if is_entering {
visited.insert(module.clone(), true);
rec_stack.insert(module.clone(), true);
path.push(module.clone());
stack.push((module.clone(), 0, false));
if let Some(deps) = self.adjacency.get(&module) {
for (i, dep) in deps.iter().enumerate().rev() {
if !visited.get(dep).copied().unwrap_or(false) {
stack.push((dep.clone(), i, true));
} else if rec_stack.get(dep).copied().unwrap_or(false) {
if let Some(cycle_start) = path.iter().position(|m| m == dep) {
let cycle = path[cycle_start..].to_vec();
cycles.push(CircularDependency { cycle });
}
}
}
}
} else {
path.pop();
rec_stack.insert(module, false);
}
}
}
pub fn calculate_coupling_metrics(&self) -> Vec<ModuleDependency> {
self.modules
.iter()
.map(|module| {
let dependencies = self.get_dependencies(module);
let dependents = self.get_dependents(module);
ModuleDependency {
module: module.clone(),
dependencies,
dependents,
}
})
.collect()
}
pub fn module_count(&self) -> usize {
self.modules.len()
}
pub fn dependency_count(&self) -> usize {
self.adjacency.values().map(|deps| deps.len()).sum()
}
pub fn has_module(&self, module: &str) -> bool {
self.modules.contains(&module.to_string())
}
pub fn get_dependencies(&self, module: &str) -> Vec<String> {
self.adjacency.get(module).cloned().unwrap_or_default()
}
pub fn get_dependents(&self, module: &str) -> Vec<String> {
self.adjacency
.iter()
.filter_map(|(other_module, deps)| {
(deps.contains(&module.to_string()) && other_module != module)
.then_some(other_module.clone())
})
.collect()
}
}
pub fn build_dependency_graph(dependencies: &[Dependency]) -> DependencyGraph {
dependencies
.iter()
.filter_map(|dep| extract_module_from_dependency(&dep.name))
.fold(DependencyGraph::new(), |mut graph, module| {
graph.add_module(module);
graph
})
}
fn extract_module_from_dependency(dep_name: &str) -> Option<String> {
dep_name.split("::").next().map(|s| s.to_string())
}
pub fn analyze_module_dependencies(
files: &[(std::path::PathBuf, Vec<Dependency>)],
) -> DependencyGraph {
files.iter().fold(
DependencyGraph::new(),
|mut graph, (file_path, file_deps)| {
let module_name = extract_module_name(file_path);
graph.add_module(module_name.clone());
for dep_module in file_deps
.iter()
.filter_map(|dep| extract_module_from_dependency(&dep.name))
.filter(|dep_module| *dep_module != module_name)
{
graph.add_dependency(module_name.clone(), dep_module);
}
graph
},
)
}
fn extract_module_name(path: &std::path::Path) -> String {
path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circular_dependency_detection() {
let mut graph = DependencyGraph::new();
graph.add_dependency("A".to_string(), "B".to_string());
graph.add_dependency("B".to_string(), "C".to_string());
graph.add_dependency("C".to_string(), "A".to_string());
let circular = graph.detect_circular_dependencies();
assert_eq!(circular.len(), 1);
assert_eq!(circular[0].cycle.len(), 3);
}
#[test]
fn test_self_dependency() {
let mut graph = DependencyGraph::new();
graph.add_dependency("A".to_string(), "A".to_string());
let circular = graph.detect_circular_dependencies();
assert_eq!(circular.len(), 1);
assert_eq!(circular[0].cycle.len(), 1);
assert_eq!(circular[0].cycle[0], "A");
}
#[test]
fn test_coupling_metrics() {
let mut graph = DependencyGraph::new();
graph.add_dependency("A".to_string(), "B".to_string());
graph.add_dependency("A".to_string(), "C".to_string());
graph.add_dependency("B".to_string(), "C".to_string());
graph.add_dependency("D".to_string(), "A".to_string());
let metrics = graph.calculate_coupling_metrics();
let a_metrics = metrics.iter().find(|m| m.module == "A").unwrap();
assert_eq!(a_metrics.dependencies.len(), 2); assert_eq!(a_metrics.dependents.len(), 1); }
#[test]
fn test_large_dependency_chain_no_stack_overflow() {
let mut graph = DependencyGraph::new();
let num_modules = 4000;
for i in 0..num_modules - 1 {
graph.add_dependency(format!("mod_{}", i), format!("mod_{}", i + 1));
}
let circular = graph.detect_circular_dependencies();
assert!(circular.is_empty(), "Linear chain should have no cycles");
}
#[test]
fn test_large_graph_with_cycle_no_stack_overflow() {
let mut graph = DependencyGraph::new();
let num_modules = 4000;
for i in 0..num_modules - 1 {
graph.add_dependency(format!("mod_{}", i), format!("mod_{}", i + 1));
}
graph.add_dependency(format!("mod_{}", num_modules - 1), "mod_0".to_string());
let circular = graph.detect_circular_dependencies();
assert_eq!(circular.len(), 1, "Should detect exactly one cycle");
assert_eq!(
circular[0].cycle.len(),
num_modules,
"Cycle should include all modules"
);
}
#[test]
fn test_wide_graph_no_stack_overflow() {
let mut graph = DependencyGraph::new();
for chain in 0..100 {
for i in 0..99 {
graph.add_dependency(
format!("chain_{}_mod_{}", chain, i),
format!("chain_{}_mod_{}", chain, i + 1),
);
}
}
let circular = graph.detect_circular_dependencies();
assert!(
circular.is_empty(),
"Independent chains should have no cycles"
);
}
}