marcinmoskala
12/28/2022, 9:52 AMefemoney
12/28/2022, 11:32 AMmarcinmoskala
12/28/2022, 11:34 AMefemoney
12/28/2022, 11:35 AMmarcinmoskala
12/28/2022, 11:36 AMefemoney
12/28/2022, 11:42 AMAs 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.
marcinmoskala
12/28/2022, 11:49 AMefemoney
12/28/2022, 11:58 AMDaggerAppComponent
and in the same codebase there is another class that has a function that returns the component eg
@ProcessWithKsp
interface Foo {
fun component(): DaggerAppComponent
}
marcinmoskala
12/28/2022, 11:59 AM@Provide
class UserRepository {
// ...
}
@Provide
class UserService(
val userRepository: UserRepository
) {
// ...
}
I generated the following provider classes:
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.marcinmoskala
12/28/2022, 12:00 PMclass 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)
}
}
marcinmoskala
12/28/2022, 12:00 PMefemoney
12/28/2022, 12:01 PMFoo
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 resolvablemarcinmoskala
12/28/2022, 12:06 PMefemoney
12/28/2022, 12:06 PMefemoney
12/28/2022, 12:07 PMmarcinmoskala
12/28/2022, 12:37 PMmarcinmoskala
12/28/2022, 1:08 PMephemient
12/28/2022, 2:13 PMOn 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
ephemient
12/28/2022, 2:14 PMJiaxiang
12/28/2022, 7:50 PM