nargo-resolver 0.0.0

Nargo dependency resolver
Documentation
//! Main dependency resolver implementation.

use nargo_types::{Error, Result, Span};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::{debug, info, warn};

use crate::{
    conflict::{Conflict, ConflictDetector, ConflictSolution},
    graph::{DependencyEdge, DependencyGraph, DependencyNode, PackageSource},
};

/// Options for dependency resolution.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolveOptions {
    /// Whether to include dev dependencies.
    pub include_dev: bool,
    /// Whether to include optional dependencies.
    pub include_optional: bool,
    /// Whether to allow prerelease versions.
    pub allow_prerelease: bool,
    /// Maximum depth for dependency resolution.
    pub max_depth: usize,
    /// Registry URL to use.
    pub registry: String,
    /// Number of parallel resolution tasks.
    pub parallel_jobs: usize,
    /// Whether to resolve workspace dependencies.
    pub resolve_workspace: bool,
    /// Whether to use lock file for resolution.
    pub use_lock_file: bool,
}

impl Default for ResolveOptions {
    fn default() -> Self {
        Self { include_dev: true, include_optional: true, allow_prerelease: false, max_depth: 100, registry: "https://registry.npmjs.org".to_string(), parallel_jobs: num_cpus::get(), resolve_workspace: true, use_lock_file: true }
    }
}

/// Result of dependency resolution.
#[derive(Debug, Clone)]
pub struct ResolveResult {
    /// The resolved dependency graph.
    pub graph: DependencyGraph,
    /// Detected conflicts.
    pub conflicts: Vec<Conflict>,
    /// Suggested solutions for conflicts.
    pub solutions: Vec<ConflictSolution>,
    /// Resolution statistics.
    pub stats: ResolveStats,
}

/// Statistics about the resolution process.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ResolveStats {
    /// Total number of packages resolved.
    pub total_packages: usize,
    /// Number of production dependencies.
    pub production_deps: usize,
    /// Number of dev dependencies.
    pub dev_deps: usize,
    /// Number of peer dependencies.
    pub peer_deps: usize,
    /// Number of optional dependencies.
    pub optional_deps: usize,
    /// Number of conflicts detected.
    pub conflicts: usize,
    /// Number of packages from registry.
    pub registry_deps: usize,
    /// Number of git dependencies.
    pub git_deps: usize,
    /// Number of path dependencies.
    pub path_deps: usize,
    /// Number of workspace dependencies.
    pub workspace_deps: usize,
    /// Resolution time in milliseconds.
    pub resolution_time_ms: u64,
}

/// Main dependency resolver.
#[derive(Debug)]
pub struct Resolver {
    /// Resolution options.
    options: ResolveOptions,
    /// Conflict detector.
    conflict_detector: ConflictDetector,
    /// Cache of resolved packages.
    resolved_cache: HashMap<String, DependencyNode>,
    /// Workspace packages cache.
    workspace_packages: HashMap<String, String>,
}

impl Resolver {
    /// Creates a new resolver with default options.
    pub fn new() -> Self {
        Self::with_options(ResolveOptions::default())
    }

    /// Creates a new resolver with custom options.
    pub fn with_options(options: ResolveOptions) -> Self {
        Self { options, conflict_detector: ConflictDetector::new(), resolved_cache: HashMap::new(), workspace_packages: HashMap::new() }
    }

    /// Sets the workspace packages for resolution.
    pub fn with_workspace_packages(mut self, packages: HashMap<String, String>) -> Self {
        self.workspace_packages = packages;
        self
    }

    /// Resolves dependencies from a Nargo.toml configuration.
    pub async fn resolve(&mut self, dependencies: &HashMap<String, nargo_config::Dependency>, dev_dependencies: &HashMap<String, nargo_config::Dependency>) -> Result<ResolveResult> {
        let start = std::time::Instant::now();
        let mut graph = DependencyGraph::new();

        info!("Starting dependency resolution with {} parallel jobs...", self.options.parallel_jobs);

        let mut version_map: HashMap<String, Vec<(String, String)>> = HashMap::new();
        let mut peer_deps: HashMap<String, (String, Option<String>)> = HashMap::new();
        let mut resolved_versions: HashMap<String, String> = HashMap::new();

        self.resolve_deps_recursive(dependencies, false, &mut graph, &mut version_map, &mut resolved_versions, 0).await?;

        if self.options.include_dev {
            self.resolve_deps_recursive(dev_dependencies, true, &mut graph, &mut version_map, &mut resolved_versions, 0).await?;
        }

        self.conflict_detector.detect_version_conflicts(&version_map);
        self.conflict_detector.detect_peer_conflicts(&peer_deps, &resolved_versions);

        let conflicts: Vec<Conflict> = self.conflict_detector.conflicts().to_vec();
        let solutions = self.conflict_detector.find_solutions();

        let stats = self.calculate_stats(&graph, &conflicts, start.elapsed().as_millis() as u64);

        info!("Resolution complete: {} packages, {} conflicts in {}ms", stats.total_packages, stats.conflicts, stats.resolution_time_ms);

        Ok(ResolveResult { graph, conflicts, solutions, stats })
    }

    /// Resolves a single dependency.
    pub async fn resolve_single(&mut self, name: &str, dependency: &nargo_config::Dependency) -> Result<DependencyNode> {
        let (version, source) = self.parse_dependency(dependency)?;

        let mut node = DependencyNode::new(name, &version).with_source(source);

        if let nargo_config::Dependency::Detailed(detail) = dependency {
            if !detail.features.is_empty() {
                node = node.with_features(detail.features.clone());
            }
            if detail.optional {
                node = node.as_optional();
            }
        }

        Ok(node)
    }

    /// Performs topological sort on resolved dependencies.
    pub fn topological_sort(&self, graph: &DependencyGraph) -> Result<Vec<DependencyNode>> {
        graph.topological_sort()
    }

    /// Detects circular dependencies.
    pub fn detect_cycles(&self, graph: &DependencyGraph) -> Option<Vec<String>> {
        graph.detect_cycle()
    }

    #[allow(clippy::too_many_arguments)]
    async fn resolve_deps_recursive(&mut self, deps: &HashMap<String, nargo_config::Dependency>, is_dev: bool, graph: &mut DependencyGraph, version_map: &mut HashMap<String, Vec<(String, String)>>, resolved_versions: &mut HashMap<String, String>, depth: usize) -> Result<()> {
        if depth >= self.options.max_depth {
            warn!("Max depth {} reached, stopping resolution", depth);
            return Ok(());
        }

        for (name, dep) in deps {
            if self.resolved_cache.contains_key(name) {
                debug!("Using cached resolution for {}", name);
                continue;
            }

            let (version, source) = self.parse_dependency(dep)?;

            if self.options.resolve_workspace && self.is_workspace_dependency(dep) {
                if let Some(ws_version) = self.workspace_packages.get(name) {
                    version_map.entry(name.clone()).or_default().push((ws_version.clone(), "workspace".to_string()));
                    resolved_versions.insert(name.clone(), ws_version.clone());
                    continue;
                }
            }

            version_map.entry(name.clone()).or_default().push((version.clone(), "root".to_string()));

            resolved_versions.insert(name.clone(), version.clone());

            let mut node = DependencyNode::new(name, &version).with_source(source).as_dev(is_dev);

            if let nargo_config::Dependency::Detailed(detail) = dep {
                if !detail.features.is_empty() {
                    node = node.with_features(detail.features.clone());
                }
                if detail.optional {
                    node = node.as_optional();
                }
            }

            let node_idx = graph.add_node(node.clone());
            self.resolved_cache.insert(name.clone(), node);

            if depth + 1 < self.options.max_depth {
                self.resolve_transitive_deps(name, &version, node_idx, is_dev, graph, version_map, resolved_versions, depth + 1).await?;
            }
        }

        Ok(())
    }

    async fn resolve_transitive_deps(&mut self, _parent_name: &str, _parent_version: &str, _parent_idx: petgraph::graph::NodeIndex, _is_dev: bool, _graph: &mut DependencyGraph, _version_map: &mut HashMap<String, Vec<(String, String)>>, _resolved_versions: &mut HashMap<String, String>, _depth: usize) -> Result<()> {
        Ok(())
    }

    fn parse_dependency(&self, dep: &nargo_config::Dependency) -> Result<(String, PackageSource)> {
        match dep {
            nargo_config::Dependency::Version(v) => {
                if v.starts_with("workspace:") {
                    Ok((v.clone(), PackageSource::Workspace))
                }
                else if v.starts_with("file:") || v.starts_with("./") || v.starts_with("../") {
                    let path = v.strip_prefix("file:").unwrap_or(v);
                    Ok(("path".to_string(), PackageSource::Path { path: path.to_string() }))
                }
                else if v.starts_with("git+") || v.starts_with("git://") {
                    let url = v.strip_prefix("git+").unwrap_or(v);
                    Ok(("git".to_string(), PackageSource::Git { url: url.to_string(), reference: None }))
                }
                else if v.starts_with("github:") {
                    let repo = v.strip_prefix("github:").unwrap_or(v);
                    Ok(("github".to_string(), PackageSource::Github { repo: repo.to_string(), reference: None }))
                }
                else {
                    Ok((v.clone(), PackageSource::Registry { registry: self.options.registry.clone() }))
                }
            }
            nargo_config::Dependency::Detailed(detail) => {
                if detail.workspace.unwrap_or(false) {
                    Ok(("workspace:*".to_string(), PackageSource::Workspace))
                }
                else if let Some(ref version) = detail.version {
                    Ok((version.clone(), PackageSource::Registry { registry: detail.registry.clone().unwrap_or_else(|| self.options.registry.clone()) }))
                }
                else if let Some(ref git) = detail.git {
                    let reference = detail.tag.clone().or_else(|| detail.branch.clone()).or_else(|| detail.rev.clone());
                    Ok(("git".to_string(), PackageSource::Git { url: git.clone(), reference }))
                }
                else if let Some(ref path) = detail.path {
                    Ok(("path".to_string(), PackageSource::Path { path: path.to_string_lossy().to_string() }))
                }
                else {
                    Err(Error::external_error("resolver".to_string(), "Invalid dependency specification".to_string(), Span::unknown()))
                }
            }
        }
    }

    fn is_workspace_dependency(&self, dep: &nargo_config::Dependency) -> bool {
        match dep {
            nargo_config::Dependency::Version(v) => v.starts_with("workspace:"),
            nargo_config::Dependency::Detailed(d) => d.workspace.unwrap_or(false),
        }
    }

    fn calculate_stats(&self, graph: &DependencyGraph, conflicts: &[Conflict], resolution_time_ms: u64) -> ResolveStats {
        let nodes: Vec<&DependencyNode> = graph.nodes().collect();

        ResolveStats { total_packages: nodes.len(), production_deps: nodes.iter().filter(|n| !n.is_dev && !n.is_optional).count(), dev_deps: nodes.iter().filter(|n| n.is_dev).count(), peer_deps: 0, optional_deps: nodes.iter().filter(|n| n.is_optional).count(), conflicts: conflicts.len(), registry_deps: nodes.iter().filter(|n| matches!(n.source, PackageSource::Registry { .. })).count(), git_deps: nodes.iter().filter(|n| matches!(n.source, PackageSource::Git { .. })).count(), path_deps: nodes.iter().filter(|n| matches!(n.source, PackageSource::Path { .. })).count(), workspace_deps: nodes.iter().filter(|n| matches!(n.source, PackageSource::Workspace)).count(), resolution_time_ms }
    }

    /// Clears the resolver cache.
    pub fn clear_cache(&mut self) {
        self.resolved_cache.clear();
        self.conflict_detector.clear();
    }

    /// Returns the resolved cache.
    pub fn cache(&self) -> &HashMap<String, DependencyNode> {
        &self.resolved_cache
    }
}

impl Default for Resolver {
    fn default() -> Self {
        Self::new()
    }
}