Skip to main content

nargo_resolver/
resolver.rs

1//! Main dependency resolver implementation.
2
3use nargo_types::{Error, Result, Span};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use tracing::{debug, info, warn};
7
8use crate::{
9    conflict::{Conflict, ConflictDetector, ConflictSolution},
10    graph::{DependencyEdge, DependencyGraph, DependencyNode, PackageSource},
11};
12
13/// Options for dependency resolution.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ResolveOptions {
16    /// Whether to include dev dependencies.
17    pub include_dev: bool,
18    /// Whether to include optional dependencies.
19    pub include_optional: bool,
20    /// Whether to allow prerelease versions.
21    pub allow_prerelease: bool,
22    /// Maximum depth for dependency resolution.
23    pub max_depth: usize,
24    /// Registry URL to use.
25    pub registry: String,
26    /// Number of parallel resolution tasks.
27    pub parallel_jobs: usize,
28    /// Whether to resolve workspace dependencies.
29    pub resolve_workspace: bool,
30    /// Whether to use lock file for resolution.
31    pub use_lock_file: bool,
32}
33
34impl Default for ResolveOptions {
35    fn default() -> Self {
36        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 }
37    }
38}
39
40/// Result of dependency resolution.
41#[derive(Debug, Clone)]
42pub struct ResolveResult {
43    /// The resolved dependency graph.
44    pub graph: DependencyGraph,
45    /// Detected conflicts.
46    pub conflicts: Vec<Conflict>,
47    /// Suggested solutions for conflicts.
48    pub solutions: Vec<ConflictSolution>,
49    /// Resolution statistics.
50    pub stats: ResolveStats,
51}
52
53/// Statistics about the resolution process.
54#[derive(Debug, Clone, Default, Serialize, Deserialize)]
55pub struct ResolveStats {
56    /// Total number of packages resolved.
57    pub total_packages: usize,
58    /// Number of production dependencies.
59    pub production_deps: usize,
60    /// Number of dev dependencies.
61    pub dev_deps: usize,
62    /// Number of peer dependencies.
63    pub peer_deps: usize,
64    /// Number of optional dependencies.
65    pub optional_deps: usize,
66    /// Number of conflicts detected.
67    pub conflicts: usize,
68    /// Number of packages from registry.
69    pub registry_deps: usize,
70    /// Number of git dependencies.
71    pub git_deps: usize,
72    /// Number of path dependencies.
73    pub path_deps: usize,
74    /// Number of workspace dependencies.
75    pub workspace_deps: usize,
76    /// Resolution time in milliseconds.
77    pub resolution_time_ms: u64,
78}
79
80/// Main dependency resolver.
81#[derive(Debug)]
82pub struct Resolver {
83    /// Resolution options.
84    options: ResolveOptions,
85    /// Conflict detector.
86    conflict_detector: ConflictDetector,
87    /// Cache of resolved packages.
88    resolved_cache: HashMap<String, DependencyNode>,
89    /// Workspace packages cache.
90    workspace_packages: HashMap<String, String>,
91}
92
93impl Resolver {
94    /// Creates a new resolver with default options.
95    pub fn new() -> Self {
96        Self::with_options(ResolveOptions::default())
97    }
98
99    /// Creates a new resolver with custom options.
100    pub fn with_options(options: ResolveOptions) -> Self {
101        Self { options, conflict_detector: ConflictDetector::new(), resolved_cache: HashMap::new(), workspace_packages: HashMap::new() }
102    }
103
104    /// Sets the workspace packages for resolution.
105    pub fn with_workspace_packages(mut self, packages: HashMap<String, String>) -> Self {
106        self.workspace_packages = packages;
107        self
108    }
109
110    /// Resolves dependencies from a Nargo.toml configuration.
111    pub async fn resolve(&mut self, dependencies: &HashMap<String, nargo_config::Dependency>, dev_dependencies: &HashMap<String, nargo_config::Dependency>) -> Result<ResolveResult> {
112        let start = std::time::Instant::now();
113        let mut graph = DependencyGraph::new();
114
115        info!("Starting dependency resolution with {} parallel jobs...", self.options.parallel_jobs);
116
117        let mut version_map: HashMap<String, Vec<(String, String)>> = HashMap::new();
118        let mut peer_deps: HashMap<String, (String, Option<String>)> = HashMap::new();
119        let mut resolved_versions: HashMap<String, String> = HashMap::new();
120
121        self.resolve_deps_recursive(dependencies, false, &mut graph, &mut version_map, &mut resolved_versions, 0).await?;
122
123        if self.options.include_dev {
124            self.resolve_deps_recursive(dev_dependencies, true, &mut graph, &mut version_map, &mut resolved_versions, 0).await?;
125        }
126
127        self.conflict_detector.detect_version_conflicts(&version_map);
128        self.conflict_detector.detect_peer_conflicts(&peer_deps, &resolved_versions);
129
130        let conflicts: Vec<Conflict> = self.conflict_detector.conflicts().to_vec();
131        let solutions = self.conflict_detector.find_solutions();
132
133        let stats = self.calculate_stats(&graph, &conflicts, start.elapsed().as_millis() as u64);
134
135        info!("Resolution complete: {} packages, {} conflicts in {}ms", stats.total_packages, stats.conflicts, stats.resolution_time_ms);
136
137        Ok(ResolveResult { graph, conflicts, solutions, stats })
138    }
139
140    /// Resolves a single dependency.
141    pub async fn resolve_single(&mut self, name: &str, dependency: &nargo_config::Dependency) -> Result<DependencyNode> {
142        let (version, source) = self.parse_dependency(dependency)?;
143
144        let mut node = DependencyNode::new(name, &version).with_source(source);
145
146        if let nargo_config::Dependency::Detailed(detail) = dependency {
147            if !detail.features.is_empty() {
148                node = node.with_features(detail.features.clone());
149            }
150            if detail.optional {
151                node = node.as_optional();
152            }
153        }
154
155        Ok(node)
156    }
157
158    /// Performs topological sort on resolved dependencies.
159    pub fn topological_sort(&self, graph: &DependencyGraph) -> Result<Vec<DependencyNode>> {
160        graph.topological_sort()
161    }
162
163    /// Detects circular dependencies.
164    pub fn detect_cycles(&self, graph: &DependencyGraph) -> Option<Vec<String>> {
165        graph.detect_cycle()
166    }
167
168    #[allow(clippy::too_many_arguments)]
169    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<()> {
170        if depth >= self.options.max_depth {
171            warn!("Max depth {} reached, stopping resolution", depth);
172            return Ok(());
173        }
174
175        for (name, dep) in deps {
176            if self.resolved_cache.contains_key(name) {
177                debug!("Using cached resolution for {}", name);
178                continue;
179            }
180
181            let (version, source) = self.parse_dependency(dep)?;
182
183            if self.options.resolve_workspace && self.is_workspace_dependency(dep) {
184                if let Some(ws_version) = self.workspace_packages.get(name) {
185                    version_map.entry(name.clone()).or_default().push((ws_version.clone(), "workspace".to_string()));
186                    resolved_versions.insert(name.clone(), ws_version.clone());
187                    continue;
188                }
189            }
190
191            version_map.entry(name.clone()).or_default().push((version.clone(), "root".to_string()));
192
193            resolved_versions.insert(name.clone(), version.clone());
194
195            let mut node = DependencyNode::new(name, &version).with_source(source).as_dev(is_dev);
196
197            if let nargo_config::Dependency::Detailed(detail) = dep {
198                if !detail.features.is_empty() {
199                    node = node.with_features(detail.features.clone());
200                }
201                if detail.optional {
202                    node = node.as_optional();
203                }
204            }
205
206            let node_idx = graph.add_node(node.clone());
207            self.resolved_cache.insert(name.clone(), node);
208
209            if depth + 1 < self.options.max_depth {
210                self.resolve_transitive_deps(name, &version, node_idx, is_dev, graph, version_map, resolved_versions, depth + 1).await?;
211            }
212        }
213
214        Ok(())
215    }
216
217    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<()> {
218        Ok(())
219    }
220
221    fn parse_dependency(&self, dep: &nargo_config::Dependency) -> Result<(String, PackageSource)> {
222        match dep {
223            nargo_config::Dependency::Version(v) => {
224                if v.starts_with("workspace:") {
225                    Ok((v.clone(), PackageSource::Workspace))
226                }
227                else if v.starts_with("file:") || v.starts_with("./") || v.starts_with("../") {
228                    let path = v.strip_prefix("file:").unwrap_or(v);
229                    Ok(("path".to_string(), PackageSource::Path { path: path.to_string() }))
230                }
231                else if v.starts_with("git+") || v.starts_with("git://") {
232                    let url = v.strip_prefix("git+").unwrap_or(v);
233                    Ok(("git".to_string(), PackageSource::Git { url: url.to_string(), reference: None }))
234                }
235                else if v.starts_with("github:") {
236                    let repo = v.strip_prefix("github:").unwrap_or(v);
237                    Ok(("github".to_string(), PackageSource::Github { repo: repo.to_string(), reference: None }))
238                }
239                else {
240                    Ok((v.clone(), PackageSource::Registry { registry: self.options.registry.clone() }))
241                }
242            }
243            nargo_config::Dependency::Detailed(detail) => {
244                if detail.workspace.unwrap_or(false) {
245                    Ok(("workspace:*".to_string(), PackageSource::Workspace))
246                }
247                else if let Some(ref version) = detail.version {
248                    Ok((version.clone(), PackageSource::Registry { registry: detail.registry.clone().unwrap_or_else(|| self.options.registry.clone()) }))
249                }
250                else if let Some(ref git) = detail.git {
251                    let reference = detail.tag.clone().or_else(|| detail.branch.clone()).or_else(|| detail.rev.clone());
252                    Ok(("git".to_string(), PackageSource::Git { url: git.clone(), reference }))
253                }
254                else if let Some(ref path) = detail.path {
255                    Ok(("path".to_string(), PackageSource::Path { path: path.to_string_lossy().to_string() }))
256                }
257                else {
258                    Err(Error::external_error("resolver".to_string(), "Invalid dependency specification".to_string(), Span::unknown()))
259                }
260            }
261        }
262    }
263
264    fn is_workspace_dependency(&self, dep: &nargo_config::Dependency) -> bool {
265        match dep {
266            nargo_config::Dependency::Version(v) => v.starts_with("workspace:"),
267            nargo_config::Dependency::Detailed(d) => d.workspace.unwrap_or(false),
268        }
269    }
270
271    fn calculate_stats(&self, graph: &DependencyGraph, conflicts: &[Conflict], resolution_time_ms: u64) -> ResolveStats {
272        let nodes: Vec<&DependencyNode> = graph.nodes().collect();
273
274        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 }
275    }
276
277    /// Clears the resolver cache.
278    pub fn clear_cache(&mut self) {
279        self.resolved_cache.clear();
280        self.conflict_detector.clear();
281    }
282
283    /// Returns the resolved cache.
284    pub fn cache(&self) -> &HashMap<String, DependencyNode> {
285        &self.resolved_cache
286    }
287}
288
289impl Default for Resolver {
290    fn default() -> Self {
291        Self::new()
292    }
293}