Repository: square/anvil (kotlin)

Issue:
Feature Request: Support generation-only mode
Currently, factory generation can be enabled in isolation but not other types of non-transforming generation like `@Contributes*` APIs. Talked offline a bit, it would be nice if there was a way to enable this mode of anvil while still disabling transformation (which requires disabling incremental stub generation).

Pull request description:
Resolves #258

Patch:
diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/AnvilCommandLineProcessor.kt b/compiler/src/main/java/com/squareup/anvil/compiler/AnvilCommandLineProcessor.kt
index 1dbc6cdf6..fd121b9dd 100644
--- a/compiler/src/main/java/com/squareup/anvil/compiler/AnvilCommandLineProcessor.kt
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/AnvilCommandLineProcessor.kt
@@ -18,6 +18,10 @@ internal const val generateDaggerFactoriesOnlyName = "generate-dagger-factories-
 internal val generateDaggerFactoriesOnlyKey =
   CompilerConfigurationKey.create<Boolean>("anvil $generateDaggerFactoriesOnlyName")
 
+internal const val disableComponentMergingName = "disable-component-merging"
+internal val disableComponentMergingKey =
+  CompilerConfigurationKey.create<Boolean>("anvil $disableComponentMergingName")
+
 /**
  * Parses arguments from the Gradle plugin for the compiler plugin.
  */
@@ -49,6 +53,14 @@ class AnvilCommandLineProcessor : CommandLineProcessor {
         "contributed code.",
       required = false,
       allowMultipleOccurrences = false
+    ),
+    CliOption(
+      optionName = disableComponentMergingName,
+      valueDescription = "<true|false>",
+      description = "Whether Anvil should generate code only and not transform code for " +
+        "@MergeComponent or @MergeSubcomponent.",
+      required = false,
+      allowMultipleOccurrences = false
     )
   )
 
@@ -63,6 +75,8 @@ class AnvilCommandLineProcessor : CommandLineProcessor {
         configuration.put(generateDaggerFactoriesKey, value.toBoolean())
       generateDaggerFactoriesOnlyName ->
         configuration.put(generateDaggerFactoriesOnlyKey, value.toBoolean())
+      disableComponentMergingName ->
+        configuration.put(disableComponentMergingKey, value.toBoolean())
     }
   }
 }
diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/AnvilComponentRegistrar.kt b/compiler/src/main/java/com/squareup/anvil/compiler/AnvilComponentRegistrar.kt
index b84ba81eb..c79291c26 100644
--- a/compiler/src/main/java/com/squareup/anvil/compiler/AnvilComponentRegistrar.kt
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/AnvilComponentRegistrar.kt
@@ -8,6 +8,7 @@ import com.squareup.anvil.compiler.codegen.ContributesBindingGenerator
 import com.squareup.anvil.compiler.codegen.ContributesMultibindingGenerator
 import com.squareup.anvil.compiler.codegen.ContributesToGenerator
 import com.squareup.anvil.compiler.codegen.dagger.AnvilAnnotationDetectorCheck
+import com.squareup.anvil.compiler.codegen.dagger.AnvilMergeAnnotationDetectorCheck
 import com.squareup.anvil.compiler.codegen.dagger.AssistedFactoryGenerator
 import com.squareup.anvil.compiler.codegen.dagger.AssistedInjectGenerator
 import com.squareup.anvil.compiler.codegen.dagger.ComponentDetectorCheck
@@ -39,16 +40,21 @@ class AnvilComponentRegistrar : ComponentRegistrar {
 
     val codeGenerators = mutableListOf<CodeGenerator>()
     val generateDaggerFactoriesOnly = configuration.get(generateDaggerFactoriesOnlyKey, false)
+    val disableComponentMerging = configuration.get(disableComponentMergingKey, false)
 
     if (!generateDaggerFactoriesOnly) {
       codeGenerators += ContributesToGenerator()
       codeGenerators += ContributesBindingGenerator()
       codeGenerators += ContributesMultibindingGenerator()
       codeGenerators += BindingModuleGenerator(scanner)
-    } else {
+    } else if (!disableComponentMerging) {
       codeGenerators += AnvilAnnotationDetectorCheck()
     }
 
+    if (disableComponentMerging) {
+      codeGenerators += AnvilMergeAnnotationDetectorCheck()
+    }
+
     if (configuration.get(generateDaggerFactoriesKey, false)) {
       codeGenerators += ProvidesMethodFactoryGenerator()
       codeGenerators += InjectConstructorFactoryGenerator()
@@ -64,7 +70,7 @@ class AnvilComponentRegistrar : ComponentRegistrar {
     //
     // It's not dangerous for our extension to run first, because we generate code, restart the
     // analysis phase and then don't return a result anymore. That means the next extension can
-    // take over. If we wouldn't do this and any other extension won't let our's run, then we
+    // take over. If we wouldn't do this and any other extension won't let ours run, then we
     // couldn't generate any code.
     AnalysisHandlerExtension.registerExtensionFirst(
       project,
@@ -74,7 +80,7 @@ class AnvilComponentRegistrar : ComponentRegistrar {
       )
     )
 
-    if (!generateDaggerFactoriesOnly) {
+    if (!generateDaggerFactoriesOnly && !disableComponentMerging) {
       SyntheticResolveExtension.registerExtension(
         project,
         InterfaceMerger(scanner)
diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/dagger/AnvilMergeAnnotationDetectorCheck.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/dagger/AnvilMergeAnnotationDetectorCheck.kt
new file mode 100644
index 000000000..554ed9ad4
--- /dev/null
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/dagger/AnvilMergeAnnotationDetectorCheck.kt
@@ -0,0 +1,41 @@
+package com.squareup.anvil.compiler.codegen.dagger
+
+import com.squareup.anvil.compiler.AnvilCompilationException
+import com.squareup.anvil.compiler.codegen.PrivateCodeGenerator
+import com.squareup.anvil.compiler.codegen.classesAndInnerClasses
+import com.squareup.anvil.compiler.codegen.hasAnnotation
+import com.squareup.anvil.compiler.mergeComponentFqName
+import com.squareup.anvil.compiler.mergeInterfacesFqName
+import com.squareup.anvil.compiler.mergeModulesFqName
+import com.squareup.anvil.compiler.mergeSubcomponentFqName
+import org.jetbrains.kotlin.descriptors.ModuleDescriptor
+import org.jetbrains.kotlin.psi.KtFile
+import java.io.File
+
+internal class AnvilMergeAnnotationDetectorCheck : PrivateCodeGenerator() {
+
+  override fun generateCodePrivate(
+    codeGenDir: File,
+    module: ModuleDescriptor,
+    projectFiles: Collection<KtFile>
+  ) {
+    val clazz = projectFiles
+      .asSequence()
+      .flatMap { it.classesAndInnerClasses() }
+      .firstOrNull {
+        it.hasAnnotation(mergeComponentFqName) ||
+          it.hasAnnotation(mergeSubcomponentFqName) ||
+          it.hasAnnotation(mergeInterfacesFqName) ||
+          it.hasAnnotation(mergeModulesFqName)
+      }
+
+    if (clazz != null) {
+      throw AnvilCompilationException(
+        message = "This Gradle module is configured to ONLY generate code with " +
+          "the `disableComponentMerging` flag. However, this module contains code that " +
+          "uses Anvil @Merge* annotations. That's not supported.",
+        element = clazz
+      )
+    }
+  }
+}
diff --git a/gradle-plugin/src/main/java/com/squareup/anvil/plugin/AnvilExtension.kt b/gradle-plugin/src/main/java/com/squareup/anvil/plugin/AnvilExtension.kt
index cc93d92b4..06f2394b4 100644
--- a/gradle-plugin/src/main/java/com/squareup/anvil/plugin/AnvilExtension.kt
+++ b/gradle-plugin/src/main/java/com/squareup/anvil/plugin/AnvilExtension.kt
@@ -25,4 +25,13 @@ open class AnvilExtension {
    * to `true`, then also [generateDaggerFactories] must be set to `true`.
    */
   var generateDaggerFactoriesOnly = false
+
+  /**
+   * Enabling this indicates that only code generation should run and no component merging should
+   * run. This is useful for cases where you want to use `@ContributesTo`, `@ContributesBinding`,
+   * or similar annotations but _not_ `@MergeComponent` or `@MergeSubcomponent` functionality.
+   * This allows for anvil use in projects with kapt enabled but _not_ require disabling
+   * incremental compilation in kapt stub generation tasks.
+   */
+  var disableComponentMerging = false
 }
diff --git a/gradle-plugin/src/main/java/com/squareup/anvil/plugin/AnvilPlugin.kt b/gradle-plugin/src/main/java/com/squareup/anvil/plugin/AnvilPlugin.kt
index 294bdb16e..876435b85 100644
--- a/gradle-plugin/src/main/java/com/squareup/anvil/plugin/AnvilPlugin.kt
+++ b/gradle-plugin/src/main/java/com/squareup/anvil/plugin/AnvilPlugin.kt
@@ -68,6 +68,10 @@ open class AnvilPlugin : KotlinCompilerPluginSupportPlugin {
         SubpluginOption(
           key = "generate-dagger-factories-only",
           value = extension.generateDaggerFactoriesOnly.toString()
+        ),
+        SubpluginOption(
+          key = "disable-component-merging",
+          value = extension.disableComponentMerging.toString()
         )
       )
     }
@@ -171,9 +175,10 @@ open class AnvilPlugin : KotlinCompilerPluginSupportPlugin {
     @Suppress("UnstableApiUsage")
     val incrementalSignal = IncrementalSignal.registerIfAbsent(project)
 
-    if (extension.generateDaggerFactoriesOnly) {
+    if (extension.generateDaggerFactoriesOnly || extension.disableComponentMerging) {
       // We don't need to disable the incremental compilation for the stub generating task, when we
-      // only generate Dagger factories. That's only needed for merging Dagger modules.
+      // only generate Dagger factories or contributing modules. That's only needed for merging
+      // Dagger modules.
       if (isAndroidProject) {
         project.androidVariantsConfigure { variant ->
           val compileTaskName = "kaptGenerateStubs${variant.name.capitalize(US)}Kotlin"


Test patch:
diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/TestUtils.kt b/compiler/src/test/java/com/squareup/anvil/compiler/TestUtils.kt
index 223efdb10..8590384ff 100644
--- a/compiler/src/test/java/com/squareup/anvil/compiler/TestUtils.kt
+++ b/compiler/src/test/java/com/squareup/anvil/compiler/TestUtils.kt
@@ -22,6 +22,7 @@ internal fun compile(
   enableDaggerAnnotationProcessor: Boolean = false,
   generateDaggerFactories: Boolean = false,
   generateDaggerFactoriesOnly: Boolean = false,
+  disableComponentMerging: Boolean = false,
   allWarningsAsErrors: Boolean = true,
   block: Result.() -> Unit = { }
 ): Result {
@@ -57,6 +58,11 @@ internal fun compile(
           pluginId = commandLineProcessor.pluginId,
           optionName = generateDaggerFactoriesOnlyName,
           optionValue = generateDaggerFactoriesOnly.toString()
+        ),
+        PluginOption(
+          pluginId = commandLineProcessor.pluginId,
+          optionName = disableComponentMergingName,
+          optionValue = disableComponentMerging.toString()
         )
       )
 
diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/AnvilMergeAnnotationDetectorCheckTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/AnvilMergeAnnotationDetectorCheckTest.kt
new file mode 100644
index 000000000..9540c24b6
--- /dev/null
+++ b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/AnvilMergeAnnotationDetectorCheckTest.kt
@@ -0,0 +1,121 @@
+package com.squareup.anvil.compiler.dagger
+
+import com.google.common.truth.Truth.assertThat
+import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.COMPILATION_ERROR
+import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK
+import com.tschuchort.compiletesting.KotlinCompilation.Result
+import org.junit.Test
+
+class AnvilMergeAnnotationDetectorCheckTest {
+
+  @Test fun `@ContributesTo is allowed`() {
+    compile(
+      """
+      package com.squareup.test
+      
+      import dagger.Module
+      
+      @Module
+      @com.squareup.anvil.annotations.ContributesTo(Any::class)
+      object AnyClass
+      """
+    ) {
+      assertThat(exitCode).isEqualTo(OK)
+    }
+  }
+
+  @Test fun `@ContributesBinding is allowed`() {
+    compile(
+      """
+      package com.squareup.test
+      
+      interface BaseType
+      
+      @com.squareup.anvil.annotations.ContributesBinding(Any::class)
+      class AnyClass : BaseType
+      """
+    ) {
+      assertThat(exitCode).isEqualTo(OK)
+    }
+  }
+
+  @Test fun `@MergeComponent is not allowed`() {
+    compile(
+      """
+      package com.squareup.test
+      
+      import dagger.Subcomponent
+      
+      @com.squareup.anvil.annotations.MergeComponent(Any::class)
+      class AnyClass
+      """
+    ) {
+      assertError()
+    }
+  }
+
+  @Test fun `@MergeSubcomponent is not allowed`() {
+    compile(
+      """
+      package com.squareup.test
+      
+      import dagger.Subcomponent
+      
+      @com.squareup.anvil.annotations.MergeSubcomponent(Any::class)
+      class AnyClass
+      """
+    ) {
+      assertError()
+    }
+  }
+
+  @Test fun `@MergeModules is not allowed`() {
+    compile(
+      """
+      package com.squareup.test
+      
+      import dagger.Subcomponent
+      
+      @com.squareup.anvil.annotations.compat.MergeModules(Any::class)
+      class AnyClass
+      """
+    ) {
+      assertError()
+    }
+  }
+
+  @Test fun `@MergeInterfaces is not allowed`() {
+    compile(
+      """
+      package com.squareup.test
+      
+      import dagger.Subcomponent
+      
+      @com.squareup.anvil.annotations.compat.MergeInterfaces(Any::class)
+      class AnyClass
+      """
+    ) {
+      assertError()
+    }
+  }
+
+  private fun Result.assertError() {
+    assertThat(exitCode).isEqualTo(COMPILATION_ERROR)
+    assertThat(messages).contains("Source.kt: (5, 1")
+    assertThat(messages).contains(
+      "This Gradle module is configured to ONLY generate code with the " +
+        "`disableComponentMerging` flag. However, this module contains code that uses " +
+        "Anvil @Merge* annotations. That's not supported."
+    )
+  }
+
+  @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
+  private fun compile(
+    vararg sources: String,
+    block: Result.() -> Unit = { }
+  ): Result = com.squareup.anvil.compiler.compile(
+    sources = sources,
+    disableComponentMerging = true,
+    block = block
+  )
+}