Skip to main content

cargo_declared/
metadata.rs

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/// Public API struct representing a dependency
17#[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/// Internal struct for resolver operations
26#[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(&current_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(&current_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}