normalize_manifest/
cargo.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
4use toml::Value;
5
6pub 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 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}