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 } } ```