use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
pub struct GradleParser;
pub struct GradleKtsParser;
impl ManifestParser for GradleParser {
fn filename(&self) -> &'static str {
"build.gradle"
}
fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
parse_gradle(content)
}
}
impl ManifestParser for GradleKtsParser {
fn filename(&self) -> &'static str {
"build.gradle.kts"
}
fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
parse_gradle(content)
}
}
fn parse_gradle(content: &str) -> Result<ParsedManifest, ManifestError> {
let mut deps = Vec::new();
let mut in_deps_block = false;
let mut brace_depth: i32 = 0;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("/*") {
continue;
}
if !in_deps_block
&& (trimmed == "dependencies {"
|| trimmed == "dependencies{"
|| trimmed.starts_with("dependencies {")
|| trimmed.starts_with("dependencies{"))
{
in_deps_block = true;
brace_depth = 1;
continue;
}
if in_deps_block {
for ch in trimmed.chars() {
match ch {
'{' => brace_depth += 1,
'}' => brace_depth -= 1,
_ => {}
}
}
if brace_depth <= 0 {
in_deps_block = false;
continue;
}
if let Some(dep) = parse_gradle_dep_line(trimmed) {
deps.push(dep);
}
}
}
Ok(ParsedManifest {
ecosystem: "gradle",
name: None,
version: None,
dependencies: deps,
})
}
fn config_kind(config: &str) -> Option<DepKind> {
let config = config.trim_end_matches('(');
match config {
"implementation" | "api" | "compileOnly" | "runtimeOnly" | "compile" | "runtime" => {
Some(DepKind::Normal)
}
"testImplementation"
| "testCompileOnly"
| "testRuntimeOnly"
| "testCompile"
| "testRuntime"
| "androidTestImplementation" => Some(DepKind::Dev),
_ if config.ends_with("TestImplementation")
|| config.ends_with("TestCompile")
|| config.starts_with("debug")
|| config.starts_with("release") =>
{
Some(DepKind::Normal)
}
_ => None,
}
}
fn parse_gradle_dep_line(line: &str) -> Option<DeclaredDep> {
let word_end = line.find(|c: char| !c.is_alphanumeric() && c != '_')?;
let config = &line[..word_end];
let kind = config_kind(config)?;
let rest = line[word_end..].trim();
let coord = if let Some(inner) = rest.strip_prefix('"') {
let end = inner.find('"')?;
&inner[..end]
} else if let Some(inner) = rest.strip_prefix('\'') {
let end = inner.find('\'')?;
&inner[..end]
} else if let Some(inner) = rest.strip_prefix('(') {
let inner = inner.trim_start();
if let Some(inner2) = inner.strip_prefix('"') {
let end = inner2.find('"')?;
&inner2[..end]
} else if let Some(inner2) = inner.strip_prefix('\'') {
let end = inner2.find('\'')?;
&inner2[..end]
} else {
return None;
}
} else {
return None;
};
let parts: Vec<&str> = coord.splitn(3, ':').collect();
match parts.as_slice() {
[group, artifact, version] => Some(DeclaredDep {
name: format!("{}:{}", group, artifact),
version_req: Some(version.to_string()),
kind,
}),
[group, artifact] => Some(DeclaredDep {
name: format!("{}:{}", group, artifact),
version_req: None,
kind,
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ManifestParser;
#[test]
fn test_parse_build_gradle_groovy() {
let content = r#"
plugins {
id 'java'
}
dependencies {
implementation 'com.google.guava:guava:32.1.0-jre'
implementation 'org.springframework:spring-core:6.0.0'
testImplementation 'junit:junit:4.13.2'
compileOnly 'org.projectlombok:lombok:1.18.28'
}
"#;
let m = GradleParser.parse(content).unwrap();
assert_eq!(m.ecosystem, "gradle");
assert_eq!(m.dependencies.len(), 4);
let guava = m
.dependencies
.iter()
.find(|d| d.name == "com.google.guava:guava")
.unwrap();
assert_eq!(guava.version_req.as_deref(), Some("32.1.0-jre"));
assert_eq!(guava.kind, DepKind::Normal);
let junit = m
.dependencies
.iter()
.find(|d| d.name == "junit:junit")
.unwrap();
assert_eq!(junit.kind, DepKind::Dev);
}
#[test]
fn test_parse_build_gradle_kts() {
let content = r#"
dependencies {
implementation("com.google.guava:guava:32.1.0-jre")
testImplementation("org.junit.jupiter:junit-jupiter:5.9.3")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
}
"#;
let m = GradleKtsParser.parse(content).unwrap();
assert_eq!(m.ecosystem, "gradle");
assert_eq!(m.dependencies.len(), 3);
let coroutines = m
.dependencies
.iter()
.find(|d| d.name.contains("kotlinx-coroutines-core"))
.unwrap();
assert_eq!(coroutines.version_req.as_deref(), Some("1.7.1"));
}
}