use crate::error::{VersionError, VersionResult};
use crate::types::{CircularDependency, PackageInfo};
use petgraph::graph::{DiGraph, NodeIndex};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct DependencyGraph {
graph: DiGraph<String, ()>,
node_map: HashMap<String, NodeIndex>,
}
impl DependencyGraph {
pub fn from_packages(packages: &[PackageInfo]) -> VersionResult<Self> {
let mut graph = DiGraph::new();
let mut node_map = HashMap::new();
for pkg in packages {
let name = pkg.name().to_string();
let idx = graph.add_node(name.clone());
node_map.insert(name, idx);
}
for pkg in packages {
let from_name = pkg.name();
let from_idx =
node_map.get(from_name).copied().ok_or_else(|| VersionError::PackageNotFound {
name: from_name.to_string(),
workspace_root: PathBuf::new(),
})?;
let dependencies = pkg.all_dependencies();
for (dep_name, _version_spec, _dep_type) in dependencies {
if let Some(&to_idx) = node_map.get(&dep_name) {
graph.add_edge(from_idx, to_idx, ());
}
}
}
Ok(Self { graph, node_map })
}
#[must_use]
pub fn dependents(&self, package: &str) -> Vec<String> {
if let Some(&idx) = self.node_map.get(package) {
self.graph
.neighbors_directed(idx, petgraph::Direction::Incoming)
.map(|neighbor_idx| self.graph[neighbor_idx].clone())
.collect()
} else {
Vec::new()
}
}
#[must_use]
pub fn dependencies(&self, package: &str) -> Vec<String> {
if let Some(&idx) = self.node_map.get(package) {
self.graph
.neighbors_directed(idx, petgraph::Direction::Outgoing)
.map(|neighbor_idx| self.graph[neighbor_idx].clone())
.collect()
} else {
Vec::new()
}
}
#[must_use]
pub fn contains(&self, package: &str) -> bool {
self.node_map.contains_key(package)
}
#[must_use]
pub fn package_count(&self) -> usize {
self.node_map.len()
}
#[must_use]
pub fn edge_count(&self) -> usize {
self.graph.edge_count()
}
#[must_use]
pub fn all_packages(&self) -> Vec<String> {
self.node_map.keys().cloned().collect()
}
#[must_use]
pub fn detect_cycles(&self) -> Vec<CircularDependency> {
use petgraph::algo::tarjan_scc;
let sccs = tarjan_scc(&self.graph);
sccs.into_iter()
.filter(|scc| scc.len() > 1)
.map(|scc| {
let cycle = scc.iter().map(|&idx| self.graph[idx].clone()).collect();
CircularDependency::new(cycle)
})
.collect()
}
#[must_use]
pub fn transitive_dependents(&self, package: &str) -> Vec<String> {
use petgraph::visit::Bfs;
use std::collections::HashSet;
let Some(&start_idx) = self.node_map.get(package) else {
return Vec::new();
};
let mut visited = HashSet::new();
let mut bfs = Bfs::new(&self.graph, start_idx);
let _ = bfs.next(&self.graph);
let mut result = Vec::new();
let mut to_visit = vec![start_idx];
visited.insert(start_idx);
while let Some(current) = to_visit.pop() {
for neighbor in self.graph.neighbors_directed(current, petgraph::Direction::Incoming) {
if visited.insert(neighbor) {
result.push(self.graph[neighbor].clone());
to_visit.push(neighbor);
}
}
}
result
}
#[must_use]
pub fn transitive_dependencies(&self, package: &str) -> Vec<String> {
use petgraph::visit::Bfs;
let Some(&start_idx) = self.node_map.get(package) else {
return Vec::new();
};
let mut result = Vec::new();
let mut bfs = Bfs::new(&self.graph, start_idx);
let _ = bfs.next(&self.graph);
while let Some(node_idx) = bfs.next(&self.graph) {
result.push(self.graph[node_idx].clone());
}
result
}
}