---
# System prepended metadata

title: A package dependency analyser done with JetBrains' AI assistant in IntelliJ
tags: [ai, personal]

---

Done with IntelliJ 2023.2 EAP + its AI assistant feature.
Need to have graphviz installed (`brew install graphviz`).


```kotlin=
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
import java.util.regex.Matcher
import java.util.regex.Pattern

data class PackageInfo(
    val packageName: String,
    val imports: MutableSet<String>,
)

private const val COM_CASAVO = "com.casavo"

fun main() {
    val analyzedPackages = analyzeImports("./core/src/main/kotlin")

    createGraphViz(analyzedPackages, fileName = "output.dot")
    generateImageGraph(fileName = "output.dot", imageFileName = "output.png")
}

private fun analyzeImports(sourceCodePath: String): Map<String, PackageInfo> {
    val packagePattern: Pattern = Pattern.compile("package\\s+[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*")
    val importPattern: Pattern = Pattern.compile("import\\s+([a-zA-z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*)")
    val packageMap = mutableMapOf<String, PackageInfo>()

    analyzeSourceCode(sourceCodePath, packagePattern, importPattern, packageMap)
    return packageMap
}

private fun analyzeSourceCode(
    sourceCodePath: String,
    packagePattern: Pattern,
    importPattern: Pattern,
    packageMap: MutableMap<String, PackageInfo>
) {
    File(sourceCodePath).walk().forEach { file ->
        if (file.extension != "kt") return@forEach

        val source = file.readText()
        val packageMatcher = packagePattern.matcher(source)
        val importMatcher = importPattern.matcher(source)

        val packageName = packageMatcher.findPackageName()
        if (!packageName.startsWith(COM_CASAVO)) {
            println("skipping $packageName")
            return@forEach
        }

        val packageInfo = packageMap.getOrPut(packageName) { PackageInfo(packageName, mutableSetOf()) }

        while (importMatcher.find()) {
            val fullImport = importMatcher.group().removePrefix("import").trim()
            val importedPackage = fullImport.substringBeforeLast('.').trim()

            if (!importedPackage.startsWith(COM_CASAVO)) {
                println("skipping $importedPackage")
                continue
            }

            if (importedPackage.isNotBlank()) {
                packageInfo.imports.add(importedPackage)
            }
        }
    }
}

private fun generateImageGraph(fileName: String, imageFileName: String) {
    val process = "dot -Tpng $fileName -o $imageFileName".runCommand()
    process?.let {
        val exitCode = it.waitFor()
        if (exitCode != 0) {
            println("The command finished with an unexpected exit code: $exitCode")
        }
    }
}

private fun createGraphViz(packageMap: Map<String, PackageInfo>, fileName: String) {
    val graph = StringBuilder()
    graph.append("digraph G {\n")
    graph.append("  node [shape=box]\n")
    packageMap.forEach { (_, packageInfo) ->
        packageInfo.imports.forEach { import ->
            graph.append("  \"${packageInfo.packageName}\" -> \"$import\";\n")
        }
    }
    graph.append("}")
    val dotPath = Paths.get(fileName)
    Files.write(dotPath, graph.toString().toByteArray())
}

private fun Matcher.findPackageName() = if (find()) {
    group().removePrefix("package").trim()
} else "No package"

private fun String.runCommand(): Process? {
    return try {
        val parts = this.split("\\s".toRegex())
        ProcessBuilder(*parts.toTypedArray())
            .start()
    } catch (e: java.io.IOException) {
        e.printStackTrace()
        null
    }
}

```