Skip to content

BoxImportCensus.kt

src/jvmMain/kotlin/afield/examples/BoxImportCensus.kt · 190 lines

package afield.examples

import afield.runtime.kotlin.BoxImport
import afield.runtime.kotlin.ImportResolution
import afield.runtime.kotlin.ImportVerdict
import afield.runtime.kotlin.analyzeBoxImports
import afield.runtime.kotlin.boxImports
import afield.document.graph.intro
import afield.runtime.kotlin.k2ImportEvidence
import afield.runtime.kotlin.referencedNames
import java.io.File
import kotlin.system.exitProcess

object BoxImportCensus

Reads the imports of every published box and reports what they are for. Writes nothing but its report: it changes no box.

For each box (a code: | block of a top-level transcript, a *_code_.kt sidecar, or a Kotlin fence on a docs page) it records analyzeBoxImports's verdict on every import. Then, across the corpus, it ranks the imports the box intro does not already provide: these are the candidates for growing the intro. For a candidate star package it lists the class names that would clash with a name an intro star package already offers, and the boxes that use such a name and would stop compiling with "ambiguous" errors. The clash check knows classes only (the classpath index has no top-level functions), so a function clash is not reported.

./gradlew boxImportCensus --args="[--k2] [--out build/box-imports]path...": with no paths it reads the whole corpus; --k2 asks the code-use sidecar for resolution evidence, one request per box. Writes report.md and findings.tsv under --out.

object BoxImportCensus {
    private data class CorpusBox(val source: String, val line: Int, val code: String)

    private val ROOTS = listOf(
        "dev_docs/agents/transcripts", "dev_docs/agents/demos", "public/docs", "templates",
    )
    private const val GENERATED = "public/docs/assets/agent_transcripts"

fun main(args: Array<String>)

body — 64 lines, from line 38
    @JvmStatic
    fun main(args: Array<String>) {
        val useK2 = "--k2" in args
        val outIndex = args.indexOf("--out")
        val out = File(if (outIndex >= 0) args[outIndex + 1] else "build/box-imports").also { it.mkdirs() }
        val paths = args.filterIndexed { i, a -> !a.startsWith("--") && (outIndex < 0 || i != outIndex + 1) }
        val repo = repoRoot()
        val files = if (paths.isEmpty()) corpusFiles(repo) else paths.map { File(it).absoluteFile }

        val boxes = files.flatMap { boxesIn(it, repo) }
        println("BoxImportCensus: ${boxes.size} box(es) in ${files.size} file(s)${if (useK2) ", with K2 evidence" else ""}")

        val rows = mutableListOf<String>()
        val verdicts = mutableMapOf<ImportVerdict, Int>()
        val uncovered = mutableMapOf<String, MutableSet<String>>() // import path -> sources
        val namesBySource = mutableMapOf<String, Set<String>>()
        for (box in boxes) {
            val evidence = if (useK2) k2ImportEvidence(box.code) else null
            namesBySource["${box.source}:${box.line}"] = referencedNames(box.code).orEmpty()
            for (f in analyzeBoxImports(box.code, resolution = evidence)) {
                verdicts.merge(f.verdict, 1, Int::plus)
                rows += listOf(box.source, (box.line + f.import.line + 1).toString(), f.verdict.name, f.import.path, f.reason)
                    .joinToString("\t")
                if (f.verdict == ImportVerdict.NEEDED || f.verdict == ImportVerdict.KEPT)
                    uncovered.getOrPut(f.import.path) { mutableSetOf() } += box.source
            }
        }

        File(out, "findings.tsv").writeText("source\tline\tverdict\timport\treason\n" + rows.joinToString("\n") + "\n")
        File(out, "report.md").writeText(report(boxes.size, files.size, verdicts, uncovered, namesBySource))
        println("BoxImportCensus: ${verdicts.entries.joinToString { "${it.key.name.lowercase()} ${it.value}" }}; wrote ${out.path}")
        exitProcess(0)
    }

    private fun report(
        boxCount: Int,
        fileCount: Int,
        verdicts: Map<ImportVerdict, Int>,
        uncovered: Map<String, Set<String>>,
        namesBySource: Map<String, Set<String>>,
    ): String = buildString {
        appendLine("# Box imports")
        appendLine()
        appendLine("$boxCount boxes in $fileCount files.")
        appendLine()
        appendLine("| verdict | imports |")
        appendLine("| --- | --- |")
        ImportVerdict.entries.forEach { appendLine("| ${it.name.lowercase()} | ${verdicts[it] ?: 0} |") }
        appendLine()
        appendLine("## Intro candidates")
        appendLine()
        appendLine("Imports boxes keep that the intro does not provide, most-used first. A star package's")
        appendLine("clashes are class names an intro star package already offers.")
        appendLine()
        appendLine("| import | files | clashing names | boxes using a clashing name |")
        appendLine("| --- | --- | --- | --- |")
        val introStars = boxImports(intro).filter(BoxImport::star).map(BoxImport::pkg).toSet()
        uncovered.entries.sortedByDescending { it.value.size }.take(60).forEach { (path, sources) ->
            val clashes = if (path.endsWith(".*")) clashingNames(path.removeSuffix(".*"), introStars) else emptySet()
            val affected = namesBySource.filterValues { names -> names.any { it in clashes } }.keys
            appendLine("| `$path` | ${sources.size} | ${clashes.sorted().joinToString(", ").ifEmpty { "—" }} | ${affected.size} |")
        }
        packageTable(uncovered, namesBySource, introStars)
    }

The by-package table: every kept import grouped by the package it imports from.

    private fun StringBuilder.packageTable(uncovered: Map<String, Set<String>>, namesBySource: Map<String, Set<String>>, introStars: Set<String>) {
        appendLine()
        appendLine("### By package")
        appendLine()
        appendLine("The same imports grouped by package: what `import <package>.*` in the intro would cover.")
        appendLine()
        appendLine("| package | files | distinct imports | clashing names | boxes using a clashing name |")
        appendLine("| --- | --- | --- | --- | --- |")
        val byPackage = uncovered.entries.groupBy { (path, _) -> if (path.endsWith(".*")) path.removeSuffix(".*") else path.substringBeforeLast('.') }
        byPackage.filterKeys { it !in introStars }.entries.sortedByDescending { (_, e) -> e.flatMap { it.value }.toSet().size }.take(40).forEach { (pkg, entries) ->
            val files = entries.flatMap { it.value }.toSet()
            val clashes = clashingNames(pkg, introStars)
            val affected = namesBySource.filterValues { names -> names.any { it in clashes } }.keys
            appendLine("| `$pkg` | ${files.size} | ${entries.size} | ${clashes.sorted().take(12).joinToString(", ").ifEmpty { "—" }} | ${affected.size} |")
        }
    }

Simple class names in pkg that some intro star package also has a class of.

    private fun clashingNames(pkg: String, introStars: Set<String>): Set<String> =
        ImportResolution.index().filter { (name, fqns) ->
            writableClassName(name) &&
                fqns.any { it.substringBeforeLast('.') == pkg } && fqns.any { it.substringBeforeLast('.') in introStars }
        }.keys

A name box code could write: not a compiler-generated class (Companion, 1, FooKt, ...).

source — 61 lines, from line 129
    private fun writableClassName(name: String) =
        Regex("[A-Z][A-Za-z0-9_]*").matches(name) && !name.endsWith("Kt") &&
            name !in setOf("Companion", "DefaultImpls", "WhenMappings", "EntriesMappings")

    private fun corpusFiles(repo: File): List<File> = ROOTS.map { File(repo, it) }.filter(File::exists)
        .flatMap { root -> root.walkTopDown().filter(File::isFile).toList() }
        .filter { f ->
            val rel = f.relativeTo(repo).invariantSeparatorsPath
            !rel.startsWith(GENERATED) && when {
                rel.endsWith("_code_.kt") -> true
                rel.endsWith(".md") -> rel.startsWith("public/docs/")
                rel.endsWith(".yaml") -> rel.startsWith("dev_docs/agents/transcripts/") && rel.count { it == '/' } == 3
                else -> false
            }
        }.sorted()

    private fun boxesIn(file: File, repo: File): List<CorpusBox> {
        val rel = file.relativeToOrSelf(repo).invariantSeparatorsPath
        val lines = file.readLines()
        return when {
            file.name.endsWith(".kt") -> listOf(CorpusBox(rel, 0, lines.joinToString("\n")))
            file.extension == "md" -> blocks(lines, { Regex("""^`{3,}\s*(?:\{?\s*\.?kotlin\b|\S*\s*\{[^}]*\bdata-lang=kotlin\b)""").containsMatchIn(it.trim()) },
                { Regex("""^`{3,}\s*$""").matches(it.trim()) }).map { (start, body) -> CorpusBox(rel, start, body) }
            else -> yamlCodeBlocks(lines).map { (start, body) -> CorpusBox(rel, start, body) }
        }
    }

    private fun blocks(lines: List<String>, opens: (String) -> Boolean, closes: (String) -> Boolean): List<Pair<Int, String>> {
        val out = mutableListOf<Pair<Int, String>>()
        var start = -1
        lines.forEachIndexed { i, line ->
            if (start < 0 && opens(line)) start = i + 1
            else if (start >= 0 && closes(line)) { out += start to lines.subList(start, i).joinToString("\n"); start = -1 }
        }
        return out
    }

    private fun yamlCodeBlocks(lines: List<String>): List<Pair<Int, String>> {
        val out = mutableListOf<Pair<Int, String>>()
        var i = 0
        while (i < lines.size) {
            val m = Regex("""^(\s*)(?:-\s+)?code:\s*\|[-+]?\s*$""").matchEntire(lines[i])
            if (m == null) { i++; continue }
            val keyIndent = m.groupValues[1].length
            val start = i + 1
            var j = start
            while (j < lines.size && (lines[j].isBlank() || lines[j].length - lines[j].trimStart().length > keyIndent)) j++
            val block = lines.subList(start, j)
            val indent = block.filter(String::isNotBlank).minOfOrNull { it.length - it.trimStart().length } ?: 0
            out += start to block.joinToString("\n") { if (it.isBlank()) "" else it.substring(indent) }
            i = j
        }
        return out
    }

    private fun repoRoot(): File {
        var dir: File? = File(System.getProperty("user.dir")).absoluteFile
        while (dir != null && !File(dir, "settings.gradle.kts").exists()) dir = dir.parentFile
        return dir ?: File(".").absoluteFile
    }
}