Repository: square/anvil
Issue: ContributesBinding throwing error when 2 implementations are using 2 different qualifiers

Based on docs the code below should work, however I'm seeing "There are multiple contributed bindings with the same bound type." exception when compiling

```
@QualifierOne
@ContributesBinding(scope = SomeScope::class)
class QualifierOneImpl @Inject constructor(): MyInterface

@QualifierTwo
@ContributesBinding(scope = SomeScope::class)
class QualifierTwoImpl @Inject constructor(): MyInterface
```

Might be related to new checks here:
 https://github.com/square/anvil/blob/main/compiler/src/main/java/com/squareup/anvil/compiler/codegen/BindingModuleGenerator.kt#L502-L521



Changed files:
compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModulePriorityTest.kt
compiler/src/main/java/com/squareup/anvil/compiler/codegen/BindingModuleGenerator.kt
compiler/src/main/java/com/squareup/anvil/compiler/Utils.kt

diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/Utils.kt b/compiler/src/main/java/com/squareup/anvil/compiler/Utils.kt
index 75bafce..47e9eeb 100644
--- a/compiler/src/main/java/com/squareup/anvil/compiler/Utils.kt
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/Utils.kt
@@ -259,6 +259,12 @@ internal fun AnnotationDescriptor.requireClass(): ClassDescriptor {
   )
 }
 
+internal fun AnnotationDescriptor.requireFqName(): FqName {
+  return fqName ?: throw AnvilCompilationException(
+    message = "Couldn't find the fqName for $this",
+  )
+}
+
 /**
  * This function should only be used for package names. If the FqName is the root (no package at
  * all), then this function returns an empty string whereas `toString()` would return "<root>". For
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 65ed96a..82487c0 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
@@ -31,6 +31,7 @@ import com.squareup.anvil.compiler.mergeModulesFqName
 import com.squareup.anvil.compiler.mergeSubcomponentFqName
 import com.squareup.anvil.compiler.priority
 import com.squareup.anvil.compiler.replaces
+import com.squareup.anvil.compiler.requireFqName
 import com.squareup.anvil.compiler.safePackageString
 import com.squareup.anvil.compiler.scope
 import com.squareup.kotlinpoet.AnnotationSpec
@@ -53,6 +54,8 @@ import org.jetbrains.kotlin.psi.KtFile
 import org.jetbrains.kotlin.psi.KtProperty
 import org.jetbrains.kotlin.resolve.DescriptorUtils
 import org.jetbrains.kotlin.resolve.constants.ArrayValue
+import org.jetbrains.kotlin.resolve.constants.EnumValue
+import org.jetbrains.kotlin.resolve.constants.KClassValue
 import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
 import org.jetbrains.kotlin.resolve.descriptorUtil.getAllSuperClassifiers
 import org.jetbrains.kotlin.types.KotlinType
@@ -245,11 +248,7 @@ internal class BindingModuleGenerator(
               bindings
             } else {
               bindings
-                .groupBy {
-                  // It's safe to use the FqName here, because we don't allow generic super types.
-                  // If we wouldn't have this limitation, we'd need to use the Kotlin type.
-                  it.boundType.fqNameSafe
-                }
+                .groupBy { binding -> binding.key(module) }
                 .map {
                   it.value.findHighestPriorityBinding()
                 }
@@ -521,3 +520,37 @@ private fun List<Binding>.findHighestPriorityBinding(): Binding {
 }
 
 private val Collection<GeneratedMethod>.specs get() = map { it.spec }
+
+private fun Binding.key(module: ModuleDescriptor): String {
+  // Careful! If we ever decide to support generic types, then we might need to use the
+  // Kotlin type and not just the FqName.
+  val fqName = boundType.fqNameSafe.asString()
+  if (annotation.ignoreQualifier()) return fqName
+
+  // Note that we sort all elements. That's important for a stable string comparison.
+  val allArguments = contributedClass.annotations
+    .filter { it.isQualifier() }
+    .sortedBy { it.requireFqName().hashCode() }
+    .joinToString(separator = "") { annotation ->
+      val annotationFqName = annotation.requireFqName().asString()
+
+      val argumentString = annotation.allValueArguments
+        .toList()
+        .sortedBy { it.first }
+        .map { (name, value) ->
+          val valueString = when (value) {
+            is KClassValue -> value.argumentType(module)
+              .classDescriptorForType().fqNameSafe.asString()
+            is EnumValue -> value.enumEntryName.asString()
+            // String, int, long, ... other primitives.
+            else -> value.toString()
+          }
+
+          name.asString() + valueString
+        }
+
+      annotationFqName + argumentString
+    }
+
+  return fqName + allArguments
+}
diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModulePriorityTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModulePriorityTest.kt
index 201666c..1920535 100644
--- a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModulePriorityTest.kt
+++ b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModulePriorityTest.kt
@@ -189,6 +189,145 @@ class BindingModulePriorityTest(
     }
   }
 
+  @Test fun `bindings can use different qualifiers`() {
+    compile(
+      """
+      package com.squareup.test
+      
+      import com.squareup.anvil.annotations.ContributesBinding
+      import javax.inject.Named
+      $import
+      
+      interface ParentInterface
+      
+      @ContributesBinding(Any::class)
+      @Named("a")
+      interface ContributingInterface : ParentInterface
+      
+      @ContributesBinding(Any::class)
+      @Named("b")
+      interface SecondContributingInterface : ParentInterface
+      
+      $annotation(Any::class)
+      interface ComponentInterface
+      """
+    ) {
+      val bindingMethods = componentInterfaceAnvilModule.declaredMethods
+        .filter { it.returnType == parentInterface }
+
+      assertThat(bindingMethods).hasSize(2)
+
+      assertThat(
+        bindingMethods.singleOrNull { it.parameterTypes.contains(contributingInterface) }
+      ).isNotNull()
+      assertThat(
+        bindingMethods.singleOrNull { it.parameterTypes.contains(secondContributingInterface) }
+      ).isNotNull()
+    }
+  }
+
+  @Test fun `bindings with the same qualifier are duplicate bindings`() {
+    compile(
+      """
+      package com.squareup.test
+      
+      import com.squareup.anvil.annotations.ContributesBinding
+      import javax.inject.Named
+      $import
+      
+      interface ParentInterface
+      
+      @ContributesBinding(Any::class)
+      @Named("a")
+      interface ContributingInterface : ParentInterface
+      
+      @ContributesBinding(Any::class)
+      @Named("a")
+      interface SecondContributingInterface : ParentInterface
+      
+      $annotation(Any::class)
+      interface ComponentInterface
+      """
+    ) {
+      assertThat(exitCode).isEqualTo(COMPILATION_ERROR)
+
+      assertThat(messages).contains(
+        "There are multiple contributed bindings with the same bound type. The bound type is " +
+          "com.squareup.test.ParentInterface. The contributed binding classes are: ["
+      )
+      // Check the contributed bindings separately, we cannot rely on the order in the string.
+      assertThat(messages).contains("com.squareup.test.ContributingInterface")
+      assertThat(messages).contains("com.squareup.test.SecondContributingInterface")
+    }
+  }
+
+  @Test fun `ignored qualifiers can lead to duplicate bindings`() {
+    compile(
+      """
+      package com.squareup.test
+      
+      import com.squareup.anvil.annotations.ContributesBinding
+      import javax.inject.Named
+      $import
+      
+      interface ParentInterface
+      
+      @ContributesBinding(Any::class, ignoreQualifier = true)
+      @Named("a")
+      interface ContributingInterface : ParentInterface
+      
+      @ContributesBinding(Any::class, ignoreQualifier = true)
+      @Named("b")
+      interface SecondContributingInterface : ParentInterface
+      
+      $annotation(Any::class)
+      interface ComponentInterface
+      """
+    ) {
+      assertThat(exitCode).isEqualTo(COMPILATION_ERROR)
+
+      assertThat(messages).contains(
+        "There are multiple contributed bindings with the same bound type. The bound type is " +
+          "com.squareup.test.ParentInterface. The contributed binding classes are: ["
+      )
+      // Check the contributed bindings separately, we cannot rely on the order in the string.
+      assertThat(messages).contains("com.squareup.test.ContributingInterface")
+      assertThat(messages).contains("com.squareup.test.SecondContributingInterface")
+    }
+  }
+
+  @Test fun `bindings with the same qualifier can be excluded`() {
+    compile(
+      """
+      package com.squareup.test
+      
+      import com.squareup.anvil.annotations.ContributesBinding
+      import javax.inject.Named
+      $import
+      
+      interface ParentInterface
+      
+      @ContributesBinding(Any::class)
+      @Named("a")
+      interface ContributingInterface : ParentInterface
+      
+      @ContributesBinding(Any::class)
+      @Named("a")
+      interface SecondContributingInterface : ParentInterface
+      
+      $annotation(Any::class, exclude = [ContributingInterface::class])
+      interface ComponentInterface
+      """
+    ) {
+      val bindingMethod = componentInterfaceAnvilModule.declaredMethods.single()
+
+      with(bindingMethod) {
+        assertThat(returnType).isEqualTo(parentInterface)
+        assertThat(parameterTypes.toList()).containsExactly(secondContributingInterface)
+      }
+    }
+  }
+
   @Test fun `multiple multibindings with the same type are allowed`() {
     compile(
       """