#![allow(dead_code)]
use std::collections::{HashMap, HashSet, VecDeque};
use anyhow::{bail, Result};
use crate::config::DependencyConfig;
#[derive(Debug, Clone)]
pub struct DependencyNode {
pub name: String,
pub version: String,
pub source: String,
pub dependencies: Vec<String>,
pub depth: usize,
pub config: DependencyConfig,
}
#[derive(Debug, Clone)]
pub struct DependencyGraph {
nodes: HashMap<String, DependencyNode>,
edges: Vec<(String, String)>,
roots: HashSet<String>,
}
impl DependencyGraph {
pub fn new() -> Self {
Self {
nodes: HashMap::new(),
edges: Vec::new(),
roots: HashSet::new(),
}
}
pub fn add_root(&mut self, name: String) {
self.roots.insert(name);
}
pub fn add_node(&mut self, node: DependencyNode) {
self.nodes.insert(node.name.clone(), node);
}
pub fn add_edge(&mut self, from: &str, to: &str) {
self.edges.push((from.to_string(), to.to_string()));
}
pub fn get_node(&self, name: &str) -> Option<&DependencyNode> {
self.nodes.get(name)
}
pub fn nodes(&self) -> &HashMap<String, DependencyNode> {
&self.nodes
}
pub fn roots(&self) -> &HashSet<String> {
&self.roots
}
pub fn detect_cycles(&self) -> Option<Vec<String>> {
let mut visited = HashSet::new();
let mut rec_stack = HashSet::new();
let mut path = Vec::new();
let adj_list = self.build_adjacency_list();
for node_name in self.nodes.keys() {
if !visited.contains(node_name) {
if let Some(cycle) = self.dfs_cycle_detection(
node_name,
&adj_list,
&mut visited,
&mut rec_stack,
&mut path,
) {
return Some(cycle);
}
}
}
None
}
fn dfs_cycle_detection(
&self,
node: &str,
adj_list: &HashMap<String, Vec<String>>,
visited: &mut HashSet<String>,
rec_stack: &mut HashSet<String>,
path: &mut Vec<String>,
) -> Option<Vec<String>> {
visited.insert(node.to_string());
rec_stack.insert(node.to_string());
path.push(node.to_string());
if let Some(neighbors) = adj_list.get(node) {
for neighbor in neighbors {
if !visited.contains(neighbor) {
if let Some(cycle) =
self.dfs_cycle_detection(neighbor, adj_list, visited, rec_stack, path)
{
return Some(cycle);
}
} else if rec_stack.contains(neighbor) {
let cycle_start = path.iter().position(|n| n == neighbor).unwrap();
return Some(path[cycle_start..].to_vec());
}
}
}
rec_stack.remove(node);
path.pop();
None
}
fn build_adjacency_list(&self) -> HashMap<String, Vec<String>> {
let mut adj_list: HashMap<String, Vec<String>> = HashMap::new();
for (from, to) in &self.edges {
adj_list
.entry(from.clone())
.or_insert_with(Vec::new)
.push(to.clone());
}
adj_list
}
pub fn topological_sort(&self) -> Result<Vec<String>> {
if let Some(cycle) = self.detect_cycles() {
bail!(
"Circular dependency detected: {}",
cycle.join(" -> ") + " -> " + &cycle[0]
);
}
let adj_list = self.build_adjacency_list();
let mut in_degree: HashMap<String, usize> = HashMap::new();
for node_name in self.nodes.keys() {
in_degree.insert(node_name.clone(), 0);
}
for neighbors in adj_list.values() {
for neighbor in neighbors {
*in_degree.entry(neighbor.clone()).or_insert(0) += 1;
}
}
let mut queue: VecDeque<String> = in_degree
.iter()
.filter(|(_, °ree)| degree == 0)
.map(|(name, _)| name.clone())
.collect();
let mut sorted = Vec::new();
while let Some(node) = queue.pop_front() {
sorted.push(node.clone());
if let Some(neighbors) = adj_list.get(&node) {
for neighbor in neighbors {
if let Some(degree) = in_degree.get_mut(neighbor) {
*degree -= 1;
if *degree == 0 {
queue.push_back(neighbor.clone());
}
}
}
}
}
if sorted.len() != self.nodes.len() {
bail!("Failed to resolve dependency order - possible circular dependency");
}
Ok(sorted)
}
pub fn format_tree(&self, indent: usize) -> String {
let mut output = String::new();
let mut visited = HashSet::new();
for root_name in &self.roots {
if let Some(node) = self.nodes.get(root_name) {
self.format_node(node, &mut output, &mut visited, 0, indent, true);
}
}
output
}
fn format_node(
&self,
node: &DependencyNode,
output: &mut String,
visited: &mut HashSet<String>,
depth: usize,
indent: usize,
is_last: bool,
) {
let prefix = if depth == 0 {
String::new()
} else {
" ".repeat(depth - 1) + if is_last { "└── " } else { "├── " }
};
let already_visited = visited.contains(&node.name);
let marker = if already_visited {
" (already resolved)"
} else {
""
};
output.push_str(&format!(
"{}{} v{}{}",
prefix, node.name, node.version, marker
));
if !node.source.starts_with("path+") {
let source_info = if node.source.starts_with("git+") {
let url = node
.source
.strip_prefix("git+")
.unwrap_or(&node.source)
.split('#')
.next()
.unwrap_or("");
format!(" (git: {})", url)
} else {
format!(" ({})", node.source)
};
output.push_str(&source_info);
}
output.push('\n');
if already_visited {
return;
}
visited.insert(node.name.clone());
let deps_count = node.dependencies.len();
for (i, dep_name) in node.dependencies.iter().enumerate() {
if let Some(dep_node) = self.nodes.get(dep_name) {
let is_last_child = i == deps_count - 1;
self.format_node(
dep_node,
output,
visited,
depth + 1,
indent,
is_last_child,
);
}
}
}
pub fn stats(&self) -> DependencyStats {
let unique_count = self.nodes.len();
let total_count = self.edges.len() + self.roots.len();
let shared_count = if total_count > unique_count {
total_count - unique_count
} else {
0
};
let max_depth = self.nodes.values().map(|n| n.depth).max().unwrap_or(0);
DependencyStats {
unique_count,
total_count,
shared_count,
max_depth,
}
}
}
impl Default for DependencyGraph {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct DependencyStats {
pub unique_count: usize,
pub total_count: usize,
pub shared_count: usize,
pub max_depth: usize,
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_node(name: &str, version: &str, deps: Vec<&str>) -> DependencyNode {
DependencyNode {
name: name.to_string(),
version: version.to_string(),
source: format!("git+https://github.com/test/{}", name),
dependencies: deps.iter().map(|s| s.to_string()).collect(),
depth: 0,
config: DependencyConfig {
name: name.to_string(),
version: version.to_string(),
git: Some(format!("https://github.com/test/{}", name)),
branch: None,
path: None,
zip: None,
optional: false,
features: vec![],
default_features: Some(true),
workspace: false,
registry: None,
},
}
}
#[test]
fn test_simple_graph() {
let mut graph = DependencyGraph::new();
let node_a = create_test_node("a", "1.0.0", vec!["b", "c"]);
let node_b = create_test_node("b", "1.0.0", vec!["c"]);
let node_c = create_test_node("c", "1.0.0", vec![]);
graph.add_root("a".to_string());
graph.add_node(node_a);
graph.add_node(node_b);
graph.add_node(node_c);
graph.add_edge("b", "a");
graph.add_edge("c", "a");
graph.add_edge("c", "b");
let sorted = graph.topological_sort().unwrap();
let pos_c = sorted.iter().position(|n| n == "c").unwrap();
let pos_b = sorted.iter().position(|n| n == "b").unwrap();
let pos_a = sorted.iter().position(|n| n == "a").unwrap();
assert!(pos_c < pos_b);
assert!(pos_b < pos_a);
}
#[test]
fn test_cycle_detection() {
let mut graph = DependencyGraph::new();
let node_a = create_test_node("a", "1.0.0", vec!["b"]);
let node_b = create_test_node("b", "1.0.0", vec!["c"]);
let node_c = create_test_node("c", "1.0.0", vec!["a"]);
graph.add_node(node_a);
graph.add_node(node_b);
graph.add_node(node_c);
graph.add_edge("b", "a");
graph.add_edge("c", "b");
graph.add_edge("a", "c");
let cycle = graph.detect_cycles();
assert!(cycle.is_some());
let cycle_vec = cycle.unwrap();
assert!(cycle_vec.contains(&"a".to_string()));
assert!(cycle_vec.contains(&"b".to_string()));
assert!(cycle_vec.contains(&"c".to_string()));
}
#[test]
fn test_shared_dependency() {
let mut graph = DependencyGraph::new();
let node_a = create_test_node("a", "1.0.0", vec!["c"]);
let node_b = create_test_node("b", "1.0.0", vec!["c"]);
let node_c = create_test_node("c", "1.0.0", vec![]);
graph.add_root("a".to_string());
graph.add_root("b".to_string());
graph.add_node(node_a);
graph.add_node(node_b);
graph.add_node(node_c);
graph.add_edge("c", "a");
graph.add_edge("c", "b");
let stats = graph.stats();
assert_eq!(stats.unique_count, 3);
assert!(stats.shared_count > 0);
}
}