normalize_manifest/
gradle.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
12
13pub struct GradleParser;
15
16pub struct GradleKtsParser;
18
19impl ManifestParser for GradleParser {
20 fn filename(&self) -> &'static str {
21 "build.gradle"
22 }
23
24 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
25 parse_gradle(content)
26 }
27}
28
29impl ManifestParser for GradleKtsParser {
30 fn filename(&self) -> &'static str {
31 "build.gradle.kts"
32 }
33
34 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
35 parse_gradle(content)
36 }
37}
38
39fn parse_gradle(content: &str) -> Result<ParsedManifest, ManifestError> {
40 let mut deps = Vec::new();
41 let mut in_deps_block = false;
42 let mut brace_depth: i32 = 0;
43
44 for line in content.lines() {
45 let trimmed = line.trim();
46
47 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("/*") {
48 continue;
49 }
50
51 if !in_deps_block
53 && (trimmed == "dependencies {"
54 || trimmed == "dependencies{"
55 || trimmed.starts_with("dependencies {")
56 || trimmed.starts_with("dependencies{"))
57 {
58 in_deps_block = true;
59 brace_depth = 1;
60 continue;
61 }
62
63 if in_deps_block {
64 for ch in trimmed.chars() {
65 match ch {
66 '{' => brace_depth += 1,
67 '}' => brace_depth -= 1,
68 _ => {}
69 }
70 }
71
72 if brace_depth <= 0 {
73 in_deps_block = false;
74 continue;
75 }
76
77 if let Some(dep) = parse_gradle_dep_line(trimmed) {
78 deps.push(dep);
79 }
80 }
81 }
82
83 Ok(ParsedManifest {
84 ecosystem: "gradle",
85 name: None,
86 version: None,
87 dependencies: deps,
88 })
89}
90
91fn config_kind(config: &str) -> Option<DepKind> {
95 let config = config.trim_end_matches('(');
96 match config {
97 "implementation" | "api" | "compileOnly" | "runtimeOnly" | "compile" | "runtime" => {
98 Some(DepKind::Normal)
99 }
100 "testImplementation"
101 | "testCompileOnly"
102 | "testRuntimeOnly"
103 | "testCompile"
104 | "testRuntime"
105 | "androidTestImplementation" => Some(DepKind::Dev),
106 _ if config.ends_with("TestImplementation")
107 || config.ends_with("TestCompile")
108 || config.starts_with("debug")
109 || config.starts_with("release") =>
110 {
111 Some(DepKind::Normal)
112 }
113 _ => None,
114 }
115}
116
117fn parse_gradle_dep_line(line: &str) -> Option<DeclaredDep> {
118 let word_end = line.find(|c: char| !c.is_alphanumeric() && c != '_')?;
120 let config = &line[..word_end];
121 let kind = config_kind(config)?;
122
123 let rest = line[word_end..].trim();
124
125 let coord = if let Some(inner) = rest.strip_prefix('"') {
130 let end = inner.find('"')?;
131 &inner[..end]
132 } else if let Some(inner) = rest.strip_prefix('\'') {
133 let end = inner.find('\'')?;
134 &inner[..end]
135 } else if let Some(inner) = rest.strip_prefix('(') {
136 let inner = inner.trim_start();
138 if let Some(inner2) = inner.strip_prefix('"') {
139 let end = inner2.find('"')?;
140 &inner2[..end]
141 } else if let Some(inner2) = inner.strip_prefix('\'') {
142 let end = inner2.find('\'')?;
143 &inner2[..end]
144 } else {
145 return None;
146 }
147 } else {
148 return None;
149 };
150
151 let parts: Vec<&str> = coord.splitn(3, ':').collect();
153 match parts.as_slice() {
154 [group, artifact, version] => Some(DeclaredDep {
155 name: format!("{}:{}", group, artifact),
156 version_req: Some(version.to_string()),
157 kind,
158 }),
159 [group, artifact] => Some(DeclaredDep {
160 name: format!("{}:{}", group, artifact),
161 version_req: None,
162 kind,
163 }),
164 _ => None,
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::ManifestParser;
172
173 #[test]
174 fn test_parse_build_gradle_groovy() {
175 let content = r#"
176plugins {
177 id 'java'
178}
179
180dependencies {
181 implementation 'com.google.guava:guava:32.1.0-jre'
182 implementation 'org.springframework:spring-core:6.0.0'
183 testImplementation 'junit:junit:4.13.2'
184 compileOnly 'org.projectlombok:lombok:1.18.28'
185}
186"#;
187 let m = GradleParser.parse(content).unwrap();
188 assert_eq!(m.ecosystem, "gradle");
189 assert_eq!(m.dependencies.len(), 4);
190
191 let guava = m
192 .dependencies
193 .iter()
194 .find(|d| d.name == "com.google.guava:guava")
195 .unwrap();
196 assert_eq!(guava.version_req.as_deref(), Some("32.1.0-jre"));
197 assert_eq!(guava.kind, DepKind::Normal);
198
199 let junit = m
200 .dependencies
201 .iter()
202 .find(|d| d.name == "junit:junit")
203 .unwrap();
204 assert_eq!(junit.kind, DepKind::Dev);
205 }
206
207 #[test]
208 fn test_parse_build_gradle_kts() {
209 let content = r#"
210dependencies {
211 implementation("com.google.guava:guava:32.1.0-jre")
212 testImplementation("org.junit.jupiter:junit-jupiter:5.9.3")
213 api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
214}
215"#;
216 let m = GradleKtsParser.parse(content).unwrap();
217 assert_eq!(m.ecosystem, "gradle");
218 assert_eq!(m.dependencies.len(), 3);
219
220 let coroutines = m
221 .dependencies
222 .iter()
223 .find(|d| d.name.contains("kotlinx-coroutines-core"))
224 .unwrap();
225 assert_eq!(coroutines.version_req.as_deref(), Some("1.7.1"));
226 }
227}