What is the rationale behind KSP multiple round pr...
# ksp
m
What is the rationale behind KSP multiple round processing? Do you know any practical examples?
e
I think this is very explanatory
m
I read it, but it lacks practical example
e
Ah that is correct. I dont have any but I’m also curious.
m
I try construct it now
e
Copy code
As an example, a processor that creates a builder for an annotated class might require all parameter types of its constructors to be valid (resolved to a concrete type). In the first round, one of the parameter type is not resolvable. Then in the second round, it becomes resolvable because of the generated files from the first round.
m
Yes, but when it a problem for the generated file that an element is not resolvable?
e
Imagine code that that generates a dagger component eg
DaggerAppComponent
and in the same codebase there is another class that has a function that returns the component eg
Copy code
@ProcessWithKsp
interface Foo {
  fun component(): DaggerAppComponent
}
m
I just implemented simple DI library using providers. For the declaraions:
Copy code
@Provide
class UserRepository {
    // ...
}

@Provide
class UserService(
    val userRepository: UserRepository
) {
    // ...
}
I generated the following provider classes:
Copy code
class UserRepositoryProvider: Provider<UserRepository> {
    fun provide(): UserRepository = UserRepository()
}

class UserServiceProvider: Provider<UserService> {
    val userRepositoryProvider = UserRepositoryProvider()
    
    fun provide(): UserService = 
        UserService(userRepositoryProvider.provide())
}
Notice, that one provider depends on the other. I did not need multiple rounds for that.
My processor is messy, but this is how it looks like:
Copy code
class ProviderGenerator(
    private val logger: KSPLogger,
    private val codeGenerator: CodeGenerator,
) : SymbolProcessor {

    override fun process(resolver: Resolver): List<KSAnnotated> {
        val symbols = resolver
            .getSymbolsWithAnnotation(Provide::class.qualifiedName!!)
            .filterIsInstance<KSClassDeclaration>()

        symbols.forEach(::generateProvider)

        return emptyList()
    }

    private fun generateProvider(classDeclaration: KSClassDeclaration) {
        val className = classDeclaration.simpleName.getShortName()
        val providerName = className + "Provider"
        val constructorParameters = classDeclaration.getConstructors().firstOrNull()?.parameters.orEmpty()
        val propertySpecs = constructorParameters.mapNotNull {
            it.name ?: return@mapNotNull null
            val argumentProviderType = ClassName.bestGuess("provider." + it.type.resolve().toClassName().simpleName + "Provider")
            val argumentProperyName = it.name!!.getShortName() + "Provider"
            PropertySpec.builder(argumentProperyName, argumentProviderType)
                .initializer("${argumentProviderType.simpleName}()")
                .build()
        }
        val fileSpec = FileSpec
            .builder("provider", providerName)
            .addType(
                TypeSpec
                    .classBuilder(providerName)
                    .addProperties(
                        propertySpecs
                    )
                    .addFunction(
                        FunSpec
                            .builder("provide")
                            .returns(classDeclaration.toClassName())
                            .addCode("return $className(${propertySpecs.joinToString { "${it.name}.provide()" }})")
                            .build()
                    )
                    .build()
            )
            .build()

        val file = codeGenerator.createNewFile(fileSpec.kspDependencies(true), fileSpec.packageName, fileSpec.name)
        OutputStreamWriter(file, StandardCharsets.UTF_8)
            .use(fileSpec::writeTo)
    }
}
Uses KotlinPoet
e
When the Ksp processor is processing interface
Foo
in my example above, there is no guarantee that
DaggerAppComponent
would already have been generated (because there are no ordering guarantees between processors). So the one that handles
ProcessWithKsp
annotated classes, would defer
Foo
to the next round, pending when the return type of
component
is resolvable
m
AFAIK this feature is only needed when we actually need to reference generated element when we process another element. In your example, just like in mine, it would most likely be enough to provide class and package of generated element, and for that we do not need multiple rounds of execution. You can depend on elements that are not generated yet. That is why it is really hard for me to think of a practical use of multiple rounds of processing.
e
@marcinmoskala in your example, the symbol itself doesn’t reference the output of the processor. You are quite literally hoping that there will exist a class but you don’t know that for sure
Exactly @marcinmoskala
m
I checked a couple of libraries, and for now, they either return empty list, or they return non-empty, when they log error, no end execution anyway, so they only pretend to use this mechanism.
I made an example using this feature, but this generated class resolution is not really needed https://github.com/MarcinMoskala/DependencyInjection-KSP
e
existing precedent: https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/Processor.html
On each round, a processor may be asked to process a subset of the annotations found on the source and class files produced by a prior round.
KSP has a simpler API than APT for determining what to continue processing
IMO it's important if you have interactions between multiple processors that need some level of cooperation
j
It might not be necessary for all processors, but we still need multiple round in the case of multiple processors being applied in same project so that any processor that does expect generated symbols from other processor will work.
👍 1
110 Views