MavenDependencyTreeParser.kt
package com.depanalyzer.parser.maven
import com.depanalyzer.core.graph.DependencyNode
object MavenDependencyTreeParser {
fun parse(treeOutput: String, verbose: Boolean = false): List<DependencyNode> {
if (treeOutput.isBlank()) {
if (verbose) System.err.println("ℹ️ Parser: Empty tree output")
return emptyList()
}
val lines = treeOutput.lines()
if (verbose) System.err.println("ℹ️ Parser: Processing ${lines.size} lines")
val treeLines = parseLines(lines)
if (verbose) System.err.println("ℹ️ Parser: Extracted ${treeLines.size} parsed lines")
if (treeLines.isEmpty()) {
if (verbose) System.err.println("⚠️ Parser: No valid tree lines found after parsing")
return emptyList()
}
val dependencyLines = treeLines.drop(1)
if (dependencyLines.isEmpty()) {
if (verbose) System.err.println("⚠️ Parser: No dependencies found (only root project line)")
return emptyList()
}
val minDepth = dependencyLines.minOfOrNull { it.depth } ?: 0
val adjustedLines = dependencyLines.map { treeLine ->
treeLine.copy(depth = treeLine.depth - minDepth)
}
val result = buildHierarchy(adjustedLines)
if (verbose) System.err.println("ℹ️ Parser: Built hierarchy with ${result.size} root dependencies")
return result
}
private fun parseLines(lines: List<String>): List<TreeLine> {
return lines
.mapNotNull { line ->
if (line.isBlank()) return@mapNotNull null
val cleanLine = line.replace(Regex("^\\[(INFO|WARNING|ERROR)] ?"), "")
parseLine(cleanLine)
}
.filter { it.groupId.isNotEmpty() && it.artifactId.isNotEmpty() }
}
private fun parseLine(line: String): TreeLine? {
val trimmed = line.trimEnd()
if (trimmed.isBlank()) return null
if (!trimmed.contains("|") && !trimmed.contains("+") && !trimmed.contains("\\")) {
val coords = trimmed.substringBefore(" ").trim()
if (coords.contains(":")) {
val parts = coords.split(":")
if (parts.size >= 3) {
return TreeLine(
depth = 0,
line = trimmed,
groupId = parts[0],
artifactId = parts[1],
version = parts.getOrNull(2) ?: "",
scope = null,
isDirect = true,
isExcluded = false
)
}
}
return null
}
val depth = calculateDepth(trimmed)
val contentStart = findContentStart(trimmed)
if (contentStart < 0 || contentStart >= trimmed.length) {
return null
}
val content = trimmed.substring(contentStart).trim()
if (content.isEmpty()) return null
val isExcluded = content.contains("[EXCLUDED]") ||
content.contains("[omitted for duplicate]") ||
content.contains("(omitted for duplicate)")
val (coords, scope) = extractCoordinates(content)
if (coords.isEmpty()) return null
val parts = coords.split(":")
if (parts.size < 3) return null
val (groupId, artifactId, version) = when (parts.size) {
3 -> {
Triple(parts[0], parts[1], parts[2])
}
4, 5 -> {
Triple(parts[0], parts[1], parts[parts.size - 2])
}
else -> {
return null
}
}
return TreeLine(
depth = depth,
line = trimmed,
groupId = groupId,
artifactId = artifactId,
version = version,
scope = scope,
isDirect = depth == 0,
isExcluded = isExcluded
)
}
private fun calculateDepth(line: String): Int {
var depth = 0
var i = 0
while (i < line.length) {
when {
line[i] == '|' -> {
depth++
i += 1
while (i < line.length && line[i] == ' ') {
i++
}
}
line[i] == '+' || line[i] == '\\' -> {
break
}
line[i] == ' ' && i + 2 < line.length && line[i + 1] == ' ' && line[i + 2] == ' ' -> {
depth++
i += 3
}
line[i] == ' ' -> {
i++
}
else -> {
break
}
}
}
return depth
}
private fun findContentStart(line: String): Int {
var i = 0
while (i < line.length) {
val ch = line[i]
if (ch !in setOf('|', '+', '\\', '-', ' ')) {
return i
}
i++
}
return -1
}
private fun extractCoordinates(content: String): Pair<String, String?> {
val scopeMatch = Regex("\\((\\w+)\\)").find(content)
val scope = scopeMatch?.groupValues?.getOrNull(1)
val coordinates = content
.substringBefore("(")
.substringBefore(" ")
.trim()
return Pair(coordinates, scope)
}
private fun buildHierarchy(treeLines: List<TreeLine>): List<DependencyNode> {
if (treeLines.isEmpty()) return emptyList()
val stack = mutableListOf<Pair<Int, DependencyNode>>()
val rootNodes = mutableListOf<DependencyNode>()
for (treeLine in treeLines) {
while (stack.isNotEmpty() && stack.last().first >= treeLine.depth) {
stack.removeAt(stack.size - 1)
}
val parentNode = if (stack.isEmpty()) {
null
} else {
stack.last().second
}
val node = DependencyNode(
id = treeLine.run { "$groupId:$artifactId" },
groupId = treeLine.groupId,
artifactId = treeLine.artifactId,
version = treeLine.version,
parent = parentNode,
children = mutableListOf(),
scope = treeLine.scope,
isDependencyManagement = false
)
parentNode?.addChild(node)
if (parentNode == null) {
rootNodes.add(node)
}
stack.add(Pair(treeLine.depth, node))
}
return rootNodes
}
}