use std::path::Path;
use anyhow::{Context, Result};
use crate::gradle::model::{GradleProject, Task, Dependency, Repository, RepositoryType, Plugin};
pub fn parse_gradle_build_script(
build_file: &Path,
base_dir: &Path,
) -> Result<GradleProject> {
let content = std::fs::read_to_string(build_file)
.with_context(|| format!("Failed to read build file: {build_file:?}"))?;
let is_kotlin = build_file.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext == "kts")
.unwrap_or(false);
if is_kotlin {
parse_kotlin_dsl(&content, build_file, base_dir)
} else {
parse_groovy_dsl(&content, build_file, base_dir)
}
}
fn parse_groovy_dsl(
content: &str,
build_file: &Path,
base_dir: &Path,
) -> Result<GradleProject> {
let mut project = GradleProject::new(base_dir.to_path_buf(), build_file.to_path_buf());
if let Some(plugins_block) = extract_block(content, "plugins") {
project.plugins = parse_plugins(&plugins_block);
}
if let Some(group) = extract_string_property(content, "group") {
project.group = Some(group);
}
if let Some(version) = extract_string_property(content, "version") {
project.version = Some(version);
}
if let Some(name) = extract_string_property(content, "name") {
project.name = name;
}
if let Some(compat) = extract_string_property(content, "sourceCompatibility") {
project.source_compatibility = Some(compat);
}
if let Some(compat) = extract_string_property(content, "targetCompatibility") {
project.target_compatibility = Some(compat);
}
if let Some(repos_block) = extract_block(content, "repositories") {
project.repositories = parse_repositories(&repos_block);
}
if let Some(deps_block) = extract_block(content, "dependencies") {
project.dependencies = parse_dependencies(&deps_block);
}
project.tasks = parse_tasks(content);
if let Some(app_block) = extract_block(content, "application") {
if let Some(main_class) = extract_main_class(&app_block) {
project.main_class = Some(main_class);
}
}
Ok(project)
}
fn extract_main_class(app_block: &str) -> Option<String> {
for line in app_block.lines() {
let line = line.trim();
if line.starts_with("mainClass") {
if let Some(eq_pos) = line.find('=') {
let value = line[eq_pos+1..].trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
if !value.is_empty() {
return Some(value);
}
}
if let Some(start) = line.find(".set(") {
let rest = &line[start+5..];
if let Some(end) = rest.find(')') {
let value = rest[..end].trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
if !value.is_empty() {
return Some(value);
}
}
}
}
}
None
}
fn parse_kotlin_dsl(
content: &str,
build_file: &Path,
base_dir: &Path,
) -> Result<GradleProject> {
parse_groovy_dsl(content, build_file, base_dir)
}
fn extract_block(content: &str, block_name: &str) -> Option<String> {
let pattern = format!("{block_name} {{");
if let Some(start) = content.find(&pattern) {
let start_pos = start + pattern.len();
let mut depth = 1;
let chars: Vec<char> = content[start_pos..].chars().collect();
for (i, ch) in chars.iter().enumerate() {
match ch {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
return Some(content[start_pos..start_pos + i].to_string());
}
}
_ => {}
}
}
}
None
}
fn extract_string_property(content: &str, property: &str) -> Option<String> {
let patterns = vec![
format!("{} = '", property),
format!("{} = \"", property),
format!("{}='", property),
format!("{}=\"", property),
];
for pattern in patterns {
if let Some(start) = content.find(&pattern) {
let start_pos = start + pattern.len();
let quote_char = pattern.chars().last().unwrap();
if let Some(end) = content[start_pos..].find(quote_char) {
return Some(content[start_pos..start_pos + end].to_string());
}
}
}
None
}
fn parse_plugins(plugins_block: &str) -> Vec<Plugin> {
let mut plugins = Vec::new();
let id_patterns = vec!["id '", "id \"", "id('", "id(\""];
for line in plugins_block.lines() {
for pattern in &id_patterns {
if let Some(start) = line.find(pattern) {
let start_pos = start + pattern.len();
let quote_char = if pattern.contains('\'') { '\'' } else { '"' };
if let Some(end) = line[start_pos..].find(quote_char) {
let plugin_id = line[start_pos..start_pos + end].to_string();
plugins.push(Plugin {
id: plugin_id,
version: None, });
}
}
}
}
plugins
}
fn parse_repositories(repos_block: &str) -> Vec<Repository> {
let mut repositories = Vec::new();
if repos_block.contains("mavenCentral()") {
repositories.push(Repository {
name: "MavenCentral".to_string(),
repo_type: RepositoryType::MavenCentral,
url: Some("https://repo1.maven.org/maven2/".to_string()),
});
}
if repos_block.contains("jcenter()") {
repositories.push(Repository {
name: "JCenter".to_string(),
repo_type: RepositoryType::JCenter,
url: Some("https://jcenter.bintray.com/".to_string()),
});
}
if repos_block.contains("google()") {
repositories.push(Repository {
name: "Google".to_string(),
repo_type: RepositoryType::Google,
url: Some("https://dl.google.com/dl/android/maven2/".to_string()),
});
}
if let Some(maven_block) = extract_block(repos_block, "maven") {
if let Some(url) = extract_string_property(&maven_block, "url") {
repositories.push(Repository {
name: "Maven".to_string(),
repo_type: RepositoryType::Maven,
url: Some(url),
});
}
}
repositories
}
fn parse_dependencies(deps_block: &str) -> Vec<Dependency> {
let mut dependencies = Vec::new();
let dependency_patterns = vec![
"implementation", "compile", "runtime", "testImplementation",
"testCompile", "testRuntime", "api", "compileOnly", "runtimeOnly",
];
for line in deps_block.lines() {
for config in &dependency_patterns {
let pattern = format!("{config} ");
if let Some(start) = line.find(&pattern) {
let dep_start = start + pattern.len();
let dep_str = line[dep_start..].trim();
let notation = dep_str
.trim_matches(|c: char| c == '\'' || c == '"')
.to_string();
let parts: Vec<&str> = notation.split(':').collect();
if parts.len() >= 2 {
dependencies.push(Dependency {
configuration: config.to_string(),
notation: notation.clone(),
group: Some(parts[0].to_string()),
artifact: Some(parts[1].to_string()),
version: parts.get(2).map(|s| s.to_string()),
classifier: None,
extension: None,
});
} else {
dependencies.push(Dependency {
configuration: config.to_string(),
notation,
group: None,
artifact: None,
version: None,
classifier: None,
extension: None,
});
}
}
}
}
dependencies
}
fn parse_tasks(content: &str) -> Vec<Task> {
let mut tasks = Vec::new();
let task_pattern = "task ";
let mut pos = 0;
while let Some(start) = content[pos..].find(task_pattern) {
let task_start = pos + start + task_pattern.len();
let remaining = &content[task_start..];
let name_end = remaining
.find([' ', '(', '{'])
.unwrap_or(remaining.len());
let task_name = remaining[..name_end].trim().to_string();
if !task_name.is_empty() {
tasks.push(Task {
name: task_name,
task_type: None, description: None,
group: None,
depends_on: Vec::new(),
actions: Vec::new(),
});
}
pos = task_start + name_end;
}
if content.contains("java") || content.contains("id 'java'") || content.contains("id(\"java\")") {
let standard_tasks = vec![
"compileJava", "processResources", "classes", "jar",
"compileTestJava", "processTestResources", "testClasses", "test",
"clean", "build", "assemble", "check",
];
for task_name in standard_tasks {
if !tasks.iter().any(|t| t.name == task_name) {
tasks.push(Task {
name: task_name.to_string(),
task_type: Some("Standard".to_string()),
description: None,
group: Some("build".to_string()),
depends_on: Vec::new(),
actions: Vec::new(),
});
}
}
}
tasks
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_parse_simple_gradle_build() {
let content = r#"
plugins {
id 'java'
}
group = 'com.example'
version = '1.0.0'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.slf4j:slf4j-api:1.7.36'
testImplementation 'junit:junit:4.13.2'
}
"#;
let build_file = PathBuf::from("build.gradle");
let base_dir = PathBuf::from("/test");
let project = parse_groovy_dsl(content, &build_file, &base_dir).unwrap();
assert_eq!(project.plugins.len(), 1);
assert_eq!(project.plugins[0].id, "java");
assert_eq!(project.group, Some("com.example".to_string()));
assert_eq!(project.version, Some("1.0.0".to_string()));
assert!(!project.repositories.is_empty());
assert!(project.dependencies.len() >= 2);
}
}