1use cargo_metadata::{DependencyKind as CargoDependencyKind, Metadata, MetadataCommand, Package};
2use serde::Serialize;
3use std::collections::{HashMap, VecDeque};
4use std::path::{Path, PathBuf};
5
6use crate::error::{Error, Result};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum DependencyKind {
11 Normal,
12 Development,
13 Build,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
18pub struct DependencyInfo {
19 pub name: String,
20 pub version: Option<String>,
21 pub source: Option<String>,
22 pub kind: DependencyKind,
23}
24
25#[derive(Debug, Clone)]
27pub struct ParsedMetadata {
28 pub workspace_root: PathBuf,
29 pub package_name: String,
30 pub root_package_id: String,
31 pub declared_deps: Vec<DependencyInfo>,
32 pub compiled_deps: Vec<DependencyInfo>,
33 pub package_graph: HashMap<String, Vec<String>>,
34 pub package_names: HashMap<String, String>,
35 pub declared_dep_ids: Vec<Option<String>>,
36 pub compiled_dep_ids: HashMap<String, String>,
37}
38
39pub fn parse_metadata(path: Option<PathBuf>) -> Result<ParsedMetadata> {
40 let metadata = load_metadata(path.as_deref())?;
41 let resolve = metadata.resolve.as_ref().ok_or(Error::NoRootPackage)?;
42 let root_id = resolve.root.as_ref().ok_or(Error::NoRootPackage)?;
43 let root_pkg = find_package(&metadata, root_id).ok_or(Error::NoRootPackage)?;
44
45 let package_names: HashMap<_, _> = metadata
46 .packages
47 .iter()
48 .map(|pkg| (pkg.id.to_string(), pkg.name.clone()))
49 .collect();
50
51 let root_dep_ids: HashMap<_, _> = resolve
52 .nodes
53 .iter()
54 .find(|node| &node.id == root_id)
55 .map(|node| {
56 node.deps
57 .iter()
58 .map(|dep| (dep.name.clone(), dep.pkg.to_string()))
59 .collect::<HashMap<_, _>>()
60 })
61 .unwrap_or_default();
62
63 let declared_deps: Vec<_> = root_pkg
64 .dependencies
65 .iter()
66 .map(|dep| map_declared_dep(dep))
67 .collect();
68
69 let declared_dep_ids = root_pkg
70 .dependencies
71 .iter()
72 .map(|dep| root_dep_ids.get(dependency_display_name(dep)).cloned())
73 .collect();
74 let compiled_dep_ids = metadata
75 .packages
76 .iter()
77 .map(|pkg| {
78 let version = pkg.version.to_string();
79 let source = pkg.source.as_ref().map(ToString::to_string);
80 (
81 dependency_key(&pkg.name, Some(&version), source.as_deref()),
82 pkg.id.to_string(),
83 )
84 })
85 .collect();
86
87 Ok(ParsedMetadata {
88 workspace_root: metadata.workspace_root.clone().into(),
89 package_name: root_pkg.name.clone(),
90 root_package_id: root_id.to_string(),
91 declared_deps,
92 compiled_deps: collect_compiled_deps(&metadata, root_id),
93 package_graph: build_package_graph(&metadata),
94 package_names,
95 declared_dep_ids,
96 compiled_dep_ids,
97 })
98}
99
100fn load_metadata(path: Option<&Path>) -> Result<Metadata> {
101 let mut command = MetadataCommand::new();
102
103 if let Some(path) = path {
104 if !path.exists() {
105 return Err(Error::PathNotFound {
106 path: path.to_path_buf(),
107 });
108 }
109
110 if path.file_name().is_some_and(|name| name == "Cargo.toml") {
111 command.manifest_path(path);
112 } else {
113 command.current_dir(path);
114 }
115 }
116
117 Ok(command.exec()?)
118}
119
120fn find_package<'a>(
121 metadata: &'a Metadata,
122 package_id: &cargo_metadata::PackageId,
123) -> Option<&'a Package> {
124 metadata.packages.iter().find(|pkg| &pkg.id == package_id)
125}
126
127fn dependency_display_name(dep: &cargo_metadata::Dependency) -> &str {
128 dep.rename.as_deref().unwrap_or(&dep.name)
129}
130
131fn map_declared_dep(dep: &cargo_metadata::Dependency) -> DependencyInfo {
132 DependencyInfo {
133 name: dependency_display_name(dep).to_string(),
134 version: Some(dep.req.to_string()),
135 source: dep
136 .path
137 .as_ref()
138 .map(|path| format!("path+{}", path))
139 .or_else(|| dep.registry.clone()),
140 kind: map_kind(dep.kind),
141 }
142}
143
144fn map_kind(kind: cargo_metadata::DependencyKind) -> DependencyKind {
145 match kind {
146 CargoDependencyKind::Development => DependencyKind::Development,
147 CargoDependencyKind::Build => DependencyKind::Build,
148 CargoDependencyKind::Normal | CargoDependencyKind::Unknown => DependencyKind::Normal,
149 }
150}
151
152fn collect_compiled_deps(
153 metadata: &Metadata,
154 root_id: &cargo_metadata::PackageId,
155) -> Vec<DependencyInfo> {
156 let Some(resolve) = metadata.resolve.as_ref() else {
157 return Vec::new();
158 };
159
160 let node_map: HashMap<_, _> = resolve.nodes.iter().map(|node| (&node.id, node)).collect();
161 let mut queue = VecDeque::from([(root_id.clone(), DependencyKind::Normal)]);
162 let mut kinds = HashMap::new();
163
164 while let Some((current_id, current_kind)) = queue.pop_front() {
165 let Some(node) = node_map.get(¤t_id) else {
166 continue;
167 };
168
169 for dep in &node.deps {
170 let edge_kind = dep
171 .dep_kinds
172 .iter()
173 .map(|info| map_kind(info.kind))
174 .max_by_key(kind_rank)
175 .unwrap_or(DependencyKind::Normal);
176 let dep_kind = propagate_kind(¤t_kind, &edge_kind);
177 let should_enqueue = match kinds.get(&dep.pkg) {
178 Some(existing) if kind_rank(existing) >= kind_rank(&dep_kind) => false,
179 _ => {
180 kinds.insert(dep.pkg.clone(), dep_kind.clone());
181 true
182 }
183 };
184
185 if should_enqueue {
186 queue.push_back((dep.pkg.clone(), dep_kind));
187 }
188 }
189 }
190
191 resolve
192 .nodes
193 .iter()
194 .filter(|node| &node.id != root_id)
195 .filter_map(|node| find_package(metadata, &node.id).zip(kinds.get(&node.id)))
196 .map(|(pkg, kind)| DependencyInfo {
197 name: pkg.name.clone(),
198 version: Some(pkg.version.to_string()),
199 source: pkg.source.as_ref().map(ToString::to_string),
200 kind: kind.clone(),
201 })
202 .collect()
203}
204
205pub(crate) fn dependency_key(name: &str, version: Option<&str>, source: Option<&str>) -> String {
206 format!(
207 "{}\u{1f}{}\u{1f}{}",
208 name,
209 version.unwrap_or("unknown"),
210 source.unwrap_or("")
211 )
212}
213
214fn kind_rank(kind: &DependencyKind) -> u8 {
215 match kind {
216 DependencyKind::Development => 0,
217 DependencyKind::Build => 1,
218 DependencyKind::Normal => 2,
219 }
220}
221
222fn propagate_kind(current: &DependencyKind, edge: &DependencyKind) -> DependencyKind {
223 if matches!(current, DependencyKind::Normal) {
224 edge.clone()
225 } else {
226 current.clone()
227 }
228}
229
230fn build_package_graph(metadata: &Metadata) -> HashMap<String, Vec<String>> {
231 let mut graph = HashMap::new();
232
233 if let Some(resolve) = &metadata.resolve {
234 for node in &resolve.nodes {
235 let deps = node
236 .deps
237 .iter()
238 .map(|dep| dep.pkg.to_string())
239 .collect::<Vec<_>>();
240
241 graph.insert(node.id.to_string(), deps);
242 }
243 }
244
245 graph
246}