diff --git a/.idea/copyright/anvil-codegen b/.idea/copyright/anvil-codegen deleted file mode 100644 index 3b8d3cd..0000000 --- a/.idea/copyright/anvil-codegen +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/.idea/copyright/anvil_codegen.xml b/.idea/copyright/anvil_codegen.xml new file mode 100644 index 0000000..eb69989 --- /dev/null +++ b/.idea/copyright/anvil_codegen.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml index 27d98f4..c2a6bbe 100644 --- a/.idea/copyright/profiles_settings.xml +++ b/.idea/copyright/profiles_settings.xml @@ -1,7 +1,7 @@ - + - + \ No newline at end of file diff --git a/activity/generator/build.gradle.kts b/activity/generator/build.gradle.kts new file mode 100644 index 0000000..2a718d7 --- /dev/null +++ b/activity/generator/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +plugins { + id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library") + id("ru.pixnews.anvil.codegen.build-logic.project.test") + id("ru.pixnews.anvil.codegen.build-logic.project.publish") + kotlin("kapt") +} + +group = "ru.pixnews.anvil.codegen.activity.generator" +version = "0.1-SNAPSHOT" + +dependencies { + api(libs.anvil.compiler.api) + implementation(libs.anvil.compiler.utils) + implementation(libs.kotlinpoet) { exclude(module = "kotlin-reflect") } + implementation(projects.common) + + compileOnly(libs.auto.service.annotations) + kapt(libs.auto.service.compiler) + + testImplementation(libs.anvil.annotations.optional) + testImplementation(libs.assertk) + testImplementation(libs.dagger) + testImplementation(projects.testUtils) + testImplementation(testFixtures(libs.anvil.compiler.utils)) +} diff --git a/activity/generator/src/main/kotlin/ru/pixnews/anvil/codegen/activity/generator/ContributesActivityCodeGenerator.kt b/activity/generator/src/main/kotlin/ru/pixnews/anvil/codegen/activity/generator/ContributesActivityCodeGenerator.kt new file mode 100644 index 0000000..234a339 --- /dev/null +++ b/activity/generator/src/main/kotlin/ru/pixnews/anvil/codegen/activity/generator/ContributesActivityCodeGenerator.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.activity.generator + +import com.google.auto.service.AutoService +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.buildFile +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.anvil.compiler.internal.reference.generateClassName +import com.squareup.anvil.compiler.internal.safePackageString +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.TypeSpec +import com.squareup.kotlinpoet.WildcardTypeName +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtFile +import ru.pixnews.anvil.codegen.common.classname.DaggerClassName +import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName +import ru.pixnews.anvil.codegen.common.fqname.FqNames +import ru.pixnews.anvil.codegen.common.util.checkClassExtendsType +import ru.pixnews.anvil.codegen.common.util.contributesToAnnotation +import java.io.File + +@AutoService(CodeGenerator::class) +public class ContributesActivityCodeGenerator : CodeGenerator { + override fun isApplicable(context: AnvilContext): Boolean = true + + override fun generateCode( + codeGenDir: File, + module: ModuleDescriptor, + projectFiles: Collection, + ): Collection { + return projectFiles + .classAndInnerClassReferences(module) + .filter { it.isAnnotatedWith(FqNames.contributesActivity) } + .map { generateActivityModule(it, codeGenDir) } + .toList() + } + + private fun generateActivityModule( + annotatedClass: ClassReference, + codeGenDir: File, + ): GeneratedFile { + annotatedClass.checkClassExtendsType(activityFqName) + + val moduleClassId = annotatedClass.generateClassName(suffix = "_ActivityModule") + val generatedPackage = moduleClassId.packageFqName.safePackageString() + val moduleClassName = moduleClassId.relativeClassName.asString() + + val moduleInterfaceSpec = TypeSpec.interfaceBuilder(moduleClassName) + .addAnnotation(DaggerClassName.module) + .addAnnotation(contributesToAnnotation(PixnewsClassName.activityScope)) + .addFunction(generateBindMethod(annotatedClass)) + .build() + + val content = FileSpec.buildFile(generatedPackage, moduleClassName) { + addType(moduleInterfaceSpec) + } + return createGeneratedFile(codeGenDir, generatedPackage, moduleClassName, content) + } + + private fun generateBindMethod( + annotatedClass: ClassReference, + ): FunSpec { + val activityClass = annotatedClass.asClassName() + + // MembersInjector + val returnType = DaggerClassName.membersInjector + .parameterizedBy(WildcardTypeName.producerOf(activityClassName)) + + return FunSpec.builder("binds${annotatedClass.shortName}Injector") + .addModifiers(KModifier.ABSTRACT) + .addAnnotation(DaggerClassName.binds) + .addAnnotation(DaggerClassName.intoMap) + .addAnnotation( + AnnotationSpec + .builder(PixnewsClassName.activityMapKey) + .addMember("activityClass = %T::class", activityClass) + .build(), + ) + .addAnnotation( + AnnotationSpec + .builder(PixnewsClassName.singleIn) + .addMember("%T::class", PixnewsClassName.activityScope) + .build(), + ) + .addParameter("target", DaggerClassName.membersInjector.parameterizedBy(activityClass)) + .returns(returnType) + .build() + } + + private companion object { + private val activityClassName = ClassName("android.app", "Activity") + private val activityFqName = FqName("android.app.Activity") + } +} diff --git a/activity/generator/src/test/kotlin/ru/pixnews/anvil/codegen/activity/generator/ContributesActivityCodeGeneratorTest.kt b/activity/generator/src/test/kotlin/ru/pixnews/anvil/codegen/activity/generator/ContributesActivityCodeGeneratorTest.kt new file mode 100644 index 0000000..efbde5b --- /dev/null +++ b/activity/generator/src/test/kotlin/ru/pixnews/anvil/codegen/activity/generator/ContributesActivityCodeGeneratorTest.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.activity.generator + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.containsExactlyInAnyOrder +import assertk.assertions.isEqualTo +import com.squareup.anvil.annotations.ContributesTo +import com.squareup.anvil.annotations.optional.SingleIn +import com.squareup.anvil.compiler.internal.testing.compileAnvil +import com.tschuchort.compiletesting.JvmCompilationResult +import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK +import dagger.Binds +import dagger.MembersInjector +import dagger.multibindings.IntoMap +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle +import org.junit.jupiter.api.fail +import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName +import ru.pixnews.anvil.codegen.testutils.haveAnnotation +import ru.pixnews.anvil.codegen.testutils.loadClass + +@OptIn(ExperimentalCompilerApi::class) +@TestInstance(Lifecycle.PER_CLASS) +class ContributesActivityCodeGeneratorTest { + private val generatedModuleName = "com.test.TestActivity_ActivityModule" + private lateinit var compilationResult: JvmCompilationResult + + @BeforeAll + @Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") + fun setup() { + val activityDiStubs = """ + package ru.pixnews.foundation.di.ui.base.activity + import android.app.Activity + import dagger.MapKey + import kotlin.reflect.KClass + + public abstract class ActivityScope private constructor() + public annotation class ActivityMapKey(val activityClass: KClass) + public annotation class ContributesActivity + """.trimIndent() + + val androidActivityStub = """ + package android.app + open class Activity + """.trimIndent() + + val testActivity = """ + package com.test + + import android.app.Activity + import ru.pixnews.foundation.di.ui.base.activity.ContributesActivity + + @ContributesActivity + class TestActivity : Activity() + """.trimIndent() + + compilationResult = compileAnvil( + sources = arrayOf( + activityDiStubs, + androidActivityStub, + testActivity, + ), + ) + } + + @Test + fun `Dagger module should be generated`() { + assertThat(compilationResult.exitCode).isEqualTo(OK) + } + + @Test + fun `Generated module should have correct annotations`() { + val clazz = compilationResult.classLoader.loadClass(generatedModuleName) + val activityScopeClass = compilationResult.classLoader.loadClass(PixnewsClassName.activityScope) + assertThat(clazz).haveAnnotation(ContributesTo::class.java) + + assertThat(clazz.getAnnotation(ContributesTo::class.java).scope.java) + .isEqualTo(activityScopeClass) + } + + @Test + fun `Generated module should have correct binding method`() { + val moduleClass = compilationResult.classLoader.loadClass(generatedModuleName) + val activityMapKey = compilationResult.classLoader.loadClass(PixnewsClassName.activityMapKey) + + val provideMethod = moduleClass.declaredMethods.firstOrNull { + it.name == "bindsTestActivityInjector" + } ?: fail("no bindsTestActivityInjector method") + + assertThat(provideMethod.returnType).isEqualTo(MembersInjector::class.java) + assertThat(provideMethod.parameterTypes) + .containsExactly(MembersInjector::class.java) + assertThat( + provideMethod.annotations.map(Annotation::annotationClass), + ).containsExactlyInAnyOrder( + Binds::class, + IntoMap::class, + SingleIn::class, + activityMapKey.kotlin, + ) + } +} diff --git a/activity/inject/build.gradle.kts b/activity/inject/build.gradle.kts new file mode 100644 index 0000000..bfce0b6 --- /dev/null +++ b/activity/inject/build.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +plugins { + id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library") + id("ru.pixnews.anvil.codegen.build-logic.project.test") + id("ru.pixnews.anvil.codegen.build-logic.project.publish") +} + +group = "ru.pixnews.anvil.codegen.activity.inject" +version = "0.1-SNAPSHOT" + +dependencies { +} diff --git a/build.gradle.kts b/build.gradle.kts index b8a228c..a2cd14d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.gradle.maven.publish.plugin.base) apply false alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.kotlin.kapt) apply false alias(libs.plugins.kotlinx.binary.compatibility.validator) apply false id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library") apply false id("ru.pixnews.anvil.codegen.build-logic.project.publish") apply false diff --git a/common/build.gradle.kts b/common/build.gradle.kts new file mode 100644 index 0000000..4161bd4 --- /dev/null +++ b/common/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +plugins { + id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library") + id("ru.pixnews.anvil.codegen.build-logic.project.test") + id("ru.pixnews.anvil.codegen.build-logic.project.publish") +} + +dependencies { + api(libs.kotlinpoet) { + exclude(module = "kotlin-reflect") + } + api(libs.anvil.compiler.api) + api(libs.anvil.compiler.utils) +} diff --git a/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/classname/AndroidClassName.kt b/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/classname/AndroidClassName.kt new file mode 100644 index 0000000..7b698d5 --- /dev/null +++ b/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/classname/AndroidClassName.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.common.classname + +import com.squareup.kotlinpoet.ClassName + +public object AndroidClassName { + @JvmStatic + public val context: ClassName = ClassName("android.content", "Context") + + @JvmStatic + public val savedStateHandle: ClassName = ClassName("androidx.lifecycle", "SavedStateHandle") + + @JvmStatic + public val workerParameters: ClassName = ClassName("androidx.work", "WorkerParameters") +} diff --git a/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/classname/AnvilClassName.kt b/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/classname/AnvilClassName.kt new file mode 100644 index 0000000..429a3ac --- /dev/null +++ b/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/classname/AnvilClassName.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.common.classname + +import com.squareup.kotlinpoet.ClassName + +public object AnvilClassName { + private const val ANVIL_ANNOTATIONS_PACKAGE = "com.squareup.anvil.annotations" + + @JvmStatic + public val contributesMultibinding: ClassName = ClassName(ANVIL_ANNOTATIONS_PACKAGE, "ContributesMultibinding") + + @JvmStatic + public val contributesTo: ClassName = ClassName(ANVIL_ANNOTATIONS_PACKAGE, "ContributesTo") +} diff --git a/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/classname/DaggerClassName.kt b/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/classname/DaggerClassName.kt new file mode 100644 index 0000000..169e1b6 --- /dev/null +++ b/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/classname/DaggerClassName.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.common.classname + +import com.squareup.kotlinpoet.ClassName + +public object DaggerClassName { + @JvmStatic + public val assistedFactory: ClassName = ClassName("dagger.assisted", "AssistedFactory") + + @JvmStatic + public val binds: ClassName = ClassName("dagger", "Binds") + + @JvmStatic + public val classKey: ClassName = ClassName("dagger.multibindings", "ClassKey") + + @JvmStatic + public val intoMap: ClassName = ClassName("dagger.multibindings", "IntoMap") + + @JvmStatic + public val intoSet: ClassName = ClassName("dagger.multibindings", "IntoSet") + + @JvmStatic + public val membersInjector: ClassName = ClassName("dagger", "MembersInjector") + + @JvmStatic + public val module: ClassName = ClassName("dagger", "Module") + + @JvmStatic + public val provides: ClassName = ClassName("dagger", "Provides") + + @JvmStatic + public val reusable: ClassName = ClassName("dagger", "Reusable") +} diff --git a/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/classname/PixnewsClassName.kt b/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/classname/PixnewsClassName.kt new file mode 100644 index 0000000..cda1cc2 --- /dev/null +++ b/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/classname/PixnewsClassName.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.common.classname + +import com.squareup.kotlinpoet.ClassName + +public object PixnewsClassName { + @JvmStatic + public val applicationContext: ClassName = ClassName( + "ru.pixnews.foundation.di.base.qualifiers", + "ApplicationContext", + ) + + @JvmStatic + public val singleIn: ClassName = ClassName( + "com.squareup.anvil.annotations.optional", + "SingleIn", + ) + + @JvmStatic + public val activityMapKey: ClassName = ClassName( + "ru.pixnews.foundation.di.ui.base.activity", + "ActivityMapKey", + ) + + @JvmStatic + public val coroutineWorkerMapKey: ClassName = ClassName( + "ru.pixnews.foundation.di.workmanager", + "CoroutineWorkerMapKey", + ) + + @JvmStatic + public val activityScope: ClassName = ClassName( + "ru.pixnews.foundation.di.ui.base.activity", + "ActivityScope", + ) + + @JvmStatic + public val appInitializersScope: ClassName = ClassName( + "ru.pixnews.foundation.initializers.inject", + "AppInitializersScope", + ) + + @JvmStatic + public val appScope: ClassName = ClassName( + "ru.pixnews.foundation.di.base.scope", + "AppScope", + ) + + @JvmStatic + public val coroutineWorkerFactory: ClassName = ClassName( + "ru.pixnews.foundation.di.workmanager", + "CoroutineWorkerFactory", + ) + + @JvmStatic + public val workManagerScope: ClassName = ClassName( + "ru.pixnews.foundation.di.workmanager", + "WorkManagerScope", + ) + + @JvmStatic + public val experiment: ClassName = ClassName( + "ru.pixnews.foundation.featuretoggles", + "Experiment", + ) + + @JvmStatic + public val experimentScope: ClassName = ClassName( + "ru.pixnews.foundation.featuretoggles.inject", + "ExperimentScope", + ) + + @JvmStatic + public val experimentVariantMapKey: ClassName = ClassName( + "ru.pixnews.foundation.featuretoggles.inject", + "ExperimentVariantMapKey", + ) + + @JvmStatic + public val experimentVariantSerializer: ClassName = ClassName( + "ru.pixnews.foundation.featuretoggles", + "ExperimentVariantSerializer", + ) + + @JvmStatic + public val singleInstrumentedTestInjector: ClassName = ClassName( + "ru.pixnews.foundation.instrumented.test.di", + "SingleInstrumentedTestInjector", + ) + + @JvmStatic + public val viewModelFactory: ClassName = ClassName( + "ru.pixnews.foundation.di.ui.base.viewmodel", + "ViewModelFactory", + ) + + @JvmStatic + public val viewModelMapKey: ClassName = ClassName( + "ru.pixnews.foundation.di.ui.base.viewmodel", + "ViewModelMapKey", + ) + + @JvmStatic + public val viewModelScope: ClassName = ClassName( + "ru.pixnews.foundation.di.ui.base.viewmodel", + "ViewModelScope", + ) + + @JvmStatic + public val asyncInitializer: ClassName = ClassName( + "ru.pixnews.foundation.initializers", + "AsyncInitializer", + ) + + @JvmStatic + public val initializer: ClassName = ClassName( + "ru.pixnews.foundation.initializers", + "Initializer", + ) +} diff --git a/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/fqname/FqNames.kt b/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/fqname/FqNames.kt new file mode 100644 index 0000000..190fe0a --- /dev/null +++ b/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/fqname/FqNames.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.common.fqname + +import com.squareup.kotlinpoet.ClassName +import org.jetbrains.kotlin.name.FqName +import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName + +public object FqNames { + @JvmField + public val contributesActivity: FqName = + FqName("ru.pixnews.foundation.di.ui.base.activity.ContributesActivity") + + @JvmField + public val contributesCoroutineWorker: FqName = + FqName("ru.pixnews.foundation.di.workmanager.ContributesCoroutineWorker") + + @JvmField + public val contributesExperiment: FqName = + FqName("ru.pixnews.foundation.featuretoggles.inject.ContributesExperiment") + + @JvmField + public val contributesInitializer: FqName = + FqName("ru.pixnews.foundation.initializers.inject.ContributesInitializer") + + @JvmField + public val contributesTest: FqName = FqName("ru.pixnews.foundation.instrumented.test.di.ContributesTest") + + @JvmField + public val contributesVariantSerializer: FqName = FqName( + "ru.pixnews.foundation.featuretoggles.inject.ContributesExperimentVariantSerializer", + ) + + @JvmField + public val contributesViewModel: FqName = + FqName("ru.pixnews.foundation.di.ui.base.viewmodel.ContributesViewModel") + + @JvmField + public val experiment: FqName = PixnewsClassName.experiment.asFqName() + + @JvmField + public val experimentVariantSerializer: FqName = PixnewsClassName.experimentVariantSerializer.asFqName() + + private fun ClassName.asFqName(): FqName = FqName(this.canonicalName) +} diff --git a/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/util/AnnotationSpecs.kt b/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/util/AnnotationSpecs.kt new file mode 100644 index 0000000..066a78a --- /dev/null +++ b/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/util/AnnotationSpecs.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.common.util + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.TypeName +import ru.pixnews.anvil.codegen.common.classname.AnvilClassName + +/** + * `@ContributesTo(className::class, replaces = [..])` + */ +public fun contributesToAnnotation( + className: ClassName, + replaces: List = emptyList(), +): AnnotationSpec { + return with(AnnotationSpec.builder(AnvilClassName.contributesTo)) { + addMember("%T::class", className) + if (replaces.isNotEmpty()) { + @Suppress("SpreadOperator") + addMember( + "replaces = [${replaces.joinToString(",") { "%T::class" }}]", + *replaces.toTypedArray(), + ) + } + build() + } +} + +/** + * `@ContributesTo(className::class)` + */ +public fun contributesMultibindingAnnotation(scope: ClassName): AnnotationSpec { + return AnnotationSpec.builder(AnvilClassName.contributesMultibinding) + .addMember("scope = %T::class", scope) + .build() +} diff --git a/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/util/ConstructorParameter.kt b/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/util/ConstructorParameter.kt new file mode 100644 index 0000000..0e53b31 --- /dev/null +++ b/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/util/ConstructorParameter.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.common.util + +import com.squareup.anvil.compiler.internal.reference.ClassReference +import com.squareup.anvil.compiler.internal.reference.ParameterReference +import com.squareup.kotlinpoet.TypeName +import ru.pixnews.anvil.codegen.common.classname.AndroidClassName + +public class ConstructorParameter( + public val name: String, + public val resolvedType: TypeName, +) + +public fun ConstructorParameter.isSavedStateHandle(): Boolean = resolvedType == AndroidClassName.savedStateHandle + +public fun List.parseConstructorParameters( + implementingClass: ClassReference, +): List = this.map { + ConstructorParameter(it.name, it.resolveTypeName(implementingClass)) +} diff --git a/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/util/Preconditions.kt b/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/util/Preconditions.kt new file mode 100644 index 0000000..556b73e --- /dev/null +++ b/common/src/main/kotlin/ru/pixnews/anvil/codegen/common/util/Preconditions.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.common.util + +import com.squareup.anvil.compiler.internal.reference.AnvilCompilationExceptionClassReference +import com.squareup.anvil.compiler.internal.reference.ClassReference +import com.squareup.anvil.compiler.internal.reference.allSuperTypeClassReferences +import org.jetbrains.kotlin.name.FqName + +public fun ClassReference.checkClassExtendsType(type: FqName) { + if (allSuperTypeClassReferences().none { it.fqName == type }) { + throw AnvilCompilationExceptionClassReference( + message = "${this.fqName} doesn't extend $type", + classReference = this, + ) + } +} + +public fun ClassReference.checkClassExtendsAnyOfType( + vararg types: FqName, +) { + if (allSuperTypeClassReferences().none { it.fqName in types }) { + throw AnvilCompilationExceptionClassReference( + message = "${this.fqName} doesn't extend any of $types", + classReference = this, + ) + } +} diff --git a/experiment/generator/build.gradle.kts b/experiment/generator/build.gradle.kts new file mode 100644 index 0000000..3290892 --- /dev/null +++ b/experiment/generator/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +plugins { + id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library") + id("ru.pixnews.anvil.codegen.build-logic.project.test") + id("ru.pixnews.anvil.codegen.build-logic.project.publish") + kotlin("kapt") +} + +group = "ru.pixnews.anvil.codegen.experiment.generator" +version = "0.1-SNAPSHOT" + +dependencies { + api(libs.anvil.compiler.api) + implementation(libs.anvil.compiler.utils) + implementation(libs.kotlinpoet) { exclude(module = "kotlin-reflect") } + implementation(projects.common) + + compileOnly(libs.auto.service.annotations) + kapt(libs.auto.service.compiler) + + testImplementation(libs.anvil.annotations.optional) + testImplementation(libs.assertk) + testImplementation(libs.dagger) + testImplementation(projects.testUtils) + testImplementation(testFixtures(libs.anvil.compiler.utils)) +} diff --git a/experiment/generator/src/main/kotlin/ru/pixnews/anvil/codegen/experiment/generator/ContributesExperimentCodeGenerator.kt b/experiment/generator/src/main/kotlin/ru/pixnews/anvil/codegen/experiment/generator/ContributesExperimentCodeGenerator.kt new file mode 100644 index 0000000..5c99a0f --- /dev/null +++ b/experiment/generator/src/main/kotlin/ru/pixnews/anvil/codegen/experiment/generator/ContributesExperimentCodeGenerator.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.experiment.generator + +import com.google.auto.service.AutoService +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.buildFile +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.anvil.compiler.internal.reference.generateClassName +import com.squareup.anvil.compiler.internal.safePackageString +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.TypeSpec +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.psi.KtFile +import ru.pixnews.anvil.codegen.common.classname.DaggerClassName +import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName +import ru.pixnews.anvil.codegen.common.fqname.FqNames +import ru.pixnews.anvil.codegen.common.util.checkClassExtendsType +import ru.pixnews.anvil.codegen.common.util.contributesToAnnotation +import java.io.File + +@AutoService(CodeGenerator::class) +public class ContributesExperimentCodeGenerator : CodeGenerator { + override fun isApplicable(context: AnvilContext): Boolean = true + + override fun generateCode( + codeGenDir: File, + module: ModuleDescriptor, + projectFiles: Collection, + ): Collection { + val experimentAnnotatedClass = projectFiles.classAndInnerClassReferences(module) + .filter { classRef -> + classRef.annotations.any { annotationRef -> + annotationRef.fqName == FqNames.contributesExperiment || + annotationRef.fqName == FqNames.contributesVariantSerializer + } + } + .toSortedSet() + + return buildList { + if (experimentAnnotatedClass.isNotEmpty()) { + add(generateExperimentModule(experimentAnnotatedClass, codeGenDir)) + } + } + } + + private fun generateExperimentModule( + annotatedClasses: Collection, + codeGenDir: File, + ): GeneratedFile { + val moduleClassId = annotatedClasses.first().generateClassName(suffix = "_Experiments_Module") + val generatedPackage = moduleClassId.packageFqName.safePackageString() + val moduleClassName = moduleClassId.relativeClassName.asString() + + val moduleTypeSpecBuilder = TypeSpec.objectBuilder(moduleClassName) + .addAnnotation(DaggerClassName.module) + .addAnnotation(contributesToAnnotation(PixnewsClassName.experimentScope)) + + annotatedClasses.forEach { + moduleTypeSpecBuilder.addFunction(generateProvidesMethod(it)) + } + + val content = FileSpec.buildFile(generatedPackage, moduleClassName) { + addType(moduleTypeSpecBuilder.build()) + } + return createGeneratedFile(codeGenDir, generatedPackage, moduleClassName, content) + } + + private fun generateProvidesMethod(annotatedClass: ClassReference): FunSpec { + val experimentAnnotations = annotatedClass.annotations.filter { annotationRef -> + annotationRef.fqName == FqNames.contributesExperiment || + annotationRef.fqName == FqNames.contributesVariantSerializer + } + + require(experimentAnnotations.size == 1) { + "$annotatedClass has an incorrect combination of annotations" + } + + val annotation = experimentAnnotations.single() + return when (annotation.fqName) { + FqNames.contributesExperiment -> providesExperimentFunction(annotatedClass) + FqNames.contributesVariantSerializer -> { + val experimentKeyAnnotation = annotation.arguments.singleOrNull() + ?: throw IllegalArgumentException("experimentKey on ContributesExperimentVariant not defined") + + providesExperimentVariantSerializerFunction( + annotatedSerializer = annotatedClass, + experimentVariantKey = experimentKeyAnnotation.value(), + ) + } + + else -> throw IllegalArgumentException("Unknown annotation $annotation") + } + } + + /** + * `@Provides @IntoSet abstract fun provideMainExperiment(experiment: MainExperiment): Experiment` + */ + private fun providesExperimentFunction(annotatedExperiment: ClassReference): FunSpec { + annotatedExperiment.checkClassExtendsType(FqNames.experiment) + + return FunSpec.builder("provide${annotatedExperiment.shortName}") + .addAnnotation(DaggerClassName.provides) + .addAnnotation(DaggerClassName.intoSet) + .returns(PixnewsClassName.experiment) + .addCode("return %T", annotatedExperiment.asClassName()) + .build() + } + + /** + * `@Provides @IntoSet abstract fun provideMainExperiment(experiment: MainExperiment): Experiment` + */ + private fun providesExperimentVariantSerializerFunction( + annotatedSerializer: ClassReference, + experimentVariantKey: String, + ): FunSpec { + annotatedSerializer.checkClassExtendsType(FqNames.experimentVariantSerializer) + + return FunSpec.builder("provide${annotatedSerializer.shortName}") + .addAnnotation(DaggerClassName.provides) + .addAnnotation(DaggerClassName.intoMap) + .addAnnotation( + AnnotationSpec + .builder(PixnewsClassName.experimentVariantMapKey) + .addMember("key = %S", experimentVariantKey) + .build(), + ) + .returns(PixnewsClassName.experimentVariantSerializer) + .addCode("return %T", annotatedSerializer.asClassName()) + .build() + } +} diff --git a/experiment/generator/src/test/kotlin/ru/pixnews/anvil/codegen/experiment/generator/ContributesExperimentCodeGeneratorTest.kt b/experiment/generator/src/test/kotlin/ru/pixnews/anvil/codegen/experiment/generator/ContributesExperimentCodeGeneratorTest.kt new file mode 100644 index 0000000..0a10396 --- /dev/null +++ b/experiment/generator/src/test/kotlin/ru/pixnews/anvil/codegen/experiment/generator/ContributesExperimentCodeGeneratorTest.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.experiment.generator + +import assertk.assertThat +import assertk.assertions.containsExactlyInAnyOrder +import assertk.assertions.isEmpty +import assertk.assertions.isEqualTo +import com.squareup.anvil.annotations.ContributesTo +import com.squareup.anvil.compiler.internal.testing.compileAnvil +import com.tschuchort.compiletesting.JvmCompilationResult +import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK +import dagger.Provides +import dagger.multibindings.IntoMap +import dagger.multibindings.IntoSet +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle +import org.junit.jupiter.api.fail +import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName +import ru.pixnews.anvil.codegen.testutils.getElementValue +import ru.pixnews.anvil.codegen.testutils.haveAnnotation +import ru.pixnews.anvil.codegen.testutils.loadClass + +@TestInstance(Lifecycle.PER_CLASS) +class ContributesExperimentCodeGeneratorTest { + private val generatedModuleName = "com.test.TestExperiment_Experiments_Module" + private lateinit var compilationResult: JvmCompilationResult + + @BeforeAll + fun setup() { + compilationResult = compileAnvil( + """ + package ru.pixnews.foundation.featuretoggles + interface Experiment + interface ExperimentVariantSerializer + """.trimIndent(), + """ + package ru.pixnews.foundation.featuretoggles.inject + annotation class ExperimentVariantMapKey(val key: String) + public annotation class ContributesExperiment + public annotation class ContributesExperimentVariantSerializer(val experimentKey: String) + public abstract class ExperimentScope private constructor() + """.trimIndent(), + """ + package com.test + import ru.pixnews.foundation.featuretoggles.Experiment + import ru.pixnews.foundation.featuretoggles.ExperimentVariantSerializer + import ru.pixnews.foundation.featuretoggles.inject.ContributesExperiment + import ru.pixnews.foundation.featuretoggles.inject.ContributesExperimentVariantSerializer + + @ContributesExperiment + public object TestExperiment : Experiment { + @ContributesExperimentVariantSerializer(experimentKey = "test.serializer") + public object TestExperimentSerializer : ExperimentVariantSerializer + + @ContributesExperimentVariantSerializer("test.serializer.no.key") + public object TestNoKeyExperimentSerializer : ExperimentVariantSerializer + } + """.trimIndent(), + ) + } + + @Test + fun `Dagger module should be generated`() { + assertThat(compilationResult.exitCode).isEqualTo(OK) + } + + @Test + fun `Generated module should have correct annotations`() { + val clazz = compilationResult.classLoader.loadClass(generatedModuleName) + assertThat(clazz).haveAnnotation(ContributesTo::class.java) + } + + @Test + fun `Generated module should have correct providing method for experiment`() { + val moduleClass = compilationResult.classLoader.loadClass(generatedModuleName) + val experimentClass = compilationResult.classLoader.loadClass(PixnewsClassName.experiment) + + val provideMethod = moduleClass.declaredMethods.firstOrNull { + it.name == "provideTestExperiment" + } ?: fail("no provideTestExperiment method") + + assertThat(provideMethod.returnType).isEqualTo(experimentClass) + assertThat(provideMethod.parameterTypes).isEmpty() + assertThat( + provideMethod.annotations.map(Annotation::annotationClass), + ).containsExactlyInAnyOrder( + Provides::class, + IntoSet::class, + ) + } + + @Test + fun `Generated module should have providing method for experiment variant serializer`() { + testExperimentVariantProvideMethod( + methodName = "provideTestExperimentSerializer", + experimentKey = "test.serializer", + ) + } + + @Test + fun `Generated module should have providing method for experiment variant serializer with no key parameter`() { + testExperimentVariantProvideMethod( + methodName = "provideTestNoKeyExperimentSerializer", + experimentKey = "test.serializer.no.key", + ) + } + + private fun testExperimentVariantProvideMethod( + methodName: String, + experimentKey: String, + ) { + val moduleClass = compilationResult.classLoader.loadClass(generatedModuleName) + val experimentVariantSerializerClass = compilationResult.classLoader.loadClass( + PixnewsClassName.experimentVariantSerializer, + ) + + @Suppress("UNCHECKED_CAST") val experimentVariantMapKey = compilationResult.classLoader + .loadClass(PixnewsClassName.experimentVariantMapKey) as Class + + val provideMethod = moduleClass.declaredMethods.firstOrNull { it.name == methodName } + ?: fail("no $methodName method") + + assertThat(provideMethod.returnType).isEqualTo(experimentVariantSerializerClass) + assertThat(provideMethod.parameterTypes).isEmpty() + assertThat( + provideMethod.annotations.map(Annotation::annotationClass), + ).containsExactlyInAnyOrder( + Provides::class, + IntoMap::class, + experimentVariantMapKey.kotlin, + ) + assertThat( + provideMethod.getAnnotation(experimentVariantMapKey).getElementValue("key"), + ).isEqualTo(experimentKey) + } +} diff --git a/experiment/inject/build.gradle.kts b/experiment/inject/build.gradle.kts new file mode 100644 index 0000000..410db50 --- /dev/null +++ b/experiment/inject/build.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +plugins { + id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library") + id("ru.pixnews.anvil.codegen.build-logic.project.test") + id("ru.pixnews.anvil.codegen.build-logic.project.publish") +} + +group = "ru.pixnews.anvil.codegen.experiment.inject" +version = "0.1-SNAPSHOT" + +dependencies { +} diff --git a/gradle.properties b/gradle.properties index a433b8e..d4c33a0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,6 @@ org.gradle.daemon.performance.enable-monitoring=false kotlin.code.style=official +kapt.include.compile.classpath=false + systemProp.org.gradle.s3.endpoint=https://storage.yandexcloud.net diff --git a/gradle/build-logic/project/kotlin/src/main/kotlin/ru.pixnews.anvil.codegen.build-logic.project.kotlin.library.gradle.kts b/gradle/build-logic/project/kotlin/src/main/kotlin/ru.pixnews.anvil.codegen.build-logic.project.kotlin.library.gradle.kts index b533c1a..7989790 100644 --- a/gradle/build-logic/project/kotlin/src/main/kotlin/ru.pixnews.anvil.codegen.build-logic.project.kotlin.library.gradle.kts +++ b/gradle/build-logic/project/kotlin/src/main/kotlin/ru.pixnews.anvil.codegen.build-logic.project.kotlin.library.gradle.kts @@ -22,8 +22,10 @@ kotlin { languageVersion = KOTLIN_1_9 apiVersion = KOTLIN_1_9 freeCompilerArgs.addAll( - "-opt-in=kotlin.RequiresOptIn", "-Xjvm-default=all", + "-opt-in=com.squareup.anvil.annotations.ExperimentalAnvilApi", + "-opt-in=kotlin.RequiresOptIn", + "-opt-in=org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi", ) } } diff --git a/gradle/build-logic/settings/src/main/kotlin/ru.pixnews.anvil.codegen.build-logic.settings.root.settings.gradle.kts b/gradle/build-logic/settings/src/main/kotlin/ru.pixnews.anvil.codegen.build-logic.settings.root.settings.gradle.kts index d47ee57..4cd0a6d 100644 --- a/gradle/build-logic/settings/src/main/kotlin/ru.pixnews.anvil.codegen.build-logic.settings.root.settings.gradle.kts +++ b/gradle/build-logic/settings/src/main/kotlin/ru.pixnews.anvil.codegen.build-logic.settings.root.settings.gradle.kts @@ -11,3 +11,5 @@ plugins { id("ru.pixnews.anvil.codegen.build-logic.settings.repositories") id("ru.pixnews.anvil.codegen.build-logic.settings.gradle-enterprise") } + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c43bb82..c535137 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,11 @@ [versions] kotlin = "1.9.22" +anvil = "2.4.8" +auto-service = "1.1.1" assertk = "0.28.0" detekt = "1.23.4" +dagger = "2.50" diktat = "2.0.0" gradle-maven-publish-plugin = "0.27.0" junit5 = "5.10.1" @@ -12,7 +15,14 @@ kotlinx-binary-compatibility-validator = "0.13.2" spotless = "6.23.3" [libraries] +anvil-annotations-optional = { group = "com.squareup.anvil", name = "annotations-optional", version.ref = "anvil" } +anvil-compiler-api = { group = "com.squareup.anvil", name = "compiler-api", version.ref = "anvil" } +anvil-compiler-utils = { group = "com.squareup.anvil", name = "compiler-utils", version.ref = "anvil" } +auto-service-annotations = { group = "com.google.auto.service", name = "auto-service-annotations", version.ref = "auto-service" } +auto-service-compiler = { group = "com.google.auto.service", name = "auto-service", version.ref = "auto-service" } assertk = { group = "com.willowtreeapps.assertk", name = "assertk", version.ref = "assertk" } +dagger = { group = "com.google.dagger", name = "dagger", version.ref = "dagger" } +dagger-compiler = { group = "com.google.dagger", name = "dagger-compiler", version.ref = "dagger" } junit-bom = { group = "org.junit", name = "junit-bom", version.ref = "junit5" } junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter" } junit-jupiter-api = { group = "org.junit.jupiter", name = "junit-jupiter-api" } @@ -31,5 +41,6 @@ gradle-maven-publish-plugin = { module = "com.vanniktech:gradle-maven-publish-pl [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } gradle-maven-publish-plugin-base = { id = "com.vanniktech.maven.publish.base", version.ref = "gradle-maven-publish-plugin" } kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "kotlinx-binary-compatibility-validator" } diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys index 2a9ecfc..6d36ed4 100644 --- a/gradle/verification-keyring.keys +++ b/gradle/verification-keyring.keys @@ -102,6 +102,35 @@ IsGn88kjyyYqOy8WJEjoOXFh++dpWiM7nZkgQcNi5A== =ggBv -----END PGP PUBLIC KEY BLOCK----- +pub 88BB19A33A18445F +uid Thomas Broyer + +sub FF59C22B07640A16 +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: BCPG v1.68 + +mQENBE//SjoBCADao3lh/I96fWIY2ZU49ljtHR4Vnzmifm3URFNuv/c8McWGxxCy +Y1+oolgVuJcy4hCqcgbkwTiAfBhjZSmsC1QK/2Vs1awFzGccPcgTBakFw/TUav12 +6Zb8y72dH0VxxcN/HUGBUOSgZg9IMe7AmmVnxbJ2ED1I3/opkC6ElPXFOl8EJdgE +Wvinp4ok3mwBGMIexQDyEN4DviuqvmB4K+gYCjS33HtHh4OrkXkCO5pDNUDgkAZK +1uG3GfmxGBjdG6nPWgIuDMEL3j1cW9r5D6I5obXsFlg6bX8mBs91jAtmfTNv+IAB +bwUOAJC+9C3ZEIsZOcBSSdUIXmuRPa51oP9nABEBAAG0IVRob21hcyBCcm95ZXIg +PHQuYnJveWVyQGx0Z3QubmV0PrkBDQRP/0o6AQgA6iTExu1NjbMu90BYP3E8ePWR +k9OE3ujnYD0C3DTMqOI0WX2PL4gVqs811szPCihBaDHljdJsp1IJIOU/vimwQw62 +0R3D/bfC3egbvQjzhG94u5Oz51MNEB3nDyPEteGOb5DGGIT7P6l5WF97/+7X6Sfa +/N6xcwhEF1BOKSMhndblMyC75FXsWB/nNRZMRROezWSYz31c+E5WHkEivWSS3L8X +KD/VaDzuV4zdZlSh7/tudaO75hKCNa/HC/wcQFg8pyI0bmfSg4+hTeOTIS6Alp+a +WEC3cICCYVt+smCSdxc++jDbErfXaLLTEiyUCqbR3Lb4T2OFguVLencnxMs8aQAR +AQABiQEfBBgBAgAJBQJP/0o6AhsMAAoJEIi7GaM6GERfmzcH+wVzLATCgDjKXNJK +xVy2numMNzNzOUPUye8I0/2V3PNTag4YB268X7PMk0vXrYmox3VMxidhE6hmEhLv +wd74uuIFKMKzB4oOoSLHXaa25asAgKkXdRxjxYswHpJeBc+qdlLVzD4+uv41We5H +7Qb9xQmJ5V0o7mtxi7Cuzg4aHasQxEKSwBjkUAx7WVIHiaP2MgYpbQHfUPf8DE2V +C43VpHMYuPvaTp3tD3U6ttDX7IbXYIvBJ3qZLiRUEWOmlQMkIo34cPw0ZD0S3KJW +nHb+CaqyEVV3BSACpDi0q0UjvXduLzHP+g2IeQ59yUd2AwwTUZTaf6AScItGmYLd +CqZ5vSc= +=B+Kr +-----END PGP PUBLIC KEY BLOCK----- + pub 893A028475557671 uid Gradle Inc. @@ -193,6 +222,48 @@ bZRG =N23Z -----END PGP PUBLIC KEY BLOCK----- +pub 9A259C7EE636C5ED +sub D66472CF54179CC4 +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: BCPG v1.68 + +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 +DG71mmILRI1wVgrDp5mFKeV/d20uMvMq +=IIN3 +-----END PGP PUBLIC KEY BLOCK----- + pub A1AE06236CA2BA62 uid DiffPlug LLC @@ -230,6 +301,32 @@ Q/IKL5Iy5doINK/iyjb/G/JLH1/TkhW9zEheiKUY6TiXeR3p =v9Tm -----END PGP PUBLIC KEY BLOCK----- +pub A6ADFC93EF34893E +sub 9C4C23E6FFE405BD +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: BCPG v1.68 + +mQENBE+xZxIBCACzKctn4ez8xOC0pGThhAwjYWGkzcwK4HNaC1usHThBFz3/t8JN +OqUXRixLyi5wELN6GHlsGVUQS3IfB4JtuhScsieSB8PTree68/knMq6JI08mJqZr +9nFrAB4eDW0UMbSL9kPmclUm/yN+qcCZBrsVn0q6CWb/Kcd8EEXEu6sGILzOGqGe +d433t5O+tGXWL2TjAz+Scsk2Hf4zcuDeQcxELAMnVaVgKuGuEZvibrjsdIvJDGI+ +0BzWIu8ZP8ldBl4SVtzGpEVzLvDUo3mOqBeTkj3rP7xLtFDN/3AFtowbLfL7L2Pg +SMcTnKK+jfFHRfbHP1Ih3rQ4ilLzhCnY/QIZABEBAAG5AQ0ET7FnEgEIAM3i3e1s +jwrx2PN8XYMPQWG+/YTtw1BYDl2+iYE+LaZvtq1hpbgeCLgEVwXrCJ4spLP1rFXo +gWqKrkJ0LRjlpdKhKBvyH1ex4grh3cWN/bIDJcJ7JA4I/Bhqhlh8hYycS9pGFeS+ +MR3aFIsii+vadrwYYvuVYGeWvdZhB7mJKYevj5Ms0OpYTfZd95Pzo4o//lNpDnrG +7Xd3tgTNU/fkpw6rFB/2Ib1Qlk+Kz1z6JNsp+tOPGGCBrzwfwglcikTuqS+xyRgC +9cHh5eCol11uSoWPKcQR2Ar8Eo56nxv/UApdu15iJ7R8cA5guKeeS4jt0CGCPs2P +huggDxI73Xvl4zsAEQEAAYkBHwQYAQIACQUCT7FnEgIbDAAKCRCmrfyT7zSJPuyl +B/9iwtIQeexMWBmQNdDe0md8HLulDfcujPtklrvYHtXMJQFaGA0Vafq0oT9MhBfb +1YCP79uF0qgswSxINYCOJx4nTPIP9BOdTwqfGo7ul27REgNq4lIUW0GkMgZAUA2f +t/vc0u/I0PqnhKCi4Pq79hLIx7eiX2ySfXfYfLXRVzbMWKMoi7lWXseQqbM0RvCA +54J1qAi6Ew+JyoYGQ7OvXdL5Eh5Tkm2cpIADyqCkp/aFDe5lqZiU1zS2fU6mpOf/ +o0co+GoYkieIxxibDCmt3BioLgmyzpGUsMNwh4pAIQUGkcxd4spC0KIWdDEvq/QJ +EEIhZlI/ojefaZkRseFrtl3X +=pJaU +-----END PGP PUBLIC KEY BLOCK----- + pub B0F3710FA64900E7 uid ?amonn McManus @@ -259,6 +356,32 @@ thPLFHFc831L =obUO -----END PGP PUBLIC KEY BLOCK----- +pub B16698A4ADF4D638 +sub 32784D4F004B405B +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: BCPG v1.68 + +mQENBFM1v9ABCADD0KoXq2ZKlUHeIVovQy3gFmW9oFAaraV48ouv8cYvqdf+s91H +NyqeyNPT/ihFeNqZJUAMyPdwN5xrWD6gxMrOCR7BFhA5kLmAKz4HfFCQ05ViyQdI +/HVNFvTdF8LNnuF+a5aNgg+jjLvFwzkyMFkuiPGuUDFnqEGxC+z9J8t40tpOTOIw +tPjSzkDN41AJDpUK/simKC5F0Im78nUbwMalE5z2IsZRWpYZyIhN1HhEdDvaDIh7 +3vENjH7enAjWh0iGRu+GTP/fayZnX0uhmausCCwMMhsr489e63ZOaJrqeC//wWrX +dtEJjcmvRmJ2hwLmgwMP4zSNKsnLGzP0sh69ABEBAAG5AQ0EUzW/0AEIAO62SMbq +gIzEFQEHlxNN5pZHd7msqDESILFYFkI1mxlkD0twFWMbk4nMH2VXhiuT3ulqKBOU +UUNQrO7egtfbGsgVv4bWbUHfeZkfvoWDlmbrvi1YLlR/ZxzGpCNc8e3aOcN3XKHl +BwfUaco6pcTBvLpKPA/TAjkTOtK4A5Azv1CSwpzSJf2bVVbrn7mi/rw78SmZAV2/ +rtiOU5a9S+3fzswLk1PO/z//d4VoCcL2s2WueVO1Q99kejkE55l3lYwEiBzVuAA9 +8Q0a5wg84vRGFZQWLIw8c20On2+dZcTLZc61ZjnsI1LSnLUUyQ/fzHQ+BynQF9l4 +3ZCPKFIl125MGikAEQEAAYkBHwQYAQIACQUCUzW/0AIbDAAKCRCxZpikrfTWOJel +B/9vDc3G/mrIHB1P/zl0Fdl5wQzaSVc9JB/ce0018ptplL711af0ZDvWqmZkJNkY +u0lHgnniaXB0l3GGUPIa/TZzVgFSRPj65FWTMkBbNnhGIwV5IVOPcSKSyDYn9/nQ +PpmWl5fDj8xiv6kKYxA4hjjRbRGuUXiI2dnyFHuFpacf/Af/Mc0U/CDDuz5a1jS4 +SYuYM/HFVL93POZzwLUR/+GDuVJFnm12rmjGnJVgR0rWcJKJt6vCsBPVfIamfl6v +G+N4TWQ9euj8Rd4hjsYPOwDfgVIUGIhpTUJ+fz046fkz9MWnX7RJh6hqLAXT7Hbc +0FTFsKYAbLJbm3Q+rU9jTdM8 +=mDDW +-----END PGP PUBLIC KEY BLOCK----- + pub B6D3AB9BCC641282 uid Eclipse Platform Project @@ -338,6 +461,32 @@ IprKXtD8103BdNqrPJev2azwqWwxFpN83tEPbK4SwWPgk1nSELXZZ5ClcDgqatg+ =uOQ4 -----END PGP PUBLIC KEY BLOCK----- +pub BAC30622339994C4 +sub FC9BDC25FB378008 +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: BCPG v1.68 + +mQENBFlMExYBCACmdTDSXPwSJeYbfYvHoDl5C7vx/0+LOTunDGJN38pNQHYQAZnv +Gyoc9ZmChrhLoim7z4ILqmNo8eegknepQ3dGdUij4NVIhR+m+8irayTbsNHvo3UG +9y7eM5tTSjyNYkyk5fAVuT7OhzIzMA+qtc3GRVxNYRKnaHajt+pOSqr+uoDtMG3n +6eAMHCAnhgh5Nd+dCFcNT+syl3zCwolA1wrzGxxOaif+xi5wwXjmF/lAt4PDIuDT +etA2/AqPM4zAC0BtC0iqVgVypjFV3EAexm/g0LNMiG/M/krzwjPq5gf1DY/57jU0 +02FpKd79HmR7bHdc4e2olEf9NlHxfbPXDDsHABEBAAG5AQ0EWUwTFgEIANmMpV3N +K8aLrLgQTyh5++det8C3D3T5tkEdljHOuN31/qdKNge8H6uKH8zXRZsj5pd8adpW +kD4TzIMvzIwzizsGw34O9hf1E2XPoDqvQr39p1sovX3PeDvRJY/7JFNt9DsphVc3 +xWQfNkC7JdMPa6JRiFHd3ynfbQ+wplf4tfaDVn1JXAWp0NSGgMtXfn5i19hHQWjm +RNAKNQLdVn8UczI8XdVM7bS4giDpQMukSyjsjgAo466iRK2+8f8BwIRe1JRvF37B +dnbvTg/dzoi1/E4ukwVJD6YE2LlDwzdGno9KxPlRsuY3nnheVgjbrGJ2XKRJkIk8 +7cMGh41VKw6L4usAEQEAAYkBHwQYAQIACQUCWUwTFgIbDAAKCRC6wwYiM5mUxEiH +CACQViGOHi0BoZ78ZJz6L48YNMx8fSdSv3YJ83Ih1n5DWCJgrDV5S3/edYinkoVI +0Lusy3MdftRg6OWaYOuOTf6MYcddO/mY363jiMByf9Uh3Dqq4sKqVLRnZbAqgD1o +dRoj2NkEQfgEH/H4JRVrxquzAKoWwJh3MhY+kajYJRJyWfc1/Bm3Bj1tcMGlGeIQ +fgWheeMg3kxrxJ9TXPqVi6VVPaPKIU5i8l46S+Wg3uvMs8vC3XzOIvhY6cwguJv9 +UkjZwGDSI952wLqnREMy0gFZ+OAB0qJpYM3nDEekWZP38G80kojnN61tZjRThu9I +i8/b+PwSW+nW3EpQZdLqZtOU +=2H2i +-----END PGP PUBLIC KEY BLOCK----- + pub BCF4173966770193 uid IntelliJ IDEA Sign Key @@ -367,6 +516,77 @@ vMxKy4GRZS18bXDI3vS6gRDNJDCqBYIhp13Os9k+ZpnwK3PPIHv4l1I0i0EHZKk= =WJEa -----END PGP PUBLIC KEY BLOCK----- +pub BF984B4145EA13F7 +uid Egor Andreevici + +sub 84761D363E7B0FC4 +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: BCPG v1.68 + +mQENBF7rgogBCADU9OwoEFdIgN5U0JU5pI7s3T1T1LeDMzAQ8l2Hq4jFrhnrjcEA +ieDSut1YIv5NTBoZv4CrklaKvvQNUXPvKrFImA4OKhBodKV3wsq2efCATDGa1JAw +VEJx6nJxxMsCLCJvmZsD+YE8/DIBI6jjnjh8jagZVkxkSRPvUIxlZCxytIyqXI0t +O8pLh8+8p5e0PgGb9OoszxEQZdBavsixdpe+0feU9cz0l0jJYx3W4ErZeCGGwNat +UUiW0ctb3iz7BkNhhoV9zepxkSLzCf5zBeyA+WfD34028pAfPpyAfDYXF4x55sVP +/3MdWGB6eU6KzPG2/QV/6or5E+C1yCMrnMy1ABEBAAG0I0Vnb3IgQW5kcmVldmlj +aSA8ZWdvckBzcXVhcmV1cC5jb20+uQENBF7rgogBCADBMYkuALuhT0pTMowmk/BH +8T9OXsu/a2EkmXf4sZqslcyZF2G3/0iQgXl/fjbVlNyxLi9C/Vl4AAPWLNfDWicw +TAuJIkCX8lyuimpO7FH2Vlr8dwHdyLd+V+LLyTbkfRqKoLDrwg9Uj4CuzH03ABfH +uI9lFKoEZ4zOUunUWugB9rN2wo/BhPmEh/QINqLDXCgYKLDYr8NOLWIfrI8fFuXo +SmjhDKwQzvDjGFXoiBDrwSI7AmaSYAJzgT4f+8yEO9rT2vTrt5wFgcHYjwYKr+RU +yFZh1jHwCSkUQluK4toTeraBxq4yTIKG7cFuer04m2/NhBi3WNwReNQ0E9jBIjNV +ABEBAAGJATwEGAEIACYWIQQdCoted8Z4p8ckRFq/mEtBReoT9wUCXuuCiAIbDAUJ +A8JnAAAKCRC/mEtBReoT91VqB/9VIYofkP4iSHXbwWrVnc6YDntN6aSvaQlmdinq +bpCC20fG6V93KlIk2xQ91vsm662NJODnvYV3Jt63tDdhgHxFn7UZJ8Vx9EzHslqA +ZZXX0jOQ465YzPPFwCJJP09sHAEb9TwufSCGRijqYsSLReT6bGCCOGM5a4qVPfLF +wU0pSS1Nf1GWk+1zTmfTtfz1cQA6VSHh+jFXVoeFuwFf3WGeY3d9TW1w07cgyQVG +KIqtobtdPkTaKqtvFpiUBg/GqiLijqXgQPBIqy3EaAxhC0qmE5UMgvJ5fD+T4JBG +eQh3BspmZac4oUdxAqzyU5N/Um+w90sQluFqwoVd5FX5i4L3 +=jVt7 +-----END PGP PUBLIC KEY BLOCK----- + +pub D364ABAA39A47320 +sub 3F606403DCA455C8 +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: BCPG v1.68 + +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 +=5tlI +-----END PGP PUBLIC KEY BLOCK----- + pub E2F38302C8075E3D uid Gradle Inc. @@ -493,6 +713,32 @@ bMW74dKTLoW6+aNn4F9nqCJ88A== =2g4Z -----END PGP PUBLIC KEY BLOCK----- +pub EE92349AD86DE446 +sub E68665C8F91BDE69 +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: BCPG v1.68 + +mQENBGO91akBCADDDpIrW/IohUSJNDu9VOUlnfEOm5VS49uqM0uucLi0BeAhy1Fo +P6Yg1cJkcK66DtnUoTM/JJLyDzJRlKnniLrYCkw8ScvtPdA5cQKJTY5ecn+9ouR2 +SC9GkBMgagbCScP1xE45q5FO+z4kwmcERIKOQ687VAk64QM6hJCupfAd6SqS/X0Q +SGttTNtmj7YBpfnU5iFX05Hj8Zkk7CX439xltO8uJNyBlDVbuUZc3/kRowKPVuuo +TK2mzllVPzE/YT6NUY04wQPmRJx0uWZQUyDBZeckdurpSImdd7sik6Wf6zVGvxvg +MC4oMufZ3EM8R4dssRSIUfnBaQ2o1LS+GVxjABEBAAG5AQ0EY73VqQEIANJPIYj9 +IsxKKOWLOkWvxAg9o9krIkohBMaOGRsx4RxQyArOCUoaG/qsG3aVOi8wML8hQK6q +oXADJ6FBGxQ67G8pperzRSj1O3BJILB6Fd1X8w40S6hSvUAZs+DM1FMuD4mf6ydu +yZUVIghGRExNeSb/vfn4KVPqdSAD7uWeQiIUYveaXrwot8+U8tRNgv+LQpCjhm5h +vWyIuxxpI+k5N07V9y0yRGWiBbgqdmfHVwdEbUSM0sMYUJUZKW+iwf5tZig9LZu3 +HAf/vyXjBWG6zkkjwO8onKFLuhL4jkygHGSawJHwYRgtlknUZ0DMVc451bbhuFHE +0dcgQCdAYJsI66MAEQEAAYkBPAQYAQgAJhYhBOsbPecXE8nsLofMJu6SNJrYbeRG +BQJjvdWpAhsMBQkDwmcAAAoJEO6SNJrYbeRGNC0H/1JBKZZ8+JLGcGefchsEWxcN +RN8yBtDtDM8pEsC99Pt+vzLaAYYFbPVKpzr57zIxZvtm8mUbWOa4Z8eHtzLRQEFi +rKuvd47YUPOyHtfdeccr0e7iQQ2rpRmOVrnkKu4LHI+f4jFEm+Pe+3CyLYe/tBKK +eBOKjRAWpQi7Jz1GQUuu9JFu4fUphzz0z5LybGHa1T7QZ+2ew8kqLl8EEeZAq4x/ +bulbaX050vfsgULn1X9AECW0CX/OafvFuSrEZsLUSw0KzmzqMPOLMXOh/EZsop17 +DqhGe5NO7GoCns3XxqjpggME9eCEQooeKHlLCAkX2/XttwVSRlrNsdVb82iKy7E= +=M4QQ +-----END PGP PUBLIC KEY BLOCK----- + pub F067A2FD751AE3E4 sub 28CFDE1EB61BB6AA -----BEGIN PGP PUBLIC KEY BLOCK----- @@ -574,6 +820,35 @@ kkKatq4qx+xU7QMdeMs8STRj =bz+O -----END PGP PUBLIC KEY BLOCK----- +pub F6D4A1D411E9D1AE +uid Christopher Povirk + +sub B5CB27F94F97173B +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: BCPG v1.68 + +mQENBE89LqsBCAC/C7QToaRF8eZgGOxcvp9aG+mFFCMjaRAb4Mh59OYdmUb6ZjfO +9388HPebGbPNR8SHYs0dBIuWY4ZJ7oUTYPswasL8vB0iPFdyHhvkCca+yk0b8ZBM +DmFlISm9HkYpoVjcFUp1oivyeJ5LRTJTd5JGEd/SWFRbB4TimdKXBzej9fIm2zVl +KInEMMd8HnSYE6nm3aNkbyiqhx81bFvl8x6X3ZMWcKs+TAVXdP9uLVvWowUwcApk +xpee442Ld1QfzMqdDnA6bGrp8LN8PZF9AXQ9Z6LTQL3p9PIq/6LPueQjpJWM+2j8 +BfhbW/F2kyHRwVNkjaa68A544shgxJcrxWzJABEBAAG0J0NocmlzdG9waGVyIFBv +dmlyayA8Y3Bvdmlya0Bnb29nbGUuY29tPrkBDQRPPS6rAQgAuYRnTE225fVwuw1T +POrQdXPAOLDkiq49bLfcxwRJe+RozKrJC1iKxb751jTozEEJLe5Xj7WcojqgDsuT +jzaLHDNvDCzRFvwfkJ4scMTAZd+2GYsC8N3Gg0JRgC2lU4wZxsanLnVMbdX2L0lZ +7WnH6S+GJ5f0Et8PM/g+V2Gj2UraBhGGak8OBQ6NhmCJBcyYg8Bh90cgD9V1hMRM +LSW7gB1vnpLM7C8Yymd3etdZSIltmDuVb3uG9s4Uwq51s2MEKsXsuFYCHTz0xT2u ++6e7Puaq5V0218QGR1Wupkl29iIUF57hFR7f6oYKkecvPKc4Yev6Ii0Mbvc1H19k +LOXUrwARAQABiQEfBBgBAgAJBQJPPS6rAhsMAAoJEPbUodQR6dGunSQH/A+4/Zbr +2jB46q1JEN/UV4U3MBQiNvCOSD9tOPMnBvVzJ53HutvGGkmafbtbwDZaN+YMs6fi +itBMqjF/eQ/pJ54aFguTPGMFrlFyjz2n/pffkHLpVHgs8V5M4ALITttwCOo8Vv7u +3VjO+ea5kiCm9MqJySrUP2Dv4lPVB32eoEUqWDxoyeACihW+Utdo8TBDVd+R8w36 +W3CUSvujW2z9jMNTF+VoVWDQWc3up7Nqb+ztW9wrjqs73nJCv9bLPahUPNzfh742 +v9vak3TkwMcDR1eZv+KvA8GXSZM6ACALzTmqRHXjGF3UZ4vowQDfiTzZKr87eBaE +FoHco7Lnn+W+8qk= +=9+x+ +-----END PGP PUBLIC KEY BLOCK----- + pub 02216ED811210DAA sub 8C40458A5F28CF7B -----BEGIN PGP PUBLIC KEY BLOCK----- @@ -875,6 +1150,52 @@ Qc4ZDKq+ywOElvONMnX4oaQ1 =f1ra -----END PGP PUBLIC KEY BLOCK----- +pub 0F9FE62F88E938D8 +uid Brad Corso + +sub BF6D15D3F1BF7BCF +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: BCPG v1.68 + +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 +=Cl60 +-----END PGP PUBLIC KEY BLOCK----- + pub 12DC99973C2318FD uid Andrey Shcheglov @@ -962,6 +1283,32 @@ J5a3V/Xk1KXWk7Q1nwD9GHF1twlwa87r2hXVwC+qFc/kRgJNR5uczkU78kbcCwQ= =PWEc -----END PGP PUBLIC KEY BLOCK----- +pub 29579F18FA8FD93B +sub 9DF7F2349731D55B +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: BCPG v1.68 + +mQENBFYFiMABCADYpblWssqGxbjTwsyroPh48BwdSKl59zbFKoEHDw87NeWq7fik +h95RkbdeWsQSvduXWgQZsUDq9cLOkuS/ChAMkAAd3MPp1NMdFmAqS7BX5wU5s5I7 +XD+/p51SWLMvgrLxoenmoE04EuQqQiXd4DbU+HGPseiNx+mN0cxPssaZMBBsmi2r +RjwcQrFTaC1iffzh8FKLQvoTDzci//b5bWcxCLbsY9dYcUaDCbBAkL8HzyZUKNE9 +XwXh/Rq8wDakI/VEg/905a9c4xq6Rss6Yn5E4V2SAo2+B3hYmvHFsefaM9kkqvXk +MQ6zjx83LAtzavOzmthjhhPIgCAfoQ5Q5oDzABEBAAG5AQ0EVgWIwAEIAJ29KWGH +aEt7gXV8EweJkrYd02nwjc1LyjUT2TRwEzZ9N9qUiVqfpkgnZn4mpHCToxFoqkHa +iv/QDfj7cp8jbZJa2wjaUkDbH2pZqLBGJ0sUUBZ1KNPM2uhhWRzAnmF/bIo3+Yfl +hGINLNqoevkYoo9cdelP3hepef4+PUuPmKmeo856uknmaWQ89LPwLlV7oj6wiqMY +p22sHqTGAgXeR/fSLMK7d0vSPm+57LZed5ECoRMeqYFUwSMV64RjTMkKPsvFBGvR +hppJ+uWQiMjFFuFq2DFeNBVtueHSdgCHx1TP9i+x+7JmYsmFFmRwnEdbxO3THFXa +gFQGr4ima+oOjLcAEQEAAYkBHwQYAQIACQUCVgWIwAIbDAAKCRApV58Y+o/ZO+ZZ +CACL1DlaVyRNjNxzC+30X6xGykPwCdwMRF3CRjoeIicss2pBJRaIdTYFpg3bCZKJ +J5KDC6s+03zmd3ddnKEq1fEfRcoLZ9PNBYF3IESHnNPlR68RL2cjMgq6segbhOxa +v13ZcOIOnyrWzgbVw0ZgN8P3vCllFtifwvuF50vTshIRY11G8Gluu+GZ7tfSkPww +Eo+pRd8scdol62aUUo6a71rDOMg2XPULz0l2hxKWfeUsksT5EY03seZd3CYqOacL +R+jaHyOc5Nh6R1MzcRz65YTwzVbKplXtZjOghMh+rS4eDIjEKlo456M4spKFBbTf +Ub+QS9kCkBU8csUzwF0nk/oP +=SZ35 +-----END PGP PUBLIC KEY BLOCK----- + pub 2C7B12F2A511E325 sub 10DA72CD7FBFA159 -----BEGIN PGP PUBLIC KEY BLOCK----- @@ -1292,6 +1639,34 @@ bT4= =MKAK -----END PGP PUBLIC KEY BLOCK----- +pub 59A252FB1199D873 +uid Tagir Valeev + +sub 92BD2D0B5B21ABA2 +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: BCPG v1.68 + +mQENBFUBG7QBCADRWXf0Fw05qRhM4cRnGKlOW1ecue1DCxHAtFwoqmAXyTCO+tI0 +MEW5SyXUkX6FsWLl6A2y+KgOs669ogzfQ0rnZMEt4HisRp8wpgk3GWR1/9aKYz/c +ymy2N3BP9cz2fJ9+3PpBccUPL+ydFKpcnEnIwiQK+p9JjEWzJBlrdUc/UEJ0R+n/ +5r/+0+BHiTEMvjAF6/SwyntpTWpu7iEzLv/pfdCuhFKa4yn+9Ciwe3wGtSiue+dh +tqKcd4YxED3oAswObBca3CC2HWWsUEH6EmfT1jUdfy1cq4X5x7AZ26oFYfG+odqW +W5dcB+13VkJtJRzQTO/2HKtITJYC65a1jKt3ABEBAAG0GlRhZ2lyIFZhbGVldiA8 +bGFueUBuZ3MucnU+uQENBFUBG7QBCADbCC7lPXB/xCBC/jqcCGnK/8t/+ixvqJPE +igxyJRenEqbrErFjOi/kRnGYLwg0dEtBBIneOMsvMBTL6GEpbFxyzeEqh/66SyHO +Ag/A3Qi1q2imkWa4baszVkrGMRIKqO59cTuvnLFNe1SQK56ZBjx6AO6KGZWhq3NM +v65ZE1x/viyqofJ4jvQ2qeOqSxa3YL7sim6tQen2gH9iTEcr6stvn7sH1Rk3OwxF +FBbcBoOxZ4gxdM5ft6xRtbnfZB/FFs/hsAsBU+qoVYJYDprSYMNQkmDXg7ELwweG +EyTZzJ3jEnTOgpBHEYS6dvpc/dPsEdCv2vUARNTT7mwGkQdrkEeFABEBAAGJAR8E +GAECAAkFAlUBG7QCGwwACgkQWaJS+xGZ2HNriQgAxxwfwZnOPGHtcZek+p2zRIjA +nZqSG2viTRZxFnLnquMZNMaF11EqQZ4y2lj0K1WSh13TMZpkdwY4bRb7C4Hmo8qS +1JFQ5SjJHRkLbFly9Gm6+HDaDA4l1EcZW14MWfPoSLP6yklirbq6wg9leDFy7EFe +MQK4dXs5CRRAwN7URs444M4OTMJq5i+x+T3Her1dSnutAZrxWL740cE+FMNTg9F5 +brjzmmok4m4TxAnOcy8Qc/fnkUrEW8XHDRMz2CUvF5ffoSMO2OzndfOHDqHscXaC +PyudpB+wOcnxI9pFwmZubWMpcir4BqXM1nWbqFd7tcYPre/0JYIUzKCQANB+Rg== +=NhW5 +-----END PGP PUBLIC KEY BLOCK----- + pub 5B05CCDE140C2876 sub 9D29AE4A6B50E01F -----BEGIN PGP PUBLIC KEY BLOCK----- @@ -1357,6 +1732,51 @@ hsWaeXOM0n2j759uNb/Nd2XA =2J9s -----END PGP PUBLIC KEY BLOCK----- +pub 5F7786DF73E61F56 +uid Ting-Yuan Huang + +sub 73F7734B17EC71F4 +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: BCPG v1.68 + +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 +=w18e +-----END PGP PUBLIC KEY BLOCK----- + pub 66B50994442D2D40 uid Square Clippy @@ -1428,6 +1848,41 @@ lV/Shw== =qKIA -----END PGP PUBLIC KEY BLOCK----- +pub 7457CA33C3CE9E15 +uid Colin Decker + +sub ABE9F3126BB741C1 +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: BCPG v1.68 + +mQENBFIXyRQBCADe285y3Pu7KzoKyP6wqeNXtvvuwMatAmPm5x/i+S8MlryqzsYa +x6twUmXV1yKjjtGrO+9fHvTOWBfSSP+fP9KTaTQYSasoJq2Mw4cQDy1i0zrxNZUw +N4/BiyjQA25sdfaOolhO0sFlZuTZpYy5wG72KkA1ygNq0L+8aBKhEF6zDU61YzCC +AxjcgTftgTeeoqkJtYa06lNz3jmJDN+zUQignfRa3ymoGtFHTzoXR9maE8RWDty4 +y+DY+8ibdGgSgKPZ0byTCDyNojgU1YTlADa/1/NY1ShYg617O1xicLNo0JEJlf2U +Tu4Ymql36+xSkYSISU97Q6Utgq27XMuZvDUDABEBAAG0IkNvbGluIERlY2tlciA8 +Y2dkZWNrZXJAZ29vZ2xlLmNvbT65AQ0EUhfJFAEIAN9NHRd2bYP/3CDi+n1ilSCh +ld0NR3DUBgS/AdqQ7IoAUfj7skyI/WyaMdV4uy6vRh5YgNg2g01nd0LLZR8Gf2Ck ++D6F88CdZaTxlkcxHV/dXMZ8yBO+0D6yFRZEL7Imsv8Ig4QXOVwfuiXEPk/Ef5Dy +9SdAVhcoErTGGR6BOGVVvexGtBwefsjMaOG0khkRbWIQ32WxfUFuAv5XBQ0ckLrl +KvYWUYhOlXg27GtFKH2EBBF0Z5ZWu7gaBFwSV0oLp9EWcD+C+WEwUSfBdqfRJtyX +vgf4kZdwdQ5caM8P2/Sdncl2l/LU1At2Smc+plr6zhIhDlLhlrzKGa16oARSBdUA +EQEAAYkCPgQYAQoACQUCUhfJFAIbLgEpCRB0V8ozw86eFcBdIAQZAQoABgUCUhfJ +FAAKCRCr6fMSa7dBwURMCADHrqwRNHkbG1QsXJr9oUK6KVkLsPhcngIhxRLlqe89 +omg9G7eGNauzs2PKsB3txotCFc7ROVNv/TAuSDYzkPos8G46p3bGesjfJb24zc6G +MT4RGIJoh1oNG1IciafIIHjp2ZJHRmEDwmvZG24OHJ+mlHLjaedtqlWu+zwwhH2V +ZrI/U3gW/x4imbk9UyyzciEIxrAc+fc19xl5PkUVcSDVC0cAqGpeZz8+SxFaf3Rr +0aGnSbeuHRjNupmoxkQOAey1ztmdWiCPf5RFfmFD+fENh+/xqYiGorYpcIN7UAsM +kvD5UHc5ZG2tTD41jM99w9Lm/xHJ9ks8gNwZESwIzr6ABKIH/1ulsflI216qPz5o +7uUxlTm8NfTyATfCUuZEDMYGOjDQPqQa8hFebqjWWYBUq2SlaKD2xMeEuEXV+M5k +88Cx6T2nvaZWMsrD7uGj+tTsFaKBGxP5p2OSEWOTETKKv6Cx7vcMTQmrqSFo47bF +KlNSs+aVM48UnQeFtTDyOhwa5jvtqtst4eQHwHWQ99BK0TEymNx0vF0nPjWA76CR +rfopOwXKdxJgoKq4MrxE92ot5I82AZBPeiWVJ+6wECeK/GoBIXZ5jEUqrQmmzIbo +WA5G5PMJ8egzLJNRJjTWHjCWrUTnwNcqaD4/qZxIlW4Lt0uvGlx6pKOJQ05u+9X/ +BzoVWrw= +=fJQM +-----END PGP PUBLIC KEY BLOCK----- + pub 7C30F7B1329DBA87 uid Ktor Release diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index fb96fb6..c52264f 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -19,6 +19,8 @@ + + @@ -26,6 +28,7 @@ + @@ -34,9 +37,13 @@ + + + + @@ -52,6 +59,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -82,6 +119,11 @@ + + + + + @@ -122,6 +164,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -152,6 +214,11 @@ + + + + + diff --git a/initializer/generator/build.gradle.kts b/initializer/generator/build.gradle.kts new file mode 100644 index 0000000..37b4dec --- /dev/null +++ b/initializer/generator/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +plugins { + id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library") + id("ru.pixnews.anvil.codegen.build-logic.project.test") + id("ru.pixnews.anvil.codegen.build-logic.project.publish") + kotlin("kapt") +} + +group = "ru.pixnews.anvil.codegen.initializer.generator" +version = "0.1-SNAPSHOT" + +dependencies { + api(libs.anvil.compiler.api) + implementation(libs.anvil.compiler.utils) + implementation(libs.kotlinpoet) { exclude(module = "kotlin-reflect") } + implementation(projects.common) + + compileOnly(libs.auto.service.annotations) + kapt(libs.auto.service.compiler) + + testImplementation(libs.anvil.annotations.optional) + testImplementation(libs.assertk) + testImplementation(libs.dagger) + testImplementation(projects.testUtils) + testImplementation(testFixtures(libs.anvil.compiler.utils)) +} diff --git a/initializer/generator/src/main/kotlin/ru/pixnews/anvil/codegen/initializer/generator/ContributesInitializerCodeGenerator.kt b/initializer/generator/src/main/kotlin/ru/pixnews/anvil/codegen/initializer/generator/ContributesInitializerCodeGenerator.kt new file mode 100644 index 0000000..32722e9 --- /dev/null +++ b/initializer/generator/src/main/kotlin/ru/pixnews/anvil/codegen/initializer/generator/ContributesInitializerCodeGenerator.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.initializer.generator + +import com.google.auto.service.AutoService +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.reference.ClassReference +import com.squareup.anvil.compiler.internal.reference.MemberFunctionReference +import com.squareup.anvil.compiler.internal.reference.allSuperTypeClassReferences +import com.squareup.anvil.compiler.internal.reference.asClassName +import com.squareup.anvil.compiler.internal.reference.asTypeName +import com.squareup.anvil.compiler.internal.reference.classAndInnerClassReferences +import com.squareup.anvil.compiler.internal.reference.generateClassName +import com.squareup.anvil.compiler.internal.safePackageString +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeSpec +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.psi.KtFile +import ru.pixnews.anvil.codegen.common.classname.DaggerClassName +import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName +import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName.asyncInitializer +import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName.initializer +import ru.pixnews.anvil.codegen.common.fqname.FqNames +import ru.pixnews.anvil.codegen.common.util.contributesToAnnotation +import ru.pixnews.anvil.codegen.common.util.parseConstructorParameters +import java.io.File + +@AutoService(CodeGenerator::class) +public class ContributesInitializerCodeGenerator : CodeGenerator { + override fun isApplicable(context: AnvilContext): Boolean = true + + override fun generateCode( + codeGenDir: File, + module: ModuleDescriptor, + projectFiles: Collection, + ): Collection { + return projectFiles + .classAndInnerClassReferences(module) + .filter { it.isAnnotatedWith(FqNames.contributesInitializer) } + .map { generateInitializerModule(it, codeGenDir) } + .toList() + } + + private fun generateInitializerModule( + annotatedClass: ClassReference, + codeGenDir: File, + ): GeneratedFile { + val boundType = checkNotNull(annotatedClass.getInitializerBoundType()) { + "${annotatedClass.fqName} doesn't extend any of $initializer or $asyncInitializer" + } + + val moduleClassId = annotatedClass.moduleNameForInitializer() + val generatedPackage = moduleClassId.packageFqName.safePackageString() + val moduleClassName = moduleClassId.relativeClassName.asString() + + val replaces: List = annotatedClass.annotations.first { it.fqName == FqNames.contributesInitializer } + .replaces(parameterIndex = 0) + .map { replacedClassRef -> + if (replacedClassRef.isInitializer()) { + replacedClassRef.moduleNameForInitializer().asClassName() + } else { + replacedClassRef.asTypeName() + } + } + + val moduleSpecBuilder = TypeSpec.objectBuilder(moduleClassName) + .addAnnotation(DaggerClassName.module) + .addAnnotation( + contributesToAnnotation( + className = PixnewsClassName.appInitializersScope, + replaces = replaces, + ), + ) + .addFunction(generateProvideMethod(annotatedClass, boundType)) + + val content = FileSpec.buildFile(generatedPackage, moduleClassName) { + addType(moduleSpecBuilder.build()) + } + + return createGeneratedFile(codeGenDir, generatedPackage, moduleClassName, content) + } + + private fun generateProvideMethod( + annotatedClass: ClassReference, + boundType: ClassName, + ): FunSpec { + val builder = FunSpec.builder("provide${annotatedClass.shortName}") + .addAnnotation(DaggerClassName.provides) + .addAnnotation(DaggerClassName.intoSet) + .addAnnotation(DaggerClassName.reusable) + .returns(boundType) + + val primaryConstructor: MemberFunctionReference = annotatedClass.constructors.firstOrNull() + ?: throw IllegalArgumentException("No primary constructor on $annotatedClass") + val constructorParameters = primaryConstructor.parameters.parseConstructorParameters(annotatedClass) + + constructorParameters.forEach { + builder.addParameter(it.name, it.resolvedType) + } + + val initializerParams = constructorParameters.joinToString(", ") { "${it.name} = ${it.name}" } + builder.addStatement("return %T(\n$initializerParams\n)", annotatedClass.asClassName()) + return builder.build() + } + + private fun ClassReference.getInitializerBoundType(): ClassName? { + return allSuperTypeClassReferences() + .map(ClassReference::asClassName) + .firstOrNull { it == initializer || it == asyncInitializer } + } + + private fun ClassReference.isInitializer() = getInitializerBoundType() != null + + private fun ClassReference.moduleNameForInitializer(): ClassId = generateClassName(suffix = "_InitializerModule") +} diff --git a/initializer/generator/src/test/kotlin/ru/pixnews/anvil/codegen/initializer/generator/ContributesInitializerCodeGeneratorTest.kt b/initializer/generator/src/test/kotlin/ru/pixnews/anvil/codegen/initializer/generator/ContributesInitializerCodeGeneratorTest.kt new file mode 100644 index 0000000..d956010 --- /dev/null +++ b/initializer/generator/src/test/kotlin/ru/pixnews/anvil/codegen/initializer/generator/ContributesInitializerCodeGeneratorTest.kt @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.initializer.generator + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.containsExactlyInAnyOrder +import assertk.assertions.isEqualTo +import com.squareup.anvil.annotations.ContributesTo +import com.squareup.anvil.compiler.internal.testing.compileAnvil +import com.tschuchort.compiletesting.JvmCompilationResult +import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK +import dagger.Module +import dagger.Provides +import dagger.Reusable +import dagger.multibindings.IntoSet +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle +import org.junit.jupiter.api.fail +import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName +import ru.pixnews.anvil.codegen.testutils.haveAnnotation +import ru.pixnews.anvil.codegen.testutils.loadClass +import javax.inject.Provider + +@TestInstance(Lifecycle.PER_CLASS) +class ContributesInitializerCodeGeneratorTest { + private val initializerInterfacesCode = """ + package ru.pixnews.foundation.initializers + + public fun interface Initializer { public fun init() } + public fun interface AsyncInitializer { public fun init() } + """.trimIndent() + private val initializersInjectCode = """ + package ru.pixnews.foundation.initializers.inject + import kotlin.reflect.KClass + public abstract class AppInitializersScope private constructor() + public annotation class ContributesInitializer( + val replaces: Array> = [], + ) + """.trimIndent() + private val loggerStubCode = """ + package co.touchlab.kermit + open class Logger + """.trimIndent() + private val firebaseStubCode = """ + package com.google.firebase; + open class FirebaseApp + """.trimIndent() + private val javaxInjectProviderCode = """ + package javax.inject; + public interface Provider { + fun get(): T + } + """.trimIndent() + private val firebaseInitializerCode = """ + package ru.pixnews.initializers + import co.touchlab.kermit.Logger + import com.google.firebase.FirebaseApp + import ru.pixnews.foundation.initializers.AsyncInitializer + import ru.pixnews.foundation.initializers.inject.ContributesInitializer + import javax.inject.Provider + + @ContributesInitializer + public class FirebaseInitializer( + private val logger: Logger, + private val firebaseApp: Provider<@JvmSuppressWildcards FirebaseApp>, + ) : AsyncInitializer { + override fun init() { + } + } + """.trimIndent() + private val testFirebaseInitializerCode = """ + package ru.pixnews.test.initializers + import co.touchlab.kermit.Logger + import com.google.firebase.FirebaseApp + import ru.pixnews.foundation.initializers.AsyncInitializer + import ru.pixnews.foundation.initializers.inject.ContributesInitializer + import ru.pixnews.initializers.FirebaseInitializer + import javax.inject.Provider + + @ContributesInitializer(replaces = [FirebaseInitializer::class]) + public class TestFirebaseInitializer( + private val logger: Logger, + ) : AsyncInitializer { + override fun init() { + } + } + """.trimIndent() + private lateinit var compilationResult: JvmCompilationResult + + @Nested + @TestInstance(Lifecycle.PER_CLASS) + inner class TestInitializer { + private val generatedModuleName = "ru.pixnews.initializers.FirebaseInitializer_InitializerModule" + + @BeforeAll + fun setup() { + compilationResult = compileAnvil( + initializersInjectCode, + firebaseStubCode, + initializerInterfacesCode, + javaxInjectProviderCode, + loggerStubCode, + firebaseInitializerCode, + ) + } + + @Test + fun `Dagger module should be generated`() { + assertThat(compilationResult.exitCode).isEqualTo(OK) + } + + @Test + fun `Generated module should have correct annotations`() { + val clazz = compilationResult.classLoader.loadClass(generatedModuleName) + val appInitializerScopeClass = + compilationResult.classLoader.loadClass(PixnewsClassName.appInitializersScope) + + assertThat(clazz).haveAnnotation(ContributesTo::class.java) + assertThat(clazz).haveAnnotation(Module::class.java) + + assertThat( + clazz.getAnnotation(ContributesTo::class.java).scope.java, + ).isEqualTo(appInitializerScopeClass) + } + + @Test + fun `Generated module should have correct provide method`() { + val moduleClass = compilationResult.classLoader.loadClass(generatedModuleName) + val asyncInitializerClass = compilationResult.classLoader.loadClass(PixnewsClassName.asyncInitializer) + val loggerClass = compilationResult.classLoader.loadClass("co.touchlab.kermit.Logger") + + val provideMethod = moduleClass.declaredMethods.firstOrNull { + it.name == "provideFirebaseInitializer" + } ?: fail("no provideFirebaseInitializer method") + + assertThat(provideMethod.returnType).isEqualTo(asyncInitializerClass) + assertThat( + provideMethod.annotations.map(Annotation::annotationClass), + ).containsExactlyInAnyOrder( + Provides::class, + IntoSet::class, + Reusable::class, + ) + assertThat(provideMethod.parameterTypes).containsExactly( + loggerClass, + Provider::class.java, + ) + } + } + + @Nested + @TestInstance(Lifecycle.PER_CLASS) + inner class TestReplaces { + private val generatedModuleName = "ru.pixnews.test.initializers.TestFirebaseInitializer_InitializerModule" + + @BeforeAll + fun setup() { + compilationResult = compileAnvil( + initializersInjectCode, + firebaseStubCode, + initializerInterfacesCode, + javaxInjectProviderCode, + loggerStubCode, + firebaseInitializerCode, + testFirebaseInitializerCode, + ) + } + + @Test + fun `Generated module should have correct annotations`() { + val clazz = compilationResult.classLoader.loadClass(generatedModuleName) + val appInitializerScopeClass = + compilationResult.classLoader.loadClass(PixnewsClassName.appInitializersScope) + val replacesScopeClass = compilationResult.classLoader.loadClass( + "ru.pixnews.initializers.FirebaseInitializer_InitializerModule", + ) + + assertThat(clazz).haveAnnotation(ContributesTo::class.java) + assertThat(clazz).haveAnnotation(Module::class.java) + + assertThat( + clazz.getAnnotation(ContributesTo::class.java).scope.java, + ).isEqualTo(appInitializerScopeClass) + assertThat( + clazz.getAnnotation(ContributesTo::class.java).replaces.map { it.java }, + ).containsExactly(replacesScopeClass) + } + } +} diff --git a/initializer/inject/build.gradle.kts b/initializer/inject/build.gradle.kts new file mode 100644 index 0000000..1573f3b --- /dev/null +++ b/initializer/inject/build.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +plugins { + id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library") + id("ru.pixnews.anvil.codegen.build-logic.project.test") + id("ru.pixnews.anvil.codegen.build-logic.project.publish") +} + +group = "ru.pixnews.anvil.codegen.initializer.inject" +version = "0.1-SNAPSHOT" + +dependencies { +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 409b52a..683c1f3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,4 +12,17 @@ plugins { } rootProject.name = "pixnews-anvil-codegen" -include("lib") +include("common") +include("test-utils") + +listOf( + "activity", + "experiment", + "initializer", + "test", + "viewmodel", + "workmanager", +).forEach { + include("$it:generator") + include("$it:inject") +} diff --git a/test-utils/build.gradle.kts b/test-utils/build.gradle.kts new file mode 100644 index 0000000..50dfa7b --- /dev/null +++ b/test-utils/build.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +plugins { + id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library") + id("ru.pixnews.anvil.codegen.build-logic.project.test") +} + +dependencies { + api(libs.anvil.compiler.api) + api(libs.anvil.compiler.utils) + api(libs.assertk) + api(libs.kotlinpoet) { exclude(module = "kotlin-reflect") } +} diff --git a/test-utils/src/main/kotlin/ru/pixnews/anvil/codegen/testutils/ClassAsserts.kt b/test-utils/src/main/kotlin/ru/pixnews/anvil/codegen/testutils/ClassAsserts.kt new file mode 100644 index 0000000..e0977ef --- /dev/null +++ b/test-utils/src/main/kotlin/ru/pixnews/anvil/codegen/testutils/ClassAsserts.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.testutils + +import assertk.Assert +import assertk.assertions.isNotNull + +public inline fun Assert>.haveAnnotation( + annotationClass: Class, +) { + transform { clazz -> clazz.getAnnotation(annotationClass) }.isNotNull() +} diff --git a/test-utils/src/main/kotlin/ru/pixnews/anvil/codegen/testutils/TestingUtils.kt b/test-utils/src/main/kotlin/ru/pixnews/anvil/codegen/testutils/TestingUtils.kt new file mode 100644 index 0000000..61180e3 --- /dev/null +++ b/test-utils/src/main/kotlin/ru/pixnews/anvil/codegen/testutils/TestingUtils.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.testutils + +import com.squareup.kotlinpoet.ClassName + +public fun ClassLoader.loadClass(clazz: ClassName): Class<*> = this.loadClass(clazz.canonicalName) + +@Suppress("UNCHECKED_CAST") +public fun Annotation.getElementValue(elementName: String): T = + this::class.java.declaredMethods.single { it.name == elementName }.invoke(this) as T diff --git a/test/generator/build.gradle.kts b/test/generator/build.gradle.kts new file mode 100644 index 0000000..7495117 --- /dev/null +++ b/test/generator/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +plugins { + id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library") + id("ru.pixnews.anvil.codegen.build-logic.project.test") + id("ru.pixnews.anvil.codegen.build-logic.project.publish") + kotlin("kapt") +} + +group = "ru.pixnews.anvil.codegen.test.generator" +version = "0.1-SNAPSHOT" + +dependencies { + api(libs.anvil.compiler.api) + implementation(libs.anvil.compiler.utils) + implementation(libs.kotlinpoet) { exclude(module = "kotlin-reflect") } + implementation(projects.common) + + compileOnly(libs.auto.service.annotations) + kapt(libs.auto.service.compiler) + + testImplementation(libs.anvil.annotations.optional) + testImplementation(libs.assertk) + testImplementation(libs.dagger) + testImplementation(projects.testUtils) + testImplementation(testFixtures(libs.anvil.compiler.utils)) +} diff --git a/test/generator/src/main/kotlin/ru/pixnews/anvil/codegen/test/generator/ContributesTestCodeGenerator.kt b/test/generator/src/main/kotlin/ru/pixnews/anvil/codegen/test/generator/ContributesTestCodeGenerator.kt new file mode 100644 index 0000000..273d432 --- /dev/null +++ b/test/generator/src/main/kotlin/ru/pixnews/anvil/codegen/test/generator/ContributesTestCodeGenerator.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.test.generator + +import com.google.auto.service.AutoService +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.buildFile +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.anvil.compiler.internal.reference.generateClassName +import com.squareup.anvil.compiler.internal.safePackageString +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.TypeSpec +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.psi.KtFile +import ru.pixnews.anvil.codegen.common.classname.DaggerClassName +import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName +import ru.pixnews.anvil.codegen.common.fqname.FqNames +import ru.pixnews.anvil.codegen.common.util.contributesToAnnotation +import java.io.File + +@AutoService(CodeGenerator::class) +public class ContributesTestCodeGenerator : CodeGenerator { + override fun isApplicable(context: AnvilContext): Boolean = true + + override fun generateCode( + codeGenDir: File, + module: ModuleDescriptor, + projectFiles: Collection, + ): Collection { + return projectFiles + .classAndInnerClassReferences(module) + .filter { it.isAnnotatedWith(FqNames.contributesTest) } + .map { generateTestModule(it, codeGenDir) } + .toList() + } + + private fun generateTestModule( + annotatedClass: ClassReference, + codeGenDir: File, + ): GeneratedFile { + val moduleClassId = annotatedClass.generateClassName(suffix = "_TestModule") + val generatedPackage = moduleClassId.packageFqName.safePackageString() + val moduleClassName = moduleClassId.relativeClassName.asString() + + val moduleSpecBuilder = TypeSpec.objectBuilder(moduleClassName) + .addAnnotation(DaggerClassName.module) + .addAnnotation(contributesToAnnotation(PixnewsClassName.appScope)) + .addFunction(generateProvideMethod(annotatedClass)) + + val content = FileSpec.buildFile(generatedPackage, moduleClassName) { + addType(moduleSpecBuilder.build()) + } + return createGeneratedFile(codeGenDir, generatedPackage, moduleClassName, content) + } + + private fun generateProvideMethod( + annotatedClass: ClassReference, + ): FunSpec { + val testClass = annotatedClass.asClassName() + return FunSpec.builder("provide${annotatedClass.shortName}Injector") + .addAnnotation(DaggerClassName.provides) + .addAnnotation(DaggerClassName.intoMap) + .addAnnotation( + AnnotationSpec + .builder(DaggerClassName.classKey) + .addMember("%T::class", testClass) + .build(), + ) + .addAnnotation( + AnnotationSpec + .builder(PixnewsClassName.singleIn) + .addMember("%T::class", PixnewsClassName.appScope) + .build(), + ) + .addParameter("injector", DaggerClassName.membersInjector.parameterizedBy(testClass)) + .returns(PixnewsClassName.singleInstrumentedTestInjector) + .addStatement("return %T(injector)", PixnewsClassName.singleInstrumentedTestInjector) + .build() + } +} diff --git a/test/generator/src/test/kotlin/ru/pixnews/anvil/codegen/test/generator/ContributesTestCodeGeneratorTest.kt b/test/generator/src/test/kotlin/ru/pixnews/anvil/codegen/test/generator/ContributesTestCodeGeneratorTest.kt new file mode 100644 index 0000000..f13eabd --- /dev/null +++ b/test/generator/src/test/kotlin/ru/pixnews/anvil/codegen/test/generator/ContributesTestCodeGeneratorTest.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.test.generator + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.containsExactlyInAnyOrder +import assertk.assertions.isEqualTo +import com.squareup.anvil.annotations.ContributesTo +import com.squareup.anvil.annotations.optional.SingleIn +import com.squareup.anvil.compiler.internal.testing.compileAnvil +import com.tschuchort.compiletesting.JvmCompilationResult +import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK +import dagger.MembersInjector +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle +import org.junit.jupiter.api.fail +import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName +import ru.pixnews.anvil.codegen.testutils.haveAnnotation +import ru.pixnews.anvil.codegen.testutils.loadClass + +@OptIn(ExperimentalCompilerApi::class) +@TestInstance(Lifecycle.PER_CLASS) +class ContributesTestCodeGeneratorTest { + private val generatedModuleName = "com.test.MainTest_TestModule" + private lateinit var compilationResult: JvmCompilationResult + + @BeforeAll + @Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") + fun setup() { + val testStubs = """ + package ru.pixnews.foundation.instrumented.test.di + import dagger.MembersInjector + + annotation class ContributesTest + + @Suppress("UNUSED_PARAMETER") + class SingleInstrumentedTestInjector(injector: MembersInjector<*>) + """.trimIndent() + + val appScopeStub = """ + package ru.pixnews.foundation.di.base.scope + public abstract class AppScope private constructor() + """.trimIndent() + + val testClass = """ + package com.test + import ru.pixnews.foundation.instrumented.test.di.ContributesTest + + @ContributesTest + class MainTest + """.trimIndent() + compilationResult = compileAnvil( + sources = arrayOf( + testStubs, + appScopeStub, + testClass, + ), + ) + } + + @Test + fun `Dagger module should be generated`() { + assertThat(compilationResult.exitCode).isEqualTo(OK) + } + + @Test + fun `Generated module should have correct annotations`() { + val clazz = compilationResult.classLoader.loadClass(generatedModuleName) + val appScopeClass = compilationResult.classLoader.loadClass(PixnewsClassName.appScope) + assertThat(clazz).haveAnnotation(ContributesTo::class.java) + + assertThat( + clazz.getAnnotation(ContributesTo::class.java).scope.java, + ).isEqualTo(appScopeClass) + } + + @Test + fun `Generated module should have correct provide method`() { + val moduleClass = compilationResult.classLoader.loadClass(generatedModuleName) + val singleInstrumentedTestInjectorClass = compilationResult.classLoader.loadClass( + PixnewsClassName.singleInstrumentedTestInjector, + ) + + val provideMethod = moduleClass.declaredMethods.firstOrNull { + it.name == "provideMainTestInjector" + } ?: fail("no provideMainTestInjector method") + assertThat(provideMethod.returnType).isEqualTo(singleInstrumentedTestInjectorClass) + assertThat(provideMethod.parameterTypes).containsExactly(MembersInjector::class.java) + assertThat(provideMethod.annotations.map(Annotation::annotationClass)).containsExactlyInAnyOrder( + Provides::class, + IntoMap::class, + SingleIn::class, + ClassKey::class, + ) + } +} diff --git a/test/inject/build.gradle.kts b/test/inject/build.gradle.kts new file mode 100644 index 0000000..551fc80 --- /dev/null +++ b/test/inject/build.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +plugins { + id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library") + id("ru.pixnews.anvil.codegen.build-logic.project.test") + id("ru.pixnews.anvil.codegen.build-logic.project.publish") +} + +group = "ru.pixnews.anvil.codegen.test.inject" +version = "0.1-SNAPSHOT" + +dependencies { +} diff --git a/viewmodel/generator/build.gradle.kts b/viewmodel/generator/build.gradle.kts new file mode 100644 index 0000000..2b76d21 --- /dev/null +++ b/viewmodel/generator/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +plugins { + id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library") + id("ru.pixnews.anvil.codegen.build-logic.project.test") + id("ru.pixnews.anvil.codegen.build-logic.project.publish") + kotlin("kapt") +} + +group = "ru.pixnews.anvil.codegen.viewmodel.generator" +version = "0.1-SNAPSHOT" + +dependencies { + api(libs.anvil.compiler.api) + implementation(libs.anvil.compiler.utils) + implementation(libs.kotlinpoet) { exclude(module = "kotlin-reflect") } + implementation(projects.common) + + compileOnly(libs.auto.service.annotations) + kapt(libs.auto.service.compiler) + + testImplementation(libs.anvil.annotations.optional) + testImplementation(libs.assertk) + testImplementation(libs.dagger) + testImplementation(projects.testUtils) + testImplementation(testFixtures(libs.anvil.compiler.utils)) +} diff --git a/viewmodel/generator/src/main/kotlin/ru/pixnews/anvil/codegen/viewmodel/generator/ContributesViewModelCodeGenerator.kt b/viewmodel/generator/src/main/kotlin/ru/pixnews/anvil/codegen/viewmodel/generator/ContributesViewModelCodeGenerator.kt new file mode 100644 index 0000000..46b4dab --- /dev/null +++ b/viewmodel/generator/src/main/kotlin/ru/pixnews/anvil/codegen/viewmodel/generator/ContributesViewModelCodeGenerator.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.viewmodel.generator + +import com.google.auto.service.AutoService +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.buildFile +import com.squareup.anvil.compiler.internal.reference.ClassReference +import com.squareup.anvil.compiler.internal.reference.MemberFunctionReference +import com.squareup.anvil.compiler.internal.reference.asClassName +import com.squareup.anvil.compiler.internal.reference.classAndInnerClassReferences +import com.squareup.anvil.compiler.internal.reference.generateClassName +import com.squareup.anvil.compiler.internal.safePackageString +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.MemberName +import com.squareup.kotlinpoet.TypeSpec +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtFile +import ru.pixnews.anvil.codegen.common.classname.DaggerClassName +import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName +import ru.pixnews.anvil.codegen.common.fqname.FqNames +import ru.pixnews.anvil.codegen.common.util.checkClassExtendsType +import ru.pixnews.anvil.codegen.common.util.contributesToAnnotation +import ru.pixnews.anvil.codegen.common.util.isSavedStateHandle +import ru.pixnews.anvil.codegen.common.util.parseConstructorParameters +import java.io.File + +@AutoService(CodeGenerator::class) +public class ContributesViewModelCodeGenerator : CodeGenerator { + override fun isApplicable(context: AnvilContext): Boolean = true + + override fun generateCode( + codeGenDir: File, + module: ModuleDescriptor, + projectFiles: Collection, + ): Collection { + return projectFiles + .classAndInnerClassReferences(module) + .filter { it.isAnnotatedWith(FqNames.contributesViewModel) } + .map { generateViewModelModule(it, codeGenDir) } + .toList() + } + + private fun generateViewModelModule( + annotatedClass: ClassReference, + codeGenDir: File, + ): GeneratedFile { + annotatedClass.checkClassExtendsType(viewModelFqName) + + val moduleClassId = annotatedClass.generateClassName(suffix = "_FactoryModule") + val generatedPackage = moduleClassId.packageFqName.safePackageString() + val moduleClassName = moduleClassId.relativeClassName.asString() + + val moduleInterfaceSpecBuilder = TypeSpec.objectBuilder(moduleClassName) + .addAnnotation(DaggerClassName.module) + .addAnnotation(contributesToAnnotation(PixnewsClassName.viewModelScope)) + .addFunction(generateProvidesFactoryMethod(annotatedClass)) + + val content = FileSpec.buildFile(generatedPackage, moduleClassName) { + addType(moduleInterfaceSpecBuilder.build()) + } + return createGeneratedFile(codeGenDir, generatedPackage, moduleClassName, content) + } + + private fun generateProvidesFactoryMethod( + annotatedClass: ClassReference, + ): FunSpec { + val viewModelClass = annotatedClass.asClassName() + + val primaryConstructor: MemberFunctionReference = annotatedClass.constructors.firstOrNull() + ?: throw IllegalArgumentException("No primary constructor on $annotatedClass") + val primaryConstructorParams = primaryConstructor.parameters.parseConstructorParameters(annotatedClass) + + val builder = FunSpec.builder("provides${annotatedClass.shortName}ViewModelFactory") + .addAnnotation(DaggerClassName.provides) + .addAnnotation(DaggerClassName.intoMap) + .addAnnotation( + AnnotationSpec + .builder(PixnewsClassName.viewModelMapKey) + .addMember("%T::class", viewModelClass) + .build(), + ) + .returns(PixnewsClassName.viewModelFactory) + + primaryConstructorParams + .filter { !it.isSavedStateHandle() } + .forEach { builder.addParameter(it.name, it.resolvedType) } + + val viewModelConstructorParameters = primaryConstructorParams.joinToString(separator = "\n") { + if (!it.isSavedStateHandle()) { + "${it.name} = ${it.name}," + } else { + "${it.name} = it.%M()" + } + } + val createViewModeStatementArgs: Array = primaryConstructorParams.mapNotNull { + if (it.isSavedStateHandle()) createSavedStateHandleMember else null + }.toTypedArray() + + builder.beginControlFlow("return %T", PixnewsClassName.viewModelFactory) + @Suppress("SpreadOperator") + builder.addStatement("%T(\n$viewModelConstructorParameters\n)", viewModelClass, *createViewModeStatementArgs) + builder.endControlFlow() + return builder.build() + } + + private companion object { + private val viewModelFqName = FqName("androidx.lifecycle.ViewModel") + private val createSavedStateHandleMember = MemberName( + packageName = "androidx.lifecycle", + simpleName = "createSavedStateHandle", + isExtension = true, + ) + } +} diff --git a/viewmodel/generator/src/test/kotlin/ru/pixnews/anvil/codegen/viewmodel/generator/ContributesViewModelCodeGeneratorTest.kt b/viewmodel/generator/src/test/kotlin/ru/pixnews/anvil/codegen/viewmodel/generator/ContributesViewModelCodeGeneratorTest.kt new file mode 100644 index 0000000..7ac2cf2 --- /dev/null +++ b/viewmodel/generator/src/test/kotlin/ru/pixnews/anvil/codegen/viewmodel/generator/ContributesViewModelCodeGeneratorTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.viewmodel.generator + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.containsExactlyInAnyOrder +import assertk.assertions.isEqualTo +import com.squareup.anvil.annotations.ContributesTo +import com.squareup.anvil.compiler.internal.testing.compileAnvil +import com.squareup.kotlinpoet.ClassName +import com.tschuchort.compiletesting.JvmCompilationResult +import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK +import dagger.Provides +import dagger.multibindings.IntoMap +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle +import org.junit.jupiter.api.fail +import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName +import ru.pixnews.anvil.codegen.testutils.haveAnnotation +import ru.pixnews.anvil.codegen.testutils.loadClass + +@OptIn(ExperimentalCompilerApi::class) +@TestInstance(Lifecycle.PER_CLASS) +class ContributesViewModelCodeGeneratorTest { + private val generatedModuleName = "com.test.TestViewModel_FactoryModule" + private val featureManagerClass = ClassName("ru.pixnews.foundation.featuretoggles", "FeatureManager") + private lateinit var compilationResult: JvmCompilationResult + + @BeforeAll + @Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") + fun setup() { + val androidxLifecycleStubs = """ + package androidx.lifecycle + + abstract class ViewModel + class SavedStateHandle + abstract class CreationExtras + + fun CreationExtras.createSavedStateHandle(): SavedStateHandle = SavedStateHandle() + """.trimIndent() + + val featureTogglesStubs = """ + package ${featureManagerClass.packageName} + interface ${featureManagerClass.simpleName} + """.trimIndent() + + val baseDiViewModelStubs = """ + package ru.pixnews.foundation.di.ui.base.viewmodel + + import androidx.lifecycle.ViewModel + import androidx.lifecycle.CreationExtras + import kotlin.reflect.KClass + + public annotation class ContributesViewModel + public annotation class ViewModelMapKey(val viewModelClass: KClass) + public abstract class ViewModelScope private constructor() + + public fun interface ViewModelFactory { + public fun create(creationExtras: CreationExtras): ViewModel + } + """.trimIndent() + + val testViewModel = """ + package com.test + + import androidx.lifecycle.ViewModel + import androidx.lifecycle.SavedStateHandle + import ru.pixnews.foundation.featuretoggles.FeatureManager + import ru.pixnews.foundation.di.ui.base.viewmodel.ContributesViewModel + + @Suppress("UNUSED_PARAMETER") + @ContributesViewModel + class TestViewModel( + featureManager: FeatureManager, + savedStateHandle: SavedStateHandle, + ) : ViewModel() + """.trimIndent() + + compilationResult = compileAnvil( + sources = arrayOf( + baseDiViewModelStubs, + androidxLifecycleStubs, + featureTogglesStubs, + testViewModel, + ), + ) + } + + @Test + fun `Dagger module should be generated`() { + assertThat(compilationResult.exitCode).isEqualTo(OK) + } + + @Test + fun `Generated module should have correct annotations`() { + val clazz = compilationResult.classLoader.loadClass(generatedModuleName) + val viewModelScopeClass = compilationResult.classLoader.loadClass(PixnewsClassName.viewModelScope) + assertThat(clazz).haveAnnotation(ContributesTo::class.java) + + assertThat( + clazz.getAnnotation(ContributesTo::class.java).scope.java, + ).isEqualTo(viewModelScopeClass) + } + + @Test + fun `Generated module should have correct provide method`() { + val moduleClass = compilationResult.classLoader.loadClass(generatedModuleName) + val viewModelMapKey = compilationResult.classLoader.loadClass(PixnewsClassName.viewModelMapKey) + val viewModelFactoryClass = compilationResult.classLoader.loadClass(PixnewsClassName.viewModelFactory) + val featureManagerClass = compilationResult.classLoader.loadClass(featureManagerClass) + + val provideMethod = moduleClass.declaredMethods.firstOrNull { + it.name == "providesTestViewModelViewModelFactory" + } ?: fail("no providesTestViewModelViewModelFactory method") + + assertThat(provideMethod.returnType).isEqualTo(viewModelFactoryClass) + assertThat(provideMethod.parameterTypes).containsExactly(featureManagerClass) + assertThat(provideMethod.annotations.map(Annotation::annotationClass)).containsExactlyInAnyOrder( + Provides::class, + IntoMap::class, + viewModelMapKey.kotlin, + ) + } +} diff --git a/viewmodel/inject/build.gradle.kts b/viewmodel/inject/build.gradle.kts new file mode 100644 index 0000000..e2ff629 --- /dev/null +++ b/viewmodel/inject/build.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +plugins { + id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library") + id("ru.pixnews.anvil.codegen.build-logic.project.test") + id("ru.pixnews.anvil.codegen.build-logic.project.publish") +} + +group = "ru.pixnews.anvil.codegen.viewmodel.inject" +version = "0.1-SNAPSHOT" + +dependencies { +} diff --git a/workmanager/generator/build.gradle.kts b/workmanager/generator/build.gradle.kts new file mode 100644 index 0000000..3a364b9 --- /dev/null +++ b/workmanager/generator/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +plugins { + id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library") + id("ru.pixnews.anvil.codegen.build-logic.project.test") + id("ru.pixnews.anvil.codegen.build-logic.project.publish") + kotlin("kapt") +} + +group = "ru.pixnews.anvil.codegen.workamanger.generator" +version = "0.1-SNAPSHOT" + +dependencies { + api(libs.anvil.compiler.api) + implementation(libs.anvil.compiler.utils) + implementation(libs.kotlinpoet) { exclude(module = "kotlin-reflect") } + implementation(projects.common) + + compileOnly(libs.auto.service.annotations) + kapt(libs.auto.service.compiler) + + testImplementation(libs.anvil.annotations.optional) + testImplementation(libs.assertk) + testImplementation(libs.dagger) + testImplementation(projects.testUtils) + testImplementation(testFixtures(libs.anvil.compiler.utils)) +} diff --git a/workmanager/generator/src/main/kotlin/ru/pixnews/anvil/codegen/workmanager/generator/ContributesCoroutineWorkerCodeGenerator.kt b/workmanager/generator/src/main/kotlin/ru/pixnews/anvil/codegen/workmanager/generator/ContributesCoroutineWorkerCodeGenerator.kt new file mode 100644 index 0000000..11caf79 --- /dev/null +++ b/workmanager/generator/src/main/kotlin/ru/pixnews/anvil/codegen/workmanager/generator/ContributesCoroutineWorkerCodeGenerator.kt @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.workmanager.generator + +import com.google.auto.service.AutoService +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.buildFile +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.anvil.compiler.internal.reference.generateClassName +import com.squareup.anvil.compiler.internal.safePackageString +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.ABSTRACT +import com.squareup.kotlinpoet.KModifier.OVERRIDE +import com.squareup.kotlinpoet.KModifier.PUBLIC +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.TypeSpec.Companion.interfaceBuilder +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtFile +import ru.pixnews.anvil.codegen.common.classname.AndroidClassName +import ru.pixnews.anvil.codegen.common.classname.DaggerClassName +import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName +import ru.pixnews.anvil.codegen.common.fqname.FqNames +import ru.pixnews.anvil.codegen.common.util.checkClassExtendsType +import ru.pixnews.anvil.codegen.common.util.contributesMultibindingAnnotation +import java.io.File + +@AutoService(CodeGenerator::class) +public class ContributesCoroutineWorkerCodeGenerator : CodeGenerator { + override fun isApplicable(context: AnvilContext): Boolean = true + + override fun generateCode( + codeGenDir: File, + module: ModuleDescriptor, + projectFiles: Collection, + ): Collection { + return projectFiles + .classAndInnerClassReferences(module) + .filter { it.isAnnotatedWith(FqNames.contributesCoroutineWorker) } + .map { generateWorkManagerFactory(it, codeGenDir) } + .toList() + } + + private fun generateWorkManagerFactory( + annotatedClass: ClassReference, + codeGenDir: File, + ): GeneratedFile { + annotatedClass.checkClassExtendsType(coroutineWorkerFqName) + + val workerClassName = annotatedClass.asClassName() + val factoryClassId = annotatedClass.generateClassName(suffix = "_AssistedFactory") + val generatedPackage = factoryClassId.packageFqName.safePackageString() + val factoryClassName = factoryClassId.relativeClassName.asString() + + val factoryInterfaceSpec = interfaceBuilder(factoryClassName) + .addAnnotation(DaggerClassName.assistedFactory) + .addAnnotation(contributesMultibindingAnnotation(PixnewsClassName.workManagerScope)) + .addAnnotation( + AnnotationSpec + .builder(PixnewsClassName.coroutineWorkerMapKey) + .addMember("%T::class", workerClassName) + .build(), + ) + .addSuperinterface(PixnewsClassName.coroutineWorkerFactory) + .addFunction(createWorkerFunction(workerClassName)) + .build() + val content = FileSpec.buildFile(generatedPackage, factoryClassName) { + addType(factoryInterfaceSpec) + } + return createGeneratedFile(codeGenDir, generatedPackage, factoryClassName, content) + } + + /** + * ``` + * override fun create(@ApplicationContext context: Context, workerParameters: WorkerParameters): + * ``` + */ + private fun createWorkerFunction(workerClass: ClassName): FunSpec { + return FunSpec.builder("create") + .addModifiers(ABSTRACT, OVERRIDE, PUBLIC) + .addParameter( + ParameterSpec.builder("context", AndroidClassName.context) + .addAnnotation(PixnewsClassName.applicationContext) + .build(), + ) + .addParameter("workerParameters", AndroidClassName.workerParameters) + .returns(workerClass) + .build() + } + + private companion object { + private val coroutineWorkerFqName = FqName("androidx.work.CoroutineWorker") + } +} diff --git a/workmanager/generator/src/test/kotlin/ru/pixnews/anvil/codegen/workmanager/generator/ContributesCoroutineWorkerCodeGeneratorTest.kt b/workmanager/generator/src/test/kotlin/ru/pixnews/anvil/codegen/workmanager/generator/ContributesCoroutineWorkerCodeGeneratorTest.kt new file mode 100644 index 0000000..554ff06 --- /dev/null +++ b/workmanager/generator/src/test/kotlin/ru/pixnews/anvil/codegen/workmanager/generator/ContributesCoroutineWorkerCodeGeneratorTest.kt @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors. + * Please see the AUTHORS file for details. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package ru.pixnews.anvil.codegen.workmanager.generator + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.isEqualTo +import com.squareup.anvil.annotations.ContributesMultibinding +import com.squareup.anvil.compiler.internal.testing.compileAnvil +import com.tschuchort.compiletesting.JvmCompilationResult +import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK +import dagger.assisted.AssistedFactory +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle +import org.junit.jupiter.api.fail +import ru.pixnews.anvil.codegen.common.classname.AndroidClassName +import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName +import ru.pixnews.anvil.codegen.testutils.getElementValue +import ru.pixnews.anvil.codegen.testutils.haveAnnotation +import ru.pixnews.anvil.codegen.testutils.loadClass + +@OptIn(ExperimentalCompilerApi::class) +@TestInstance(Lifecycle.PER_CLASS) +class ContributesCoroutineWorkerCodeGeneratorTest { + private val generatedFactoryName = "com.test.TestWorker_AssistedFactory" + private lateinit var compilationResult: JvmCompilationResult + + @BeforeAll + @Suppress("LOCAL_VARIABLE_EARLY_DECLARATION", "LongMethod") + fun setup() { + val androidContextStub = """ + package android.content + open class Context + """.trimIndent() + + val workManagerStubs = """ + package androidx.work + import android.content.Context + + open class WorkerParameters + + @Suppress("UNUSED_PARAMETER") + abstract class CoroutineWorker( + appContext: Context, + params: WorkerParameters + ) { + public abstract suspend fun doWork(): Result + } + """.trimIndent() + + val daggerStubs = """ + package dagger.assisted + public annotation class Assisted + public annotation class AssistedInject + """.trimIndent() + + val appContextQualifier = """ + package ru.pixnews.foundation.di.base.qualifiers + public annotation class ApplicationContext + """.trimIndent() + + val workmanagerDiStubs = """ + package ru.pixnews.foundation.di.workmanager + import android.content.Context + import androidx.work.CoroutineWorker + import androidx.work.WorkerParameters + import ru.pixnews.foundation.di.base.qualifiers.ApplicationContext + import kotlin.reflect.KClass + + public abstract class WorkManagerScope private constructor() + public annotation class ContributesCoroutineWorker + public annotation class CoroutineWorkerMapKey(val workerClass: KClass) + + public interface CoroutineWorkerFactory { + public fun create( + @ApplicationContext context: Context, + workerParameters: WorkerParameters, + ): CoroutineWorker + } + """.trimIndent() + + val testWorker = """ + package com.test + + import android.content.Context + import androidx.work.CoroutineWorker + import androidx.work.WorkerParameters + import dagger.assisted.Assisted + import dagger.assisted.AssistedInject + import ru.pixnews.foundation.di.base.qualifiers.ApplicationContext + import ru.pixnews.foundation.di.workmanager.ContributesCoroutineWorker + + @Suppress("UNUSED_PARAMETER") + @ContributesCoroutineWorker + public class TestWorker @AssistedInject constructor( + @Assisted @ApplicationContext appContext: Context, + @Assisted params: WorkerParameters, + ) : CoroutineWorker(appContext, params) { + override suspend fun doWork(): Result { + return Result.success(Unit) + } + } + """.trimIndent() + + compilationResult = compileAnvil( + sources = arrayOf( + androidContextStub, + workManagerStubs, + daggerStubs, + appContextQualifier, + workmanagerDiStubs, + testWorker, + ), + ) + } + + @Test + fun `Dagger factory should be generated`() { + assertThat(compilationResult.exitCode).isEqualTo(OK) + } + + @Test + fun `Generated factory should have correct annotations and superclass`() { + val clazz = compilationResult.classLoader.loadClass(generatedFactoryName) + val testWorkerClass = compilationResult.classLoader.loadClass("com.test.TestWorker") + val workManagerScopeClass = compilationResult.classLoader.loadClass(PixnewsClassName.workManagerScope) + val coroutineWorkerFactoryClass = + compilationResult.classLoader.loadClass(PixnewsClassName.coroutineWorkerFactory) + + @Suppress("UNCHECKED_CAST") + val coroutineWorkerMapKeyClass: Class = compilationResult.classLoader.loadClass( + PixnewsClassName.coroutineWorkerMapKey, + ) as Class + + assertThat(clazz).haveAnnotation(AssistedFactory::class.java) + assertThat(clazz).haveAnnotation(ContributesMultibinding::class.java) + assertThat( + clazz.getAnnotation(ContributesMultibinding::class.java).scope.java, + ).isEqualTo(workManagerScopeClass) + + assertThat( + clazz.getAnnotation(coroutineWorkerMapKeyClass).getElementValue>("workerClass"), + ).isEqualTo(testWorkerClass) + + assertTrue { coroutineWorkerFactoryClass.isAssignableFrom(clazz) } + } + + @Test + fun `Generated factory should have correct create method`() { + val factoryClass = compilationResult.classLoader.loadClass(generatedFactoryName) + val testWorkerClass = compilationResult.classLoader.loadClass("com.test.TestWorker") + val androidContextClass = compilationResult.classLoader.loadClass(AndroidClassName.context) + val workerParamsClass = compilationResult.classLoader.loadClass(AndroidClassName.workerParameters) + + val createMethod = factoryClass.declaredMethods.firstOrNull { + it.name == "create" + } ?: fail("no create() method") + + assertThat(createMethod.parameterTypes).containsExactly(androidContextClass, workerParamsClass) + assertThat(createMethod.returnType).isEqualTo(testWorkerClass) + } +} diff --git a/lib/build.gradle.kts b/workmanager/inject/build.gradle.kts similarity index 73% rename from lib/build.gradle.kts rename to workmanager/inject/build.gradle.kts index 29140be..1af30c9 100644 --- a/lib/build.gradle.kts +++ b/workmanager/inject/build.gradle.kts @@ -5,5 +5,8 @@ plugins { id("ru.pixnews.anvil.codegen.build-logic.project.publish") } +group = "ru.pixnews.anvil.codegen.workmanager.inject" +version = "0.1-SNAPSHOT" + dependencies { }