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 {
}