Skip to main content

normalize_manifest/
cargo.rs

1//! Parser for `Cargo.toml` files (Rust/Cargo).
2
3use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
4use toml::Value;
5
6/// Parser for `Cargo.toml` files.
7pub struct CargoParser;
8
9impl ManifestParser for CargoParser {
10    fn filename(&self) -> &'static str {
11        "Cargo.toml"
12    }
13
14    fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
15        let toml: Value = content
16            .parse::<Value>()
17            .map_err(|e| ManifestError(e.to_string()))?;
18
19        let package = toml.get("package");
20        let name = package
21            .and_then(|p| p.get("name"))
22            .and_then(|v| v.as_str())
23            .map(|s| s.to_string());
24        let version = package
25            .and_then(|p| p.get("version"))
26            .and_then(|v| v.as_str())
27            .map(|s| s.to_string());
28
29        let mut deps = Vec::new();
30        extract_cargo_deps(&toml, "dependencies", DepKind::Normal, &mut deps);
31        extract_cargo_deps(&toml, "dev-dependencies", DepKind::Dev, &mut deps);
32        extract_cargo_deps(&toml, "build-dependencies", DepKind::Build, &mut deps);
33
34        Ok(ParsedManifest {
35            ecosystem: "cargo",
36            name,
37            version,
38            dependencies: deps,
39        })
40    }
41}
42
43fn extract_cargo_deps(toml: &Value, section: &str, kind: DepKind, out: &mut Vec<DeclaredDep>) {
44    let Some(table) = toml.get(section).and_then(|v| v.as_table()) else {
45        return;
46    };
47    for (name, val) in table {
48        let version_req = if let Some(s) = val.as_str() {
49            Some(s.to_string())
50        } else if let Some(t) = val.as_table() {
51            t.get("version")
52                .and_then(|v| v.as_str())
53                .map(|s| s.to_string())
54        } else {
55            None
56        };
57        out.push(DeclaredDep {
58            name: name.clone(),
59            version_req,
60            kind,
61        });
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use crate::ManifestParser;
69
70    #[test]
71    fn test_parse_cargo_toml() {
72        let content = r#"
73[package]
74name = "my-crate"
75version = "0.2.0"
76edition = "2024"
77
78[dependencies]
79serde = "1"
80tokio = { version = "1", features = ["full"] }
81
82[dev-dependencies]
83tempfile = "3"
84
85[build-dependencies]
86cc = "1"
87"#;
88        let m = CargoParser.parse(content).unwrap();
89        assert_eq!(m.ecosystem, "cargo");
90        assert_eq!(m.name.as_deref(), Some("my-crate"));
91        assert_eq!(m.version.as_deref(), Some("0.2.0"));
92
93        let normal: Vec<_> = m
94            .dependencies
95            .iter()
96            .filter(|d| d.kind == DepKind::Normal)
97            .collect();
98        assert_eq!(normal.len(), 2);
99
100        let serde_dep = normal.iter().find(|d| d.name == "serde").unwrap();
101        assert_eq!(serde_dep.version_req.as_deref(), Some("1"));
102
103        let tokio_dep = normal.iter().find(|d| d.name == "tokio").unwrap();
104        assert_eq!(tokio_dep.version_req.as_deref(), Some("1"));
105
106        let dev: Vec<_> = m
107            .dependencies
108            .iter()
109            .filter(|d| d.kind == DepKind::Dev)
110            .collect();
111        assert_eq!(dev.len(), 1);
112        assert_eq!(dev[0].name, "tempfile");
113
114        let build: Vec<_> = m
115            .dependencies
116            .iter()
117            .filter(|d| d.kind == DepKind::Build)
118            .collect();
119        assert_eq!(build.len(), 1);
120        assert_eq!(build[0].name, "cc");
121    }
122
123    #[test]
124    fn test_no_package_section() {
125        // Workspace-only Cargo.toml
126        let content = "[workspace]\nmembers = [\"crates/*\"]\n";
127        let m = CargoParser.parse(content).unwrap();
128        assert_eq!(m.ecosystem, "cargo");
129        assert!(m.name.is_none());
130        assert!(m.version.is_none());
131        assert!(m.dependencies.is_empty());
132    }
133}