use regex::Regex;
use std::fs;
use std::path::Path;
use crate::types::{Task, TaskDefinitionType, TaskRunner};
pub fn parse(file_path: &Path) -> Result<Vec<Task>, String> {
let content = fs::read_to_string(file_path)
.map_err(|e| format!("Failed to read Gradle build file: {}", e))?;
let mut tasks = Vec::new();
add_common_tasks(&mut tasks, file_path);
extract_custom_tasks(&content, &mut tasks, file_path)?;
extract_plugin_tasks(&content, &mut tasks, file_path)?;
Ok(tasks)
}
fn add_common_tasks(tasks: &mut Vec<Task>, file_path: &Path) {
let common_tasks = [
("build", "Assembles and tests this project"),
("clean", "Deletes the build directory"),
("test", "Runs the tests"),
("assemble", "Assembles the outputs of this project"),
("check", "Runs all checks"),
("compileJava", "Compiles Java sources"),
("compileKotlin", "Compiles Kotlin sources"),
("jar", "Assembles a jar archive"),
("javadoc", "Generates Javadoc API documentation"),
("run", "Runs this project as a JVM application"),
("distZip", "Bundles the project as a distribution"),
("distTar", "Bundles the project as a tar distribution"),
("wrapper", "Generates Gradle wrapper files"),
];
for (task_name, description) in common_tasks.iter() {
tasks.push(Task {
name: task_name.to_string(),
file_path: file_path.to_path_buf(),
definition_type: TaskDefinitionType::Gradle,
runner: TaskRunner::Gradle,
source_name: task_name.to_string(),
description: Some(description.to_string()),
shadowed_by: None,
disambiguated_name: None,
});
}
}
fn extract_custom_tasks(
content: &str,
tasks: &mut Vec<Task>,
file_path: &Path,
) -> Result<(), String> {
let groovy_task_regex = Regex::new(r"task\s+(\w+)(?:\s*\{|\s*\(|\s+.*?\{)")
.map_err(|e| format!("Failed to compile regex: {}", e))?;
let kotlin_task_regex = Regex::new(r#"tasks\s*\.\s*register\s*<.*>\s*\(\s*"(\w+)"\s*\)"#)
.map_err(|e| format!("Failed to compile Kotlin task regex: {}", e))?;
let kotlin_task_alt_regex = Regex::new(r#"task\s*\(\s*"(\w+)"\s*\)"#)
.map_err(|e| format!("Failed to compile alternative Kotlin task regex: {}", e))?;
for cap in groovy_task_regex.captures_iter(content) {
if let Some(task_name) = cap.get(1) {
tasks.push(Task {
name: task_name.as_str().to_string(),
file_path: file_path.to_path_buf(),
definition_type: TaskDefinitionType::Gradle,
runner: TaskRunner::Gradle,
source_name: task_name.as_str().to_string(),
description: extract_task_description(content, task_name.as_str()),
shadowed_by: None,
disambiguated_name: None,
});
}
}
for cap in kotlin_task_regex.captures_iter(content) {
if let Some(task_name) = cap.get(1) {
tasks.push(Task {
name: task_name.as_str().to_string(),
file_path: file_path.to_path_buf(),
definition_type: TaskDefinitionType::Gradle,
runner: TaskRunner::Gradle,
source_name: task_name.as_str().to_string(),
description: extract_task_description(content, task_name.as_str()),
shadowed_by: None,
disambiguated_name: None,
});
}
}
for cap in kotlin_task_alt_regex.captures_iter(content) {
if let Some(task_name) = cap.get(1) {
tasks.push(Task {
name: task_name.as_str().to_string(),
file_path: file_path.to_path_buf(),
definition_type: TaskDefinitionType::Gradle,
runner: TaskRunner::Gradle,
source_name: task_name.as_str().to_string(),
description: extract_task_description(content, task_name.as_str()),
shadowed_by: None,
disambiguated_name: None,
});
}
}
Ok(())
}
fn extract_task_description(content: &str, task_name: &str) -> Option<String> {
let task_pattern = format!(r"task\s+{}", regex::escape(task_name));
let description_single_quote_pattern = format!(r"description\s+'([^']*)'");
let description_double_quote_pattern = format!(r#"description\s+"([^"]*)""#);
if let Ok(regex) = Regex::new(&format!(
"{}.+?{}",
task_pattern, description_single_quote_pattern
)) {
if let Some(caps) = regex.captures(content) {
if let Some(desc) = caps.get(1) {
return Some(desc.as_str().to_string());
}
}
}
if let Ok(regex) = Regex::new(&format!(
"{}.+?{}",
task_pattern, description_double_quote_pattern
)) {
if let Some(caps) = regex.captures(content) {
if let Some(desc) = caps.get(1) {
return Some(desc.as_str().to_string());
}
}
}
let kotlin_pattern = format!(
r#"tasks.*?"{}".+?description\s*=\s*"([^"]*)""#,
regex::escape(task_name)
);
if let Ok(regex) = Regex::new(&kotlin_pattern) {
if let Some(caps) = regex.captures(content) {
if let Some(desc) = caps.get(1) {
return Some(desc.as_str().to_string());
}
}
}
Some("Custom Gradle task".to_string())
}
fn extract_plugin_tasks(
content: &str,
tasks: &mut Vec<Task>,
file_path: &Path,
) -> Result<(), String> {
let plugins = [
(
"java",
vec!["classes", "testClasses", "javadoc", "jar", "test", "check"],
),
(
"application",
vec!["run", "startScripts", "distTar", "distZip", "installDist"],
),
("kotlin", vec!["compileKotlin", "compileTestKotlin"]),
("spring-boot", vec!["bootRun", "bootJar", "bootWar"]),
(
"android",
vec![
"assembleDebug",
"assembleRelease",
"installDebug",
"installRelease",
],
),
];
let apply_plugin_regex = Regex::new(r#"apply\s+plugin\s*:\s*['"]([^'"]+)['"]"#)
.map_err(|e| format!("Failed to compile apply plugin regex: {}", e))?;
let plugins_id_regex = Regex::new(r#"plugins\s*\{\s*.*?id\s*\(\s*["']([^"']+)["']\s*\)"#)
.map_err(|e| format!("Failed to compile plugins id regex: {}", e))?;
let plugins_id_alt_regex = Regex::new(r#"plugins\s*\{\s*.*?id\s*["']([^"']+)["']"#)
.map_err(|e| format!("Failed to compile alternative plugins id regex: {}", e))?;
let mut identified_plugins = Vec::new();
for pattern in &[apply_plugin_regex, plugins_id_regex, plugins_id_alt_regex] {
for cap in pattern.captures_iter(content) {
if let Some(plugin_name) = cap.get(1) {
identified_plugins.push(plugin_name.as_str().to_string());
}
}
}
for plugin in identified_plugins {
for &(plugin_prefix, ref plugin_tasks) in &plugins {
if plugin.contains(plugin_prefix) {
for &task_name in plugin_tasks {
tasks.push(Task {
name: task_name.to_string(),
file_path: file_path.to_path_buf(),
definition_type: TaskDefinitionType::Gradle,
runner: TaskRunner::Gradle,
source_name: task_name.to_string(),
description: Some(format!("Task from {} plugin", plugin_prefix)),
shadowed_by: None,
disambiguated_name: None,
});
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::TempDir;
#[test]
fn test_parse_groovy_gradle() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("build.gradle");
let content = r#"
plugins {
id 'java'
id 'application'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.example:library:1.0.0'
testImplementation 'junit:junit:4.13'
}
task customTask {
description 'A custom task'
doLast {
println 'Hello from custom task'
}
}
task anotherTask(type: Copy) {
from 'src'
into 'build/copied'
}
application {
mainClass = 'com.example.Main'
}
"#;
let mut file = File::create(&file_path).unwrap();
file.write_all(content.as_bytes()).unwrap();
let tasks = parse(&file_path).unwrap();
assert!(tasks.iter().any(|t| t.name == "build"));
assert!(tasks.iter().any(|t| t.name == "clean"));
assert!(tasks.iter().any(|t| t.name == "test"));
assert!(tasks.iter().any(|t| t.name == "jar"));
assert!(tasks.iter().any(|t| t.name == "run"));
assert!(tasks.iter().any(|t| t.name == "customTask"));
assert!(tasks.iter().any(|t| t.name == "anotherTask"));
let custom_task = tasks.iter().find(|t| t.name == "customTask").unwrap();
assert_eq!(custom_task.definition_type, TaskDefinitionType::Gradle);
assert_eq!(custom_task.runner, TaskRunner::Gradle);
}
#[test]
fn test_parse_kotlin_gradle() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("build.gradle.kts");
let content = r#"
plugins {
id("java")
id("org.springframework.boot") version "2.5.0"
kotlin("jvm") version "1.5.0"
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.register<Copy>("copyDocs") {
from("src/docs")
into("build/docs")
}
tasks.withType<Test> {
useJUnitPlatform()
}
task("customKotlinTask") {
doLast {
println("Running Kotlin task")
}
}
application {
mainClass.set("com.example.Application")
}
"#;
let mut file = File::create(&file_path).unwrap();
file.write_all(content.as_bytes()).unwrap();
let tasks = parse(&file_path).unwrap();
assert!(tasks.iter().any(|t| t.name == "build"));
assert!(tasks.iter().any(|t| t.name == "clean"));
assert!(tasks.iter().any(|t| t.name == "test"));
assert!(tasks.iter().any(|t| t.name == "compileKotlin"));
assert!(tasks.iter().any(|t| t.name == "copyDocs"));
assert!(tasks.iter().any(|t| t.name == "customKotlinTask"));
let custom_task = tasks.iter().find(|t| t.name == "copyDocs").unwrap();
assert_eq!(custom_task.definition_type, TaskDefinitionType::Gradle);
assert_eq!(custom_task.runner, TaskRunner::Gradle);
}
}