How to process Kotlin code in Gradle KTS?

Viewed 443

I have a package in my app and I'd like to go over all classes in that package to then generate some JSON schema automatically.

I'd like to create a gradle task and with some sort of build-time dependency would allow me to do:

tasks.register("my fancy task") {

    doLast {
        "my.package.name".readKotlinFiles().classes.forEach { klass ->
            klass.properties["id"]... and do something here
        }
    }
}

How can you do such thing easily?

1 Answers

I was doing something similar, loading classes form the project, but not as a Gradle task or plugin but in Java application itself. To execute this logic as Gradle task / plugin I would do it in the following way:

  1. First you generate jar package from your Kotlin project as a standard process of Kotlin-Gradle build.
  2. As Gradle build is not a part of your project source code you have to load classes of the actual project with properly configured ClassLoader.
  3. Then you can read those classes and generate the report.

Now that would translate into the pseudocode of a Gradle task:

tasks.register("processClasses") {

  dependsOn tasks.named("assemble") // generate jar
  doLast {

    // Initiate classloader of assembled project

    // Read target package from gradle properties or other kind of parameters
    
    // Get list of all classes under the specific package
    
    // Load the list of classes from classloader
    
    // Execute custom logic upon the list of classes
    
    // Generate report in specific format (JSON, YAML, HTML, ...)
  }
}

I tried to validate the logic and created a POC as the following GitHub project. There are two flavors of build scripts:

  • Groovy build script is in master branch
  • Kotlin build script version is in kotlin branch. Note multiple aspects should be improved and adapted to the specific goal you have in mind (really curious actually). You are welcome to make any suggestions, fork the code, do changes..

Excerpt of the code below:


// Pack all the dependencies into jar
tasks.jar {
    from(configurations.runtimeClasspath.map { configuration ->
        configuration.asFileTree.fold(files().asFileTree) { collection, file ->
            if (file.isDirectory) collection else collection.plus(zipTree(file))
        }
    })
}

val packageToProcess: String by project

tasks.register("processClasses") {

    group = "process"
    description = "Process classes form a specific package"
    dependsOn(tasks.named("assemble"))
    doLast {

        // Instantiate classloader from jar as you will need other dependencies for loading classes
        val file: File = project.projectDir.toPath().resolve(tasks.jar.get().archiveFile.get().toString()).toFile()
        println("Packaged Kotlin project jar file: $file")
        val classloader: ClassLoader = URLClassLoader(arrayOf(file.toURI().toURL()))

        // Iterate through all of the class files in the specified package
        val packageToScan: String = packageToProcess.replace(".", File.separator)
        val path: java.nio.file.Path = project.file("build/classes/kotlin/main/").toPath().resolve(packageToScan)
        val classesFromSpecificPackage = Files.walk(path)
            .filter {
                Files.isRegularFile(it)
            }
            .map {
                project.file("build/classes/kotlin/main/").toPath().relativize(it)
            }
            .map { it.toString().substring(0, it.toString().lastIndexOf(".")).replace(File.separator, ".") }
            .map {
                //println("Class = $it")  // print class if necessary before loading it
                classloader.loadClass(it)
            }

        // Do something with the classes form specific package
        // Now just basic information is printed directly to the console
        println("=======================================")
        println("Classes from package:  $packageToScan")
        println("")
        classesFromSpecificPackage.forEach {
            println("  Class: ${it.canonicalName}")
            val fields = it.declaredFields
            for (element in fields) {
                println("  - Declared field: $element")
            }
            println("")
        }
        println("=======================================")
    }
}
Related