use nargo_types::{Error, Result};
use petgraph::{
algo::{is_cyclic_directed, toposort},
graph::{DiGraph, NodeIndex},
visit::EdgeRef,
Direction,
};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PackageSource {
Registry {
registry: String,
},
Git {
url: String,
reference: Option<String>,
},
Path {
path: String,
},
Github {
repo: String,
reference: Option<String>,
},
Workspace,
}
impl Default for PackageSource {
fn default() -> Self {
PackageSource::Registry { registry: "https://registry.npmjs.org".to_string() }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyNode {
pub name: String,
pub version: String,
pub source: PackageSource,
pub is_dev: bool,
pub is_optional: bool,
pub features: Vec<String>,
}
impl DependencyNode {
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
Self { name: name.into(), version: version.into(), source: PackageSource::default(), is_dev: false, is_optional: false, features: Vec::new() }
}
pub fn with_source(mut self, source: PackageSource) -> Self {
self.source = source;
self
}
pub fn as_dev(mut self, is_dev: bool) -> Self {
self.is_dev = is_dev;
self
}
pub fn set_dev(mut self) -> Self {
self.is_dev = true;
self
}
pub fn as_optional(mut self) -> Self {
self.is_optional = true;
self
}
pub fn with_features(mut self, features: Vec<String>) -> Self {
self.features = features;
self
}
pub fn key(&self) -> String {
format!("{}@{}", self.name, self.version)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyEdge {
pub constraint: String,
pub is_peer: bool,
}
impl DependencyEdge {
pub fn new(constraint: impl Into<String>) -> Self {
Self { constraint: constraint.into(), is_peer: false }
}
pub fn as_peer(mut self) -> Self {
self.is_peer = true;
self
}
}
#[derive(Debug, Default, Clone)]
pub struct DependencyGraph {
graph: DiGraph<DependencyNode, DependencyEdge>,
name_to_index: HashMap<String, NodeIndex>,
key_to_index: HashMap<String, NodeIndex>,
}
impl DependencyGraph {
pub fn new() -> Self {
Self { graph: DiGraph::new(), name_to_index: HashMap::new(), key_to_index: HashMap::new() }
}
pub fn add_node(&mut self, node: DependencyNode) -> NodeIndex {
let key = node.key();
let name = node.name.clone();
let idx = self.graph.add_node(node);
self.name_to_index.insert(name, idx);
self.key_to_index.insert(key, idx);
idx
}
pub fn add_edge(&mut self, from: NodeIndex, to: NodeIndex, edge: DependencyEdge) {
self.graph.add_edge(from, to, edge);
}
pub fn get_node_by_name(&self, name: &str) -> Option<&DependencyNode> {
self.name_to_index.get(name).map(|idx| &self.graph[*idx])
}
pub fn get_node_by_key(&self, key: &str) -> Option<&DependencyNode> {
self.key_to_index.get(key).map(|idx| &self.graph[*idx])
}
pub fn get_node(&self, idx: NodeIndex) -> Option<&DependencyNode> {
self.graph.node_weight(idx)
}
pub fn nodes(&self) -> impl Iterator<Item = &DependencyNode> {
self.graph.node_weights()
}
pub fn node_count(&self) -> usize {
self.graph.node_count()
}
pub fn edge_count(&self) -> usize {
self.graph.edge_count()
}
pub fn has_cycle(&self) -> bool {
is_cyclic_directed(&self.graph)
}
pub fn detect_cycle(&self) -> Option<Vec<String>> {
if !self.has_cycle() {
return None;
}
let mut visited = HashSet::new();
let mut path = Vec::new();
let mut result = None;
for node_idx in self.graph.node_indices() {
if result.is_some() {
break;
}
self.dfs_cycle(node_idx, &mut visited, &mut path, &mut result);
}
result
}
fn dfs_cycle(&self, node: NodeIndex, visited: &mut HashSet<NodeIndex>, path: &mut Vec<NodeIndex>, result: &mut Option<Vec<String>>) {
if result.is_some() {
return;
}
if path.contains(&node) {
let cycle_start = path.iter().position(|&n| n == node).unwrap();
let cycle: Vec<String> = path[cycle_start..].iter().chain(std::iter::once(&node)).map(|&idx| self.graph[idx].key()).collect();
*result = Some(cycle);
return;
}
if visited.contains(&node) {
return;
}
visited.insert(node);
path.push(node);
for neighbor in self.graph.neighbors_directed(node, Direction::Outgoing) {
self.dfs_cycle(neighbor, visited, path, result);
}
path.pop();
}
pub fn topological_sort(&self) -> Result<Vec<DependencyNode>> {
if self.has_cycle() {
let cycle = self.detect_cycle();
let msg = if let Some(cycle) = cycle { format!("Circular dependency detected: {}", cycle.join(" -> ")) } else { "Circular dependency detected".to_string() };
return Err(Error::external_error("resolver".to_string(), msg, nargo_types::Span::unknown()));
}
let sorted: Vec<NodeIndex> = toposort(&self.graph, None).map_err(|e| Error::external_error("resolver".to_string(), format!("Topological sort failed: {:?}", e), nargo_types::Span::unknown()))?;
Ok(sorted.into_iter().filter_map(|idx| self.graph.node_weight(idx).cloned()).collect())
}
pub fn dependencies_of(&self, node_idx: NodeIndex) -> Vec<(&DependencyNode, &DependencyEdge)> {
self.graph
.edges_directed(node_idx, Direction::Outgoing)
.filter_map(|edge| {
let target = edge.target();
self.graph.node_weight(target).map(|node| (node, edge.weight()))
})
.collect()
}
pub fn dependents_of(&self, node_idx: NodeIndex) -> Vec<(&DependencyNode, &DependencyEdge)> {
self.graph
.edges_directed(node_idx, Direction::Incoming)
.filter_map(|edge| {
let source = edge.source();
self.graph.node_weight(source).map(|node| (node, edge.weight()))
})
.collect()
}
pub fn node_index(&self, name: &str) -> Option<NodeIndex> {
self.name_to_index.get(name).copied()
}
}