Browse Source
* 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>pull/3580/head
Jorge Martin Espinosa
2 weeks ago
committed by
GitHub
54 changed files with 462 additions and 347 deletions
@ -1,144 +0,0 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1 @@
|
||||
io.element.android.anvilcodegen.ContributesNodeProcessorProvider |
@ -1 +1 @@
@@ -1 +1 @@
|
||||
Subproject commit ceb65e32d95052c028a37654a4a0410639f69053 |
||||
Subproject commit b4f0427e3595049d39846aabcdc06e818f2e96ea |
@ -1,13 +0,0 @@
@@ -1,13 +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. |
||||
*/ |
||||
|
||||
package io.element.android.features.messages.impl.timeline.session |
||||
|
||||
data class SessionState( |
||||
val isSessionVerified: Boolean, |
||||
val isKeyBackupEnabled: Boolean, |
||||
) |
@ -1,18 +0,0 @@
@@ -1,18 +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. |
||||
*/ |
||||
|
||||
package io.element.android.features.roomlist.impl |
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider |
||||
|
||||
open class InvitesStateProvider : PreviewParameterProvider<InvitesState> { |
||||
override val values: Sequence<InvitesState> |
||||
get() = sequenceOf( |
||||
InvitesState.SeenInvites, |
||||
InvitesState.NewInvites, |
||||
) |
||||
} |
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
/* |
||||
* Copyright 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.tests.testutils |
||||
|
||||
import androidx.activity.ComponentActivity |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule |
||||
import org.junit.Assert.assertFalse |
||||
import org.junit.rules.TestRule |
||||
import kotlin.coroutines.CoroutineContext |
||||
|
||||
object RobolectricDispatcherCleaner { |
||||
// HACK: Workaround for https://github.com/robolectric/robolectric/issues/7055#issuecomment-1551119229 |
||||
fun clearAndroidUiDispatcher(pkg: String = "androidx.compose.ui.platform") { |
||||
val clazz = javaClass.classLoader!!.loadClass("$pkg.AndroidUiDispatcher") |
||||
val combinedContextClass = javaClass.classLoader!!.loadClass("kotlin.coroutines.CombinedContext") |
||||
val companionClazz = clazz.getDeclaredField("Companion").get(clazz) |
||||
val combinedContext = companionClazz.javaClass.getDeclaredMethod("getMain") |
||||
.invoke(companionClazz) as CoroutineContext |
||||
val androidUiDispatcher = combinedContextClass.getDeclaredField("element") |
||||
.apply { isAccessible = true } |
||||
.get(combinedContext) |
||||
.let { clazz.cast(it) } |
||||
var scheduledFrameDispatch = clazz.getDeclaredField("scheduledFrameDispatch") |
||||
.apply { isAccessible = true } |
||||
.getBoolean(androidUiDispatcher) |
||||
var scheduledTrampolineDispatch = clazz.getDeclaredField("scheduledTrampolineDispatch") |
||||
.apply { isAccessible = true } |
||||
.getBoolean(androidUiDispatcher) |
||||
val dispatchCallback = clazz.getDeclaredField("dispatchCallback") |
||||
.apply { isAccessible = true } |
||||
.get(androidUiDispatcher) as Runnable |
||||
if (scheduledFrameDispatch || scheduledTrampolineDispatch) { |
||||
dispatchCallback.run() |
||||
scheduledFrameDispatch = clazz.getDeclaredField("scheduledFrameDispatch") |
||||
.apply { isAccessible = true } |
||||
.getBoolean(androidUiDispatcher) |
||||
scheduledTrampolineDispatch = clazz.getDeclaredField("scheduledTrampolineDispatch") |
||||
.apply { isAccessible = true } |
||||
.getBoolean(androidUiDispatcher) |
||||
} |
||||
assertFalse(scheduledFrameDispatch) |
||||
assertFalse(scheduledTrampolineDispatch) |
||||
} |
||||
} |
||||
|
||||
fun <R : TestRule, A : ComponentActivity> AndroidComposeTestRule<R, A>.setSafeContent(content: @Composable () -> Unit) { |
||||
RobolectricDispatcherCleaner.clearAndroidUiDispatcher() |
||||
setContent(content) |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:0a12b9a2dfafb1f8cdb329a41c422ebe913b6a8da2f48a217955f43b9f46ac4d |
||||
size 23505 |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:053b62a43c2ebec26c9d32f9cd002e01f90665be7530d281aad302377585dfae |
||||
size 21420 |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:306561dca24390baca77657839f96b63f93114e5bce523764903cf9030185320 |
||||
size 30358 |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:c0f023eda3a5fd9ff9584e93a6685010de35fb9464025b8c82634be801ada533 |
||||
size 27476 |
Loading…
Reference in new issue