Repository: square/anvil (kotlin)

Issue:
Excluded bindings are excluded from all components with the same scope.
Given the following example
```kotlin
interface ParentInterface
      
@ContributesBinding(Any::class)
interface ContributingInterface : ParentInterface
      
@MergeComponent(Any::class)
interface ComponentInterface
      
@MergeComponent(
  scope = Any::class,
  exclude = [ContributingInterface::class]
)
interface ComponentInterface2
```

We'd expect that the `ContributingInterface` would be present in `ComponentInterface`, but not `ComponentInterface2`. However, it is getting excluded in both. I don't believe this is a regression.

Pull request:
Ensure that excluded bindings and modules are only excluded for the specific component that merges a scope and not all components merging the same scope.

Fixes #321

Patch:
diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/BindingModuleGenerator.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/BindingModuleGenerator.kt
index a48d60df6..8ade164a0 100644
--- a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/BindingModuleGenerator.kt
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/BindingModuleGenerator.kt
@@ -84,10 +84,9 @@ internal class BindingModuleGenerator(
 
   // Keeps track of for which scopes which files were generated. Usually there is only one file,
   // but technically there can be multiple.
-  private val mergedScopes = mutableMapOf<FqName, MutableList<Pair<File, KtClassOrObject>>>()
-    .withDefault { mutableListOf() }
+  private val mergedClasses = mutableMapOf<MergedClassKey, File>()
 
-  private val excludedTypesForScope = mutableMapOf<FqName, List<FqName>>()
+  private val excludedTypesForClass = mutableMapOf<MergedClassKey, List<FqName>>()
 
   private val contributedBindingClasses = mutableListOf<FqName>()
   private val contributedMultibindingClasses = mutableListOf<FqName>()
@@ -130,11 +129,12 @@ internal class BindingModuleGenerator(
       .map { (psiClass, mergeAnnotation) ->
         val annotationFqName = mergeAnnotation.requireFqName(module)
         val scope = psiClass.scope(annotationFqName, module)
+        val mergedClassKey = MergedClassKey(scope, psiClass)
 
         // Remember for which scopes which types were excluded so that we later don't generate
         // a binding method for these types.
         psiClass.excludeOrNull(annotationFqName, module)
-          ?.let { excludedTypesForScope[scope] = it }
+          ?.let { excludedTypesForClass[mergedClassKey] = it }
 
         val packageName = generatePackageName(psiClass)
         val className = psiClass.generateClassName()
@@ -157,8 +157,7 @@ internal class BindingModuleGenerator(
           generatedMethods = emptyList()
         )
 
-        mergedScopes[scope] = mergedScopes.getValue(scope)
-          .apply { this += file to psiClass }
+        mergedClasses[mergedClassKey] = file
 
         GeneratedFile(file, content)
       }
@@ -169,9 +168,11 @@ internal class BindingModuleGenerator(
     codeGenDir: File,
     module: ModuleDescriptor
   ): Collection<GeneratedFile> {
-    return mergedScopes.flatMap { (scope, daggerModuleFiles) ->
-      // Precompute this list once per scope since it expensive.
-      val excludedNames = excludedTypesForScope[scope].orEmpty()
+    return mergedClasses.map { (mergedClassKey, daggerModuleFile) ->
+      val scope = mergedClassKey.scope
+
+      // Precompute this list once per class since it's expensive.
+      val excludedNames = excludedTypesForClass[mergedClassKey].orEmpty()
 
       // Contributed Dagger modules can replace other Dagger modules but also contributed bindings.
       // If a binding is replaced, then we must not generate the binding method.
@@ -194,7 +195,7 @@ internal class BindingModuleGenerator(
             )
             .filter { it.annotationOrNull(daggerModuleFqName) != null }
             // Ignore replaced bindings specified by excluded modules for this scope.
-            .filter { it.fqNameSafe !in excludedTypesForScope[scope].orEmpty() }
+            .filter { it.fqNameSafe !in excludedNames }
             .flatMap {
               it.annotationOrNull(contributesToFqName, scope)
                 ?.replaces(module)
@@ -235,7 +236,7 @@ internal class BindingModuleGenerator(
         return (contributedBindingsThisModule + contributedBindingsDependencies)
           .minus(replacedBindings)
           .filterNot { it.fqNameSafe in bindingsReplacedInDaggerModules }
-          .filterNot { it.fqNameSafe in excludedTypesForScope[scope].orEmpty() }
+          .filterNot { it.fqNameSafe in excludedNames }
           .filter {
             val annotation = it.annotationOrNull(annotationFqName)
             annotation != null && scope == annotation.scope(module).fqNameSafe
@@ -358,17 +359,15 @@ internal class BindingModuleGenerator(
         isMultibinding = true
       )
 
-      daggerModuleFiles.map { (file, psiClass) ->
-        val content = daggerModuleContent(
-          scope = scope.asString(),
-          psiClass = psiClass,
-          generatedMethods = generatedMethods
-        )
+      val content = daggerModuleContent(
+        scope = scope.asString(),
+        psiClass = mergedClassKey.clazz,
+        generatedMethods = generatedMethods
+      )
 
-        file.writeText(content)
+      daggerModuleFile.writeText(content)
 
-        GeneratedFile(file, content)
-      }
+      GeneratedFile(daggerModuleFile, content)
     }
   }
 
@@ -565,3 +564,28 @@ private fun Binding.key(module: ModuleDescriptor): String {
 
   return fqName + allArguments
 }
+
+private class MergedClassKey(
+  val scope: FqName,
+  val clazz: KtClassOrObject
+) {
+  private val classFqName = clazz.requireFqName()
+
+  override fun equals(other: Any?): Boolean {
+    if (this === other) return true
+    if (javaClass != other?.javaClass) return false
+
+    other as MergedClassKey
+
+    if (scope != other.scope) return false
+    if (classFqName != other.classFqName) return false
+
+    return true
+  }
+
+  override fun hashCode(): Int {
+    var result = scope.hashCode()
+    result = 31 * result + classFqName.hashCode()
+    return result
+  }
+}


Test patch:
diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/ModuleMergerTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/ModuleMergerTest.kt
index 290b75286..394624466 100644
--- a/compiler/src/test/java/com/squareup/anvil/compiler/ModuleMergerTest.kt
+++ b/compiler/src/test/java/com/squareup/anvil/compiler/ModuleMergerTest.kt
@@ -705,6 +705,41 @@ class ModuleMergerTest(
     }
   }
 
+  @Test fun `contributed modules are only excluded in one module`() {
+    compile(
+      """
+      package com.squareup.test
+      
+      $import
+      import com.squareup.anvil.annotations.ContributesTo
+        
+      @ContributesTo(Any::class, replaces = [DaggerModule2::class])
+      @dagger.Module
+      abstract class DaggerModule1
+      
+      @ContributesTo(Any::class)
+      @dagger.Module
+      abstract class DaggerModule2 
+            
+      $annotation(Any::class)
+      interface ComponentInterface
+            
+      $annotation(
+        scope = Any::class,
+        exclude = [DaggerModule1::class]
+      )
+      interface ComponentInterface2  
+      """
+    ) {
+      val component1 = componentInterface.anyDaggerComponent
+      assertThat(component1.modules.withoutAnvilModule()).containsExactly(daggerModule1.kotlin)
+
+      val component2 =
+        classLoader.loadClass("com.squareup.test.ComponentInterface2").anyDaggerComponent
+      assertThat(component2.modules.withoutAnvilModule()).containsExactly(daggerModule2.kotlin)
+    }
+  }
+
   @Test fun `contributed bindings can be excluded`() {
     compile(
       """
diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModuleGeneratorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModuleGeneratorTest.kt
index 1d32c0534..2bbd71573 100644
--- a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModuleGeneratorTest.kt
+++ b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModuleGeneratorTest.kt
@@ -437,6 +437,49 @@ class BindingModuleGeneratorTest(
     }
   }
 
+  @Test fun `bindings are excluded only in one component`() {
+    compile(
+      """
+      package com.squareup.test
+      
+      import com.squareup.anvil.annotations.ContributesBinding
+      import com.squareup.anvil.annotations.ContributesTo
+      $import
+
+      interface ParentInterface
+      
+      @ContributesBinding(Any::class)
+      interface ContributingInterface : ParentInterface
+            
+      $annotation(Any::class)
+      interface ComponentInterface
+            
+      $annotation(
+        scope = Any::class,
+        exclude = [ContributingInterface::class]
+      )
+      interface ComponentInterface2
+      """
+    ) {
+      val anvilModule1 = if (annotationClass == MergeModules::class) {
+        componentInterface.daggerModule.includes.toList()
+      } else {
+        componentInterface.anyDaggerComponent.modules
+      }.single()
+
+      val anvilModule2 = if (annotationClass == MergeModules::class) {
+        classLoader.loadClass("com.squareup.test.ComponentInterface2")
+          .daggerModule.includes.toList()
+      } else {
+        classLoader.loadClass("com.squareup.test.ComponentInterface2")
+          .anyDaggerComponent.modules
+      }.single()
+
+      assertThat(anvilModule1.java.declaredMethods).hasLength(1)
+      assertThat(anvilModule2.java.declaredMethods).isEmpty()
+    }
+  }
+
   @Test fun `the super type can be evaluated for a delegated class`() {
     compile(
       """