Use Anvil KSP instead of the Square KAPT one (#3564)
* Use Anvil KSP instead of the Square KAPT one * Fix several configuration cache, lint and test issues * Allow incremental kotlin compilation in the CI * Workaround Robolectric + Compose issue that caused `AppNotIdleException` * Update the `enterprise` commit hash * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
f344a1282c
commit
79c17f714f
54 changed files with 463 additions and 348 deletions
|
|
@ -1,144 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalAnvilApi::class)
|
||||
|
||||
package io.element.android.anvilcodegen
|
||||
|
||||
import com.google.auto.service.AutoService
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import com.squareup.anvil.annotations.ExperimentalAnvilApi
|
||||
import com.squareup.anvil.compiler.api.AnvilCompilationException
|
||||
import com.squareup.anvil.compiler.api.AnvilContext
|
||||
import com.squareup.anvil.compiler.api.CodeGenerator
|
||||
import com.squareup.anvil.compiler.api.GeneratedFile
|
||||
import com.squareup.anvil.compiler.api.createGeneratedFile
|
||||
import com.squareup.anvil.compiler.internal.asClassName
|
||||
import com.squareup.anvil.compiler.internal.buildFile
|
||||
import com.squareup.anvil.compiler.internal.fqName
|
||||
import com.squareup.anvil.compiler.internal.reference.ClassReference
|
||||
import com.squareup.anvil.compiler.internal.reference.asClassName
|
||||
import com.squareup.anvil.compiler.internal.reference.classAndInnerClassReferences
|
||||
import com.squareup.kotlinpoet.AnnotationSpec
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.FileSpec
|
||||
import com.squareup.kotlinpoet.FunSpec
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import com.squareup.kotlinpoet.STAR
|
||||
import com.squareup.kotlinpoet.TypeSpec
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.psi.KtFile
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* This is an anvil plugin that allows Node to use [ContributesNode] alone and let this plugin automatically
|
||||
* handle the rest of the Dagger wiring required for constructor injection.
|
||||
*/
|
||||
@AutoService(CodeGenerator::class)
|
||||
class ContributesNodeCodeGenerator : CodeGenerator {
|
||||
override fun isApplicable(context: AnvilContext): Boolean = true
|
||||
|
||||
override fun generateCode(codeGenDir: File, module: ModuleDescriptor, projectFiles: Collection<KtFile>): Collection<GeneratedFile> {
|
||||
return projectFiles.classAndInnerClassReferences(module)
|
||||
.filter { it.isAnnotatedWith(ContributesNode::class.fqName) }
|
||||
.flatMap { listOf(generateModule(it, codeGenDir, module), generateAssistedFactory(it, codeGenDir, module)) }
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun generateModule(nodeClass: ClassReference.Psi, codeGenDir: File, module: ModuleDescriptor): GeneratedFile {
|
||||
val generatedPackage = nodeClass.packageFqName.toString()
|
||||
val moduleClassName = "${nodeClass.shortName}_Module"
|
||||
val scope = nodeClass.annotations.single { it.fqName == ContributesNode::class.fqName }.scope()
|
||||
val content = FileSpec.buildFile(generatedPackage, moduleClassName) {
|
||||
addType(
|
||||
TypeSpec.classBuilder(moduleClassName)
|
||||
.addModifiers(KModifier.ABSTRACT)
|
||||
.addAnnotation(Module::class)
|
||||
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember("%T::class", scope.asClassName()).build())
|
||||
.addFunction(
|
||||
FunSpec.builder("bind${nodeClass.shortName}Factory")
|
||||
.addModifiers(KModifier.ABSTRACT)
|
||||
.addParameter("factory", ClassName(generatedPackage, "${nodeClass.shortName}_AssistedFactory"))
|
||||
.returns(assistedNodeFactoryFqName.asClassName(module).parameterizedBy(STAR))
|
||||
.addAnnotation(Binds::class)
|
||||
.addAnnotation(IntoMap::class)
|
||||
.addAnnotation(
|
||||
AnnotationSpec.Companion.builder(nodeKeyFqName.asClassName(module)).addMember(
|
||||
"%T::class",
|
||||
nodeClass.asClassName()
|
||||
).build()
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
return createGeneratedFile(codeGenDir, generatedPackage, moduleClassName, content)
|
||||
}
|
||||
|
||||
private fun generateAssistedFactory(nodeClass: ClassReference.Psi, codeGenDir: File, module: ModuleDescriptor): GeneratedFile {
|
||||
val generatedPackage = nodeClass.packageFqName.toString()
|
||||
val assistedFactoryClassName = "${nodeClass.shortName}_AssistedFactory"
|
||||
val constructor = nodeClass.constructors.singleOrNull { it.isAnnotatedWith(AssistedInject::class.fqName) }
|
||||
val assistedParameters = constructor?.parameters?.filter { it.isAnnotatedWith(Assisted::class.fqName) }.orEmpty()
|
||||
if (constructor == null || assistedParameters.size != 2) {
|
||||
throw AnvilCompilationException(
|
||||
"${nodeClass.fqName} must have an @AssistedInject constructor with 2 @Assisted parameters",
|
||||
element = nodeClass.clazz,
|
||||
)
|
||||
}
|
||||
val contextAssistedParam = assistedParameters[0]
|
||||
if (contextAssistedParam.name != "buildContext") {
|
||||
throw AnvilCompilationException(
|
||||
"${nodeClass.fqName} @Assisted parameter must be named buildContext",
|
||||
element = contextAssistedParam.parameter,
|
||||
)
|
||||
}
|
||||
val pluginsAssistedParam = assistedParameters[1]
|
||||
if (pluginsAssistedParam.name != "plugins") {
|
||||
throw AnvilCompilationException(
|
||||
"${nodeClass.fqName} @Assisted parameter must be named plugins",
|
||||
element = pluginsAssistedParam.parameter,
|
||||
)
|
||||
}
|
||||
|
||||
val nodeClassName = nodeClass.asClassName()
|
||||
val buildContextClassName = contextAssistedParam.type().asTypeName()
|
||||
val pluginsClassName = pluginsAssistedParam.type().asTypeName()
|
||||
val content = FileSpec.buildFile(generatedPackage, assistedFactoryClassName) {
|
||||
addType(
|
||||
TypeSpec.interfaceBuilder(assistedFactoryClassName)
|
||||
.addSuperinterface(assistedNodeFactoryFqName.asClassName(module).parameterizedBy(nodeClassName))
|
||||
.addAnnotation(AssistedFactory::class)
|
||||
.addFunction(
|
||||
FunSpec.builder("create")
|
||||
.addModifiers(KModifier.OVERRIDE, KModifier.ABSTRACT)
|
||||
.addParameter("buildContext", buildContextClassName)
|
||||
.addParameter("plugins", pluginsClassName)
|
||||
.returns(nodeClassName)
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
return createGeneratedFile(codeGenDir, generatedPackage, assistedFactoryClassName, content)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val assistedNodeFactoryFqName = FqName("io.element.android.libraries.architecture.AssistedNodeFactory")
|
||||
private val nodeKeyFqName = FqName("io.element.android.libraries.architecture.NodeKey")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.anvilcodegen
|
||||
|
||||
import com.google.devtools.ksp.KspExperimental
|
||||
import com.google.devtools.ksp.getConstructors
|
||||
import com.google.devtools.ksp.isAnnotationPresent
|
||||
import com.google.devtools.ksp.processing.CodeGenerator
|
||||
import com.google.devtools.ksp.processing.Dependencies
|
||||
import com.google.devtools.ksp.processing.KSPLogger
|
||||
import com.google.devtools.ksp.processing.Resolver
|
||||
import com.google.devtools.ksp.processing.SymbolProcessor
|
||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||
import com.google.devtools.ksp.symbol.KSType
|
||||
import com.google.devtools.ksp.validate
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import com.squareup.kotlinpoet.AnnotationSpec
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.FileSpec
|
||||
import com.squareup.kotlinpoet.FunSpec
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import com.squareup.kotlinpoet.STAR
|
||||
import com.squareup.kotlinpoet.TypeSpec
|
||||
import com.squareup.kotlinpoet.ksp.toTypeName
|
||||
import com.squareup.kotlinpoet.ksp.writeTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
|
||||
class ContributesNodeProcessor(
|
||||
private val logger: KSPLogger,
|
||||
private val codeGenerator: CodeGenerator,
|
||||
private val config: Config,
|
||||
) : SymbolProcessor {
|
||||
data class Config(
|
||||
val enableLogging: Boolean = false,
|
||||
)
|
||||
|
||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||
val annotatedSymbols = resolver.getSymbolsWithAnnotation(ContributesNode::class.qualifiedName!!)
|
||||
.filterIsInstance<KSClassDeclaration>()
|
||||
|
||||
val (validSymbols, invalidSymbols) = annotatedSymbols.partition { it.validate() }
|
||||
|
||||
if (validSymbols.isEmpty()) return invalidSymbols
|
||||
|
||||
for (ksClass in validSymbols) {
|
||||
if (config.enableLogging) {
|
||||
logger.warn("Processing ${ksClass.qualifiedName?.asString()}")
|
||||
}
|
||||
generateModule(ksClass)
|
||||
generateFactory(ksClass)
|
||||
}
|
||||
|
||||
return invalidSymbols
|
||||
}
|
||||
|
||||
private fun generateModule(ksClass: KSClassDeclaration) {
|
||||
val annotation = ksClass.annotations.find { it.shortName.asString() == "ContributesNode" }!!
|
||||
val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType
|
||||
val modulePackage = ksClass.packageName.asString()
|
||||
val moduleClassName = "${ksClass.simpleName.asString()}_Module"
|
||||
val content = FileSpec.builder(
|
||||
packageName = modulePackage,
|
||||
fileName = moduleClassName,
|
||||
)
|
||||
.addType(
|
||||
TypeSpec.classBuilder(moduleClassName)
|
||||
.addModifiers(KModifier.ABSTRACT)
|
||||
.addAnnotation(Module::class)
|
||||
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember("%T::class", scope.toTypeName()).build())
|
||||
.addFunction(
|
||||
FunSpec.builder("bind${ksClass.simpleName.asString()}Factory")
|
||||
.addModifiers(KModifier.ABSTRACT)
|
||||
.addParameter("factory", ClassName(modulePackage, "${ksClass.simpleName.asString()}_AssistedFactory"))
|
||||
.returns(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(STAR))
|
||||
.addAnnotation(Binds::class)
|
||||
.addAnnotation(IntoMap::class)
|
||||
.addAnnotation(
|
||||
AnnotationSpec.Companion.builder(ClassName.bestGuess(nodeKeyFqName.asString())).addMember(
|
||||
"%T::class",
|
||||
ClassName.bestGuess(ksClass.qualifiedName!!.asString())
|
||||
).build()
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
|
||||
content.writeTo(
|
||||
codeGenerator = codeGenerator,
|
||||
dependencies = Dependencies(
|
||||
aggregating = true,
|
||||
ksClass.containingFile!!
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(KspExperimental::class)
|
||||
private fun generateFactory(ksClass: KSClassDeclaration) {
|
||||
val generatedPackage = ksClass.packageName.asString()
|
||||
val assistedFactoryClassName = "${ksClass.simpleName.asString()}_AssistedFactory"
|
||||
val constructor = ksClass.getConstructors().singleOrNull { it.isAnnotationPresent(AssistedInject::class) }
|
||||
val assistedParameters = constructor?.parameters?.filter { it.isAnnotationPresent(Assisted::class) }.orEmpty()
|
||||
if (constructor == null || assistedParameters.size != 2) {
|
||||
error(
|
||||
"${ksClass.qualifiedName} must have an @AssistedInject constructor with 2 @Assisted parameters",
|
||||
)
|
||||
}
|
||||
val contextAssistedParam = assistedParameters[0]
|
||||
if (contextAssistedParam.name?.asString() != "buildContext") {
|
||||
error(
|
||||
"${ksClass.qualifiedName} @Assisted parameter must be named buildContext",
|
||||
)
|
||||
}
|
||||
val pluginsAssistedParam = assistedParameters[1]
|
||||
if (pluginsAssistedParam.name?.asString() != "plugins") {
|
||||
error(
|
||||
"${ksClass.qualifiedName} @Assisted parameter must be named plugins",
|
||||
)
|
||||
}
|
||||
|
||||
val nodeClassName = ClassName.bestGuess(ksClass.qualifiedName!!.asString())
|
||||
val buildContextClassName = contextAssistedParam.type.toTypeName()
|
||||
val pluginsClassName = pluginsAssistedParam.type.toTypeName()
|
||||
val content = FileSpec.builder(generatedPackage, assistedFactoryClassName)
|
||||
.addType(
|
||||
TypeSpec.interfaceBuilder(assistedFactoryClassName)
|
||||
.addSuperinterface(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(nodeClassName))
|
||||
.addAnnotation(AssistedFactory::class)
|
||||
.addFunction(
|
||||
FunSpec.builder("create")
|
||||
.addModifiers(KModifier.OVERRIDE, KModifier.ABSTRACT)
|
||||
.addParameter("buildContext", buildContextClassName)
|
||||
.addParameter("plugins", pluginsClassName)
|
||||
.returns(nodeClassName)
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
|
||||
content.writeTo(
|
||||
codeGenerator = codeGenerator,
|
||||
dependencies = Dependencies(
|
||||
aggregating = true,
|
||||
ksClass.containingFile!!
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val assistedNodeFactoryFqName = FqName("io.element.android.libraries.architecture.AssistedNodeFactory")
|
||||
private val nodeKeyFqName = FqName("io.element.android.libraries.architecture.NodeKey")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.anvilcodegen
|
||||
|
||||
import com.google.devtools.ksp.processing.SymbolProcessor
|
||||
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
|
||||
import com.google.devtools.ksp.processing.SymbolProcessorProvider
|
||||
|
||||
class ContributesNodeProcessorProvider : SymbolProcessorProvider {
|
||||
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
|
||||
val enableLogging = environment.options["enableLogging"]?.toBoolean() ?: false
|
||||
return ContributesNodeProcessor(
|
||||
logger = environment.logger,
|
||||
codeGenerator = environment.codeGenerator,
|
||||
config = ContributesNodeProcessor.Config(enableLogging = enableLogging),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
io.element.android.anvilcodegen.ContributesNodeProcessorProvider
|
||||
Loading…
Add table
Add a link
Reference in a new issue