Done with IntelliJ 2023.2 EAP + its AI assistant feature.
Need to have graphviz installed (brew install graphviz
).
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
}
}