cuenv_workspaces/
resolver.rs

1//! Dependency resolution implementation for workspaces.
2//!
3//! This module provides the [`GenericDependencyResolver`] which implements the
4//! [`DependencyResolver`] trait. It builds dependency graphs by combining
5//! workspace configuration (manifests) with resolved lockfile data.
6
7use crate::core::traits::{DependencyGraph, DependencyResolver};
8use crate::core::types::{DependencyRef, LockfileEntry, PackageManager, Workspace};
9use crate::discovery::read_json_file;
10use crate::error::Result;
11use petgraph::graph::NodeIndex;
12use serde::Deserialize;
13use std::collections::HashMap;
14use std::path::Path;
15
16#[cfg(feature = "toml")]
17use crate::discovery::read_toml_file;
18
19/// A generic dependency resolver that works across supported package managers.
20///
21/// This resolver implements a strategy-based approach to handle differences between
22/// package managers (e.g., npm vs. Cargo) while providing a unified interface
23/// for dependency resolution.
24pub struct GenericDependencyResolver;
25
26impl DependencyResolver for GenericDependencyResolver {
27    fn resolve_dependencies(
28        &self,
29        _workspace: &Workspace,
30        lockfile: &[LockfileEntry],
31    ) -> Result<DependencyGraph> {
32        let mut graph = DependencyGraph::new();
33        // Key by (name, version) to handle multiple versions of the same package
34        let mut node_map: HashMap<(String, String), NodeIndex> = HashMap::new();
35
36        // 1. Create nodes for all lockfile entries
37        for entry in lockfile {
38            let dep_ref = DependencyRef {
39                name: entry.name.clone(),
40                version_req: entry.version.clone(),
41            };
42            let idx = graph.add_node(dep_ref);
43            node_map.insert((entry.name.clone(), entry.version.clone()), idx);
44        }
45
46        // 2. Add edges based on dependencies declared in lockfile entries
47        for entry in lockfile {
48            if let Some(&source_idx) = node_map.get(&(entry.name.clone(), entry.version.clone())) {
49                for dep in &entry.dependencies {
50                    // Find the dependency in the graph
51                    // We look up by (name, version_req). This assumes that the lockfile parser
52                    // populates `version_req` with the *resolved* version for that dependency,
53                    // or that the version requirement exactly matches one of the present versions.
54                    //
55                    // For many lockfiles (like Cargo.lock), the dependency list contains the
56                    // concrete version that was resolved.
57                    if let Some(&target_idx) =
58                        node_map.get(&(dep.name.clone(), dep.version_req.clone()))
59                    {
60                        graph.add_edge(source_idx, target_idx, ());
61                    } else {
62                        // Fallback: If strict lookup fails (e.g. version_req is a range like "^1.0.0"),
63                        // we would ideally perform semver resolution against available nodes.
64                        // For now, we log a trace if we can't find the exact match.
65                        // This keeps the graph correct for well-formed resolved lockfiles.
66                        tracing::trace!(
67                            "Could not find exact match for dependency {} {} -> {} {}",
68                            entry.name,
69                            entry.version,
70                            dep.name,
71                            dep.version_req
72                        );
73                    }
74                }
75            }
76        }
77
78        Ok(graph)
79    }
80
81    fn resolve_workspace_deps(&self, workspace: &Workspace) -> Result<Vec<DependencyRef>> {
82        let mut workspace_deps = Vec::new();
83
84        for member in &workspace.members {
85            let deps = match workspace.manager {
86                PackageManager::Npm
87                | PackageManager::Bun
88                | PackageManager::Pnpm
89                | PackageManager::YarnClassic
90                | PackageManager::YarnModern
91                | PackageManager::Deno => self.parse_js_deps(&member.manifest_path)?,
92                PackageManager::Cargo => Self::parse_rust_deps(&member.manifest_path)?,
93            };
94
95            workspace_deps.extend(deps);
96        }
97
98        Ok(workspace_deps)
99    }
100
101    fn resolve_external_deps(&self, lockfile: &[LockfileEntry]) -> Result<Vec<DependencyRef>> {
102        Ok(lockfile
103            .iter()
104            .filter(|entry| !entry.is_workspace_member)
105            .map(|entry| DependencyRef {
106                name: entry.name.clone(),
107                version_req: entry.version.clone(),
108            })
109            .collect())
110    }
111
112    fn detect_workspace_protocol(&self, spec: &str) -> bool {
113        // JS: "workspace:*" or "workspace:^1.2.3"
114        // Rust: we map { workspace = true } to "workspace" version requirement (internal convention)
115        spec.starts_with("workspace:") || spec == "workspace"
116    }
117}
118
119impl GenericDependencyResolver {
120    fn parse_js_deps(&self, path: &Path) -> Result<Vec<DependencyRef>> {
121        #[derive(Deserialize)]
122        struct PackageJsonDeps {
123            dependencies: Option<HashMap<String, String>>,
124            #[serde(rename = "devDependencies")]
125            dev_dependencies: Option<HashMap<String, String>>,
126        }
127
128        let pkg: PackageJsonDeps = read_json_file(path)?;
129        let mut result = Vec::new();
130
131        let mut add_deps = |deps: HashMap<String, String>| {
132            for (name, version) in deps {
133                if self.detect_workspace_protocol(&version) {
134                    result.push(DependencyRef {
135                        name,
136                        version_req: version,
137                    });
138                }
139            }
140        };
141
142        if let Some(deps) = pkg.dependencies {
143            add_deps(deps);
144        }
145        if let Some(deps) = pkg.dev_dependencies {
146            add_deps(deps);
147        }
148
149        Ok(result)
150    }
151
152    fn parse_rust_deps(path: &Path) -> Result<Vec<DependencyRef>> {
153        #[cfg(feature = "toml")]
154        {
155            #[derive(Deserialize)]
156            struct CargoTomlDeps {
157                dependencies: Option<HashMap<String, toml::Value>>,
158                #[serde(rename = "dev-dependencies")]
159                dev_dependencies: Option<HashMap<String, toml::Value>>,
160            }
161
162            // If toml is not available, we can't parse. But if manager is Cargo, toml should be available.
163            // We'll return empty if toml is missing but this code block is cfg-gated.
164            let pkg: CargoTomlDeps = read_toml_file(path)?;
165            let mut result = Vec::new();
166
167            let mut add_deps = |deps: HashMap<String, toml::Value>| {
168                for (name, value) in deps {
169                    let is_workspace = if let toml::Value::Table(t) = &value {
170                        t.get("workspace")
171                            .and_then(toml::Value::as_bool)
172                            .unwrap_or(false)
173                    } else {
174                        false
175                    };
176
177                    if is_workspace {
178                        result.push(DependencyRef {
179                            name,
180                            version_req: "workspace".to_string(),
181                        });
182                    }
183                }
184            };
185
186            if let Some(deps) = pkg.dependencies {
187                add_deps(deps);
188            }
189            if let Some(deps) = pkg.dev_dependencies {
190                add_deps(deps);
191            }
192
193            Ok(result)
194        }
195        #[cfg(not(feature = "toml"))]
196        {
197            // Should not happen if features are configured correctly for Cargo
198            let _ = path;
199            Ok(Vec::new())
200        }
201    }
202}