GradleGroovyDependencyParser.kt
package com.depanalyzer.parser
import com.depanalyzer.repository.ProjectRepository
import java.io.File
class GradleGroovyDependencyParser(
private val repoParser: GradleRepositoryParser = GradleRepositoryParser()
) {
fun parse(buildFile: File): List<ParsedGradleDependency> {
require(buildFile.exists() && buildFile.isFile) { "Invalid build.gradle path: ${buildFile.absolutePath}" }
require(buildFile.name == "build.gradle") { "Expected build.gradle, got ${buildFile.name}" }
val content = buildFile.readText()
val vars = parseVariables(content)
val dependenciesBody = extractDependenciesBody(content) ?: return emptyList()
val cleanBody = stripBlockComments(dependenciesBody)
val result = mutableListOf<ParsedGradleDependency>()
cleanBody.lines().forEach { line ->
parseDependencyLine(line, vars)?.let(result::add)
}
return result
}
fun repositories(buildFile: File): List<ProjectRepository> {
return repoParser.parse(buildFile)
}
private fun stripBlockComments(content: String): String {
val blockCommentRegex = Regex("""/\*[\s\S]*?\*/""")
return content.replace(blockCommentRegex, "")
}
private fun parseVariables(content: String): Map<String, String> {
val vars = mutableMapOf<String, String>()
// 1. Capture 'def varName = "value"'
val defRegex = Regex("""def\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*['"]([^'"]+)['"]""")
defRegex.findAll(content).forEach { match ->
vars[match.groupValues[1]] = match.groupValues[2]
}
// 2. Capture 'ext { ... }' block
val extBlockRegex = Regex("""ext\s*\{([\s\S]*?)}""")
extBlockRegex.findAll(content).forEach { blockMatch ->
val body = blockMatch.groupValues[1]
val assignmentRegex = Regex("""([A-Za-z_][A-Za-z0-9_]*)\s*=\s*['"]([^'"]+)['"]""")
assignmentRegex.findAll(body).forEach { match ->
vars[match.groupValues[1]] = match.groupValues[2]
}
}
// 3. Capture 'ext.varName = "value"'
val extDotRegex = Regex("""ext\.([A-Za-z_][A-Za-z0-9_]*)\s*=\s*['"]([^'"]+)['"]""")
extDotRegex.findAll(content).forEach { match ->
vars[match.groupValues[1]] = match.groupValues[2]
}
return vars
}
private fun extractDependenciesBody(content: String): String? {
val startRegex = Regex("""\bdependencies\s*\{""")
val startMatch = startRegex.find(content) ?: return null
val start = startMatch.range.first
val openBrace = content.indexOf('{', start)
if (openBrace == -1) return null
var depth = 0
var index = openBrace
while (index < content.length) {
when (content[index]) {
'{' -> depth++
'}' -> {
depth--
if (depth == 0) {
return content.substring(openBrace + 1, index)
}
}
}
index++
}
return null
}
private fun parseDependencyLine(line: String, vars: Map<String, String>): ParsedGradleDependency? {
val noComment = line.substringBefore("//").trim()
if (noComment.isBlank()) return null
if (noComment.contains("project(")) return null
// Flexible regex for configuration and notation
// String notation: implementation 'group:artifact:version' or implementation("group:artifact:version")
val stringNotation = Regex("""^([A-Za-z_][A-Za-z0-9_]*)\s*\(?\s*['"]([^:'"]+):([^:'"]+):([^'"]+)['"]\s*\)?$""")
stringNotation.matchEntire(noComment)?.let { match ->
val configuration = match.groupValues[1]
val groupId = match.groupValues[2]
val artifactId = match.groupValues[3]
val version = resolveVersion(match.groupValues[4], vars)
return ParsedGradleDependency(
groupId = groupId,
artifactId = artifactId,
version = version,
configuration = configuration
)
}
// Map notation: implementation group: '...', name: '...', version: '...'
// It can also be implementation(group: '...', name: '...', version: '...')
val mapNotation = Regex(
"""^([A-Za-z_][A-Za-z0-9_]*)\s*\(?\s*group\s*:\s*['"]([^'"]+)['"]\s*,\s*name\s*:\s*['"]([^'"]+)['"]\s*,\s*version\s*:\s*([^)\s]+)\s*\)?$"""
)
mapNotation.matchEntire(noComment)?.let { match ->
val configuration = match.groupValues[1]
val groupId = match.groupValues[2]
val artifactId = match.groupValues[3]
val rawVersion = match.groupValues[4].trim().removePrefix("'").removeSuffix("'").removePrefix("\"").removeSuffix("\"")
val version = resolveVersion(rawVersion, vars)
return ParsedGradleDependency(
groupId = groupId,
artifactId = artifactId,
version = version,
configuration = configuration
)
}
return null
}
private fun resolveVersion(raw: String, vars: Map<String, String>): String {
// Handle ${varName}, $varName, ext.varName, or just varName
val cleanRaw = raw.removePrefix("\${").removeSuffix("}").removePrefix("$")
return when {
cleanRaw.startsWith("ext.") -> vars[cleanRaw.removePrefix("ext.")] ?: raw
vars.containsKey(cleanRaw) -> vars[cleanRaw] ?: raw
else -> raw
}
}
}