Search code examples
kotlinkotlin-symbol-processingksp

Is it possible to get instance of an kotlin object to invoke its method in compile time during KSP phase?


I am creating a library that scans various annotations and takes actions based on them which is a standard use case of KSP (Kotlin Symbol Processing).

The non-standard thing is the fact that the user of the library can affect the result of execution by providing the path to an object taking the corresponding action (implementing the defined interface) within used annotation.

In such case, I would like to retrieve an instance of such an object and call the corresponding method from it. Unfortunately, I fail to resolve the instance of this class - the ClassNotFoundException is thrown (despite the fact that Resolver.getClassDeclarationByName() works correctly i.e.: it finds the class given its qualified name)

Here is a simplified example, presenting the problem:

Library code

package com.example.greeter.processor

import com.google.devtools.ksp.*
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*

annotation class Greet(val greeterName: String)

interface Greeter {
    fun greet(logger: KSPLogger)
}

class GreeterProcessor(
    val logger: KSPLogger
) : SymbolProcessor {
    override fun process(resolver: Resolver) = GreeterProcessorContext(logger, resolver).process()

    private class GreeterProcessorContext(val logger: KSPLogger,
                                          val resolver: Resolver) {
        fun process(): List<KSAnnotated> {
            val symbols = resolver.getSymbolsWithAnnotation(Greet::class.qualifiedName!!)
            val ret = symbols.filter { !it.validate() }.toList()
            symbols
                .filter { it is KSClassDeclaration && it.validate() }
                .forEach { it.accept(GreeterVisitor(), Unit) }
            return ret
        }

        inner class GreeterVisitor : KSVisitorVoid() {
            @OptIn(KspExperimental::class)
            override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
                val greeterName = classDeclaration.getAnnotationsByType(Greet::class).first().greeterName
                val correspondingGreeterClassDeclaration = resolver.getClassDeclarationByName(resolver.getKSNameFromString(greeterName))
                if (correspondingGreeterClassDeclaration == null) {
                    logger.error("No greeter of name $greeterName for class ${classDeclaration.qualifiedName}", classDeclaration)
                    return
                }
                val classDeclarationName = classDeclaration.simpleName.asString()
                logger.info("Corresponding greeter class declaration for $classDeclarationName: ${correspondingGreeterClassDeclaration.simpleName.asString()} - using getClassDeclarationByName")   //<=== This works fine
                val greeterClass = Class.forName(greeterName)    //<=== Throws java.lang.ClassNotFoundException
                logger.info("Corresponding greeter class declaration for $classDeclarationName: ${greeterClass.canonicalName} - using Class.forName()")
                val greeterInstance = greeterClass.kotlin.objectInstance!! as Greeter
                greeterInstance.greet(logger)
            }
        }
    }
}

Library consumer code

package com.example.greeter.implementation

import com.example.greeter.processor.*
import com.google.devtools.ksp.processing.KSPLogger

@Greet("com.example.greeter.implementation.GreeterImpl")
object GreeterTest

object GreeterImpl : Greeter {
    override fun greet(logger: KSPLogger) {
        logger.info("Hello from ${javaClass.simpleName}!")
    }
}

As you can see, it succeeded in retrieving the class declaration from KSP, but failed to obtain an instance of the class. I am aware that KSP processes classes before they are compiled, but for already compiled classes it should be possible to retrieve such an instance. The following lines of library class code describe the problem well

logger.info("Corresponding greeter class declaration for $classDeclarationName: ${correspondingGreeterClassDeclaration.simpleName.asString()} - using getClassDeclarationByName")   //<=== This works fine
val greeterClass = Class.forName(greeterName)    //<=== Throws java.lang.ClassNotFoundException
logger.info("Corresponding greeter class declaration for $classDeclarationName: ${greeterClass.canonicalName} - using Class.forName()")
val greeterInstance = greeterClass.kotlin.objectInstance!! as Greeter
greeterInstance.greet(logger)

Expected result

The expected result is to call the “greet” method from GreeterImpl. What can I do to solve my problem? The expected result is that the line

val greeterClass = Class.forName(greeterName) //<=== Throws java.lang.ClassNotFoundException

instead of throwing an exception, returns an instance of the class object, on which we then execute the greet method.

Try-it-yourself

An example ready to reproduce can be found here on my Github.

Thank you very much in advance for your help!


Solution

  • Your KSP processor program fails to access the type GreeterImpl because that class is not on the classpath of the KSP processor program. Getting that class on the classpath requires its code to have been first compiled to a .class file.

    However, as you have written it, the KSP processor program is part of that same compilation process for GreeterImpl. So the overall build is circular and it's not going to work without some rethinking.

    If it works for your design, what you can do is put GreeterImpl into another module so that it is compiled separately. Then that module/JAR can be passed to the KSP processor program as a dependency (perhaps as an implementation dependency in that program's Gradle build, if it is known at that time, or perhaps loaded at runtime via a URLClassloader from an argument passed to the processor).

    I imagine there are other possibilities depending on the wider objectives and circumstances of what you are trying to do.