normalize_manifest/
gradle_libs.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
4use std::collections::HashMap;
5use toml::Value;
6
7pub struct GradleLibsParser;
13
14impl ManifestParser for GradleLibsParser {
15 fn filename(&self) -> &'static str {
16 "libs.versions.toml"
17 }
18
19 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
20 let toml: Value = content
21 .parse::<Value>()
22 .map_err(|e| ManifestError(e.to_string()))?;
23
24 let versions: HashMap<String, String> = toml
26 .get("versions")
27 .and_then(|v| v.as_table())
28 .map(|t| {
29 t.iter()
30 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
31 .collect()
32 })
33 .unwrap_or_default();
34
35 let mut deps = Vec::new();
36
37 if let Some(libraries) = toml.get("libraries").and_then(|v| v.as_table()) {
38 for (_alias, entry) in libraries {
39 let Some(table) = entry.as_table() else {
40 continue;
41 };
42
43 let Some(module) = table.get("module").and_then(|v| v.as_str()) else {
45 continue;
46 };
47
48 let version_req = if let Some(vref) = table
50 .get("version")
51 .and_then(|v| v.as_table())
52 .and_then(|t| t.get("ref"))
53 .and_then(|v| v.as_str())
54 {
55 versions.get(vref).cloned()
56 } else {
57 table
58 .get("version")
59 .and_then(|v| v.as_str())
60 .map(|s| s.to_string())
61 };
62
63 deps.push(DeclaredDep {
64 name: module.to_string(),
65 version_req,
66 kind: DepKind::Normal,
67 });
68 }
69 }
70
71 Ok(ParsedManifest {
72 ecosystem: "gradle",
73 name: None,
74 version: None,
75 dependencies: deps,
76 })
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use crate::ManifestParser;
84
85 #[test]
86 fn test_gradle_libs_version_catalog() {
87 let content = r#"
88[versions]
89junit = "4.13.2"
90retrofit = "2.9.0"
91
92[libraries]
93junit = { module = "junit:junit", version.ref = "junit" }
94retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
95retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version = "2.9.0" }
96
97[bundles]
98retrofit = ["retrofit", "retrofit-gson"]
99"#;
100 let m = GradleLibsParser.parse(content).unwrap();
101 assert_eq!(m.ecosystem, "gradle");
102 assert_eq!(m.dependencies.len(), 3);
103
104 let junit = m
105 .dependencies
106 .iter()
107 .find(|d| d.name == "junit:junit")
108 .unwrap();
109 assert_eq!(junit.version_req.as_deref(), Some("4.13.2"));
110 assert_eq!(junit.kind, DepKind::Normal);
111
112 let retrofit = m
113 .dependencies
114 .iter()
115 .find(|d| d.name == "com.squareup.retrofit2:retrofit")
116 .unwrap();
117 assert_eq!(retrofit.version_req.as_deref(), Some("2.9.0"));
118
119 let gson = m
120 .dependencies
121 .iter()
122 .find(|d| d.name == "com.squareup.retrofit2:converter-gson")
123 .unwrap();
124 assert_eq!(gson.version_req.as_deref(), Some("2.9.0"));
125 }
126
127 #[test]
128 fn test_gradle_libs_no_versions_section() {
129 let content = r#"
130[libraries]
131mylib = { module = "com.example:mylib", version = "1.0.0" }
132"#;
133 let m = GradleLibsParser.parse(content).unwrap();
134 assert_eq!(m.dependencies.len(), 1);
135 assert_eq!(m.dependencies[0].name, "com.example:mylib");
136 assert_eq!(m.dependencies[0].version_req.as_deref(), Some("1.0.0"));
137 }
138
139 #[test]
140 fn test_gradle_libs_unresolvable_ref_gives_none() {
141 let content = r#"
142[versions]
143# intentionally empty
144
145[libraries]
146orphan = { module = "com.example:orphan", version.ref = "nonexistent" }
147"#;
148 let m = GradleLibsParser.parse(content).unwrap();
149 assert_eq!(m.dependencies.len(), 1);
150 assert!(m.dependencies[0].version_req.is_none());
151 }
152}