Repository: square/anvil
Pull request #49 (2020-07-31 01:34:36)

Generate an Anvil Dagger module with contributed bindings
This PR depends on #48 and actually implements some parts in order to make to `@ContributesBinding` work. The bounded type is still required and replacements and exclusions don't work, yet.


Patch:
diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/AnvilCompilationException.kt b/compiler/src/main/java/com/squareup/anvil/compiler/AnvilCompilationException.kt
index 811da113..55772c6b 100644
--- a/compiler/src/main/java/com/squareup/anvil/compiler/AnvilCompilationException.kt
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/AnvilCompilationException.kt
@@ -18,5 +18,5 @@ class AnvilCompilationException(
   ) : this(message, cause = cause, element = classDescriptor.identifier)
 }
 
-private val ClassDescriptor.identifier: PsiElement
-  get() = requireNotNull((findPsi() as? PsiNameIdentifierOwner)?.identifyingElement)
+private val ClassDescriptor.identifier: PsiElement?
+  get() = (findPsi() as? PsiNameIdentifierOwner)?.identifyingElement
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 3f457519..781a8094 100644
--- a/compiler/src/main/java/com/squareup/anvil/compiler/AnvilComponentRegistrar.kt
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/AnvilComponentRegistrar.kt
@@ -1,6 +1,7 @@
 package com.squareup.anvil.compiler
 
 import com.google.auto.service.AutoService
+import com.squareup.anvil.compiler.codegen.BindingModuleGenerator
 import com.squareup.anvil.compiler.codegen.CodeGenerationExtension
 import com.squareup.anvil.compiler.codegen.ContributesBindingGenerator
 import com.squareup.anvil.compiler.codegen.ContributesToGenerator
@@ -31,7 +32,8 @@ class AnvilComponentRegistrar : ComponentRegistrar {
         codeGenDir = sourceGenFolder,
         codeGenerators = listOf(
             ContributesToGenerator(),
-            ContributesBindingGenerator()
+            ContributesBindingGenerator(),
+            BindingModuleGenerator(scanner)
         )
     )
 
diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/ClassScanner.kt b/compiler/src/main/java/com/squareup/anvil/compiler/ClassScanner.kt
index 95b6efbe..8f66ceff 100644
--- a/compiler/src/main/java/com/squareup/anvil/compiler/ClassScanner.kt
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/ClassScanner.kt
@@ -18,9 +18,12 @@ internal class ClassScanner {
    */
   fun findContributedClasses(
     module: ModuleDescriptor,
-    packageName: String = HINT_CONTRIBUTES_PACKAGE_PREFIX
+    packageName: String,
+    annotation: FqName
   ): List<ClassDescriptor> {
-    return cache.getOrPut(packageName) {
+    val key = packageName + annotation.asString()
+
+    return cache.getOrPut(key) {
       val packageDescriptor = module.getPackage(FqName(packageName))
       generateSequence(packageDescriptor.subPackages()) { subPackages ->
         subPackages
@@ -34,7 +37,7 @@ internal class ClassScanner {
           }
           .filterIsInstance<PropertyDescriptor>()
           .map { DescriptorUtils.getClassDescriptorForType(it.type.argumentType()) }
-          .filter { it.annotationOrNull(contributesToFqName) != null }
+          .filter { it.annotationOrNull(annotation) != null }
           .toList()
     }
   }
diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/InterfaceMerger.kt b/compiler/src/main/java/com/squareup/anvil/compiler/InterfaceMerger.kt
index 553584ab..d99b77d3 100644
--- a/compiler/src/main/java/com/squareup/anvil/compiler/InterfaceMerger.kt
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/InterfaceMerger.kt
@@ -42,7 +42,11 @@ internal class InterfaceMerger(
     }
 
     val classes = classScanner
-        .findContributedClasses(thisDescriptor.module)
+        .findContributedClasses(
+            module = thisDescriptor.module,
+            packageName = HINT_CONTRIBUTES_PACKAGE_PREFIX,
+            annotation = contributesToFqName
+        )
         .asSequence()
         .filter {
           DescriptorUtils.isInterface(it) && it.annotationOrNull(daggerModuleFqName) == null
diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/ModuleMerger.kt b/compiler/src/main/java/com/squareup/anvil/compiler/ModuleMerger.kt
index 54363add..bc24ee8d 100644
--- a/compiler/src/main/java/com/squareup/anvil/compiler/ModuleMerger.kt
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/ModuleMerger.kt
@@ -53,7 +53,11 @@ internal class ModuleMerger(
           ?.map { it.toType(codegen) }
 
     val modules = classScanner
-        .findContributedClasses(codegen.descriptor.module)
+        .findContributedClasses(
+            module = codegen.descriptor.module,
+            packageName = HINT_CONTRIBUTES_PACKAGE_PREFIX,
+            annotation = contributesToFqName
+        )
         .asSequence()
         .mapNotNull {
           val contributesAnnotation =
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 d0bbf486..be920ce8 100644
--- a/compiler/src/main/java/com/squareup/anvil/compiler/Utils.kt
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/Utils.kt
@@ -6,6 +6,7 @@ import com.squareup.anvil.annotations.MergeComponent
 import com.squareup.anvil.annotations.MergeSubcomponent
 import com.squareup.anvil.annotations.compat.MergeInterfaces
 import com.squareup.anvil.annotations.compat.MergeModules
+import dagger.Binds
 import dagger.Component
 import dagger.Module
 import dagger.Subcomponent
@@ -35,9 +36,11 @@ internal val contributesBindingFqName = FqName(ContributesBinding::class.java.ca
 internal val daggerComponentFqName = FqName(Component::class.java.canonicalName)
 internal val daggerSubcomponentFqName = FqName(Subcomponent::class.java.canonicalName)
 internal val daggerModuleFqName = FqName(Module::class.java.canonicalName)
+internal val daggerBindsFqName = FqName(Binds::class.java.canonicalName)
 
 internal const val HINT_CONTRIBUTES_PACKAGE_PREFIX = "hint.anvil"
 internal const val HINT_BINDING_PACKAGE_PREFIX = "anvil.hint.binding"
+internal const val MODULE_PACKAGE_PREFIX = "anvil.module"
 
 internal fun ClassDescriptor.annotationOrNull(
   annotationFqName: FqName,
@@ -94,3 +97,11 @@ internal fun AnnotationDescriptor.scope(module: ModuleDescriptor): ClassDescript
           .argumentType()
   )
 }
+
+internal fun AnnotationDescriptor.boundType(module: ModuleDescriptor): ClassDescriptor {
+  val kClassValue = requireNotNull(getAnnotationValue("boundType")) as KClassValue
+  return DescriptorUtils.getClassDescriptorForType(
+      kClassValue.getType(module)
+          .argumentType()
+  )
+}
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
new file mode 100644
index 00000000..8f80ffaf
--- /dev/null
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/BindingModuleGenerator.kt
@@ -0,0 +1,236 @@
+package com.squareup.anvil.compiler.codegen
+
+import com.squareup.anvil.compiler.AnvilCompilationException
+import com.squareup.anvil.compiler.ClassScanner
+import com.squareup.anvil.compiler.HINT_BINDING_PACKAGE_PREFIX
+import com.squareup.anvil.compiler.MODULE_PACKAGE_PREFIX
+import com.squareup.anvil.compiler.annotation
+import com.squareup.anvil.compiler.annotationOrNull
+import com.squareup.anvil.compiler.boundType
+import com.squareup.anvil.compiler.codegen.CodeGenerator.GeneratedFile
+import com.squareup.anvil.compiler.contributesBindingFqName
+import com.squareup.anvil.compiler.contributesToFqName
+import com.squareup.anvil.compiler.daggerBindsFqName
+import com.squareup.anvil.compiler.daggerModuleFqName
+import com.squareup.anvil.compiler.mergeComponentFqName
+import com.squareup.anvil.compiler.mergeModulesFqName
+import com.squareup.anvil.compiler.mergeSubcomponentFqName
+import com.squareup.anvil.compiler.scope
+import org.jetbrains.kotlin.descriptors.ClassDescriptor
+import org.jetbrains.kotlin.descriptors.ModuleDescriptor
+import org.jetbrains.kotlin.descriptors.resolveClassByFqName
+import org.jetbrains.kotlin.incremental.KotlinLookupLocation
+import org.jetbrains.kotlin.incremental.components.NoLookupLocation
+import org.jetbrains.kotlin.name.FqName
+import org.jetbrains.kotlin.psi.KtClassLiteralExpression
+import org.jetbrains.kotlin.psi.KtClassOrObject
+import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
+import org.jetbrains.kotlin.psi.KtFile
+import org.jetbrains.kotlin.psi.KtProperty
+import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
+import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
+import org.jetbrains.kotlin.resolve.descriptorUtil.getAllSuperClassifiers
+import java.io.File
+import java.util.Locale.US
+
+private val supportedFqNames = listOf(
+    mergeComponentFqName,
+    mergeSubcomponentFqName,
+    mergeModulesFqName
+)
+
+internal class BindingModuleGenerator(
+  private val classScanner: ClassScanner
+) : CodeGenerator {
+
+  // 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 contributedBindingClasses = mutableListOf<FqName>()
+
+  override fun generateCode(
+    codeGenDir: File,
+    module: ModuleDescriptor,
+    projectFiles: Collection<KtFile>
+  ): Collection<GeneratedFile> {
+    // Even though we support multiple rounds the Kotlin compiler let's us generate code only once.
+    // It's possible that we use the @ContributesBinding annotation in the module in which we
+    // merge components. Remember for which classes a hint was generated and generate a @Binds
+    // method for them later.
+    findContributedBindingClasses(projectFiles)
+
+    // Generate a Dagger module for each @MergeComponent and friends.
+    return projectFiles.asSequence()
+        .flatMap { it.classesAndInnerClasses() }
+        .filter { psiClass -> supportedFqNames.any { psiClass.hasAnnotation(it) } }
+        .map { psiClass ->
+          val classDescriptor =
+            module.resolveClassByFqName(psiClass.requireFqName(), KotlinLookupLocation(psiClass))
+                ?: throw AnvilCompilationException(
+                    "Couldn't resolve class for PSI element.", element = psiClass
+                )
+
+          // The annotation must be present due to the filter above.
+          val scope = supportedFqNames
+              .mapNotNull {
+                classDescriptor.annotationOrNull(it)
+                    ?.scope(module)
+              }
+              .first().fqNameSafe
+
+          val packageName = generatePackageName(psiClass)
+          val className = generateClassName(psiClass)
+
+          val directory = File(codeGenDir, packageName.replace('.', File.separatorChar))
+          val file = File(directory, "$className.kt")
+          check(file.parentFile.exists() || file.parentFile.mkdirs()) {
+            "Could not generate package directory: ${file.parentFile}"
+          }
+
+          // We cheat and don't actually write the content to the file. We write the content in the
+          // flush() method when we collected all binding methods to avoid two writes.
+          check(file.exists() || file.createNewFile()) {
+            "Could not create new file: $file"
+          }
+
+          val content = daggerModuleContent(
+              scope = scope.asString(),
+              psiClass = psiClass,
+              bindingMethods = emptyList()
+          )
+
+          mergedScopes[scope] = mergedScopes.getValue(scope)
+              .apply { this += file to psiClass }
+
+          GeneratedFile(file, content)
+        }
+        .toList()
+  }
+
+  @OptIn(ExperimentalStdlibApi::class)
+  override fun flush(
+    codeGenDir: File,
+    module: ModuleDescriptor
+  ) {
+    mergedScopes.forEach { (scope, daggerModuleFiles) ->
+      val contributedBindingsThisModule = contributedBindingClasses
+          .mapNotNull { clazz ->
+            module.resolveClassByFqName(clazz, NoLookupLocation.FROM_BACKEND)
+          }
+      val contributedBindingsDependencies = classScanner.findContributedClasses(
+          module,
+          packageName = HINT_BINDING_PACKAGE_PREFIX,
+          annotation = contributesBindingFqName
+      )
+
+      val bindingMethods = (contributedBindingsThisModule + contributedBindingsDependencies)
+          .filter {
+            val annotation = it.annotationOrNull(contributesBindingFqName)
+            annotation != null && scope == annotation.scope(module).fqNameSafe
+          }
+          .map { contributedClass ->
+            val annotation = contributedClass.annotation(contributesBindingFqName)
+            val boundType = annotation.boundType(module)
+
+            checkExtendsBoundType(type = contributedClass, boundType = boundType)
+
+            val concreteType = contributedClass.fqNameSafe
+            val methodName = concreteType
+                .asString()
+                .split(".")
+                .joinToString(separator = "", prefix = "bind") { it.capitalize(US) }
+
+            val paramName = concreteType.shortName()
+                .asString()
+                .decapitalize(US)
+
+            """
+              |
+              |  @$daggerBindsFqName abstract fun $methodName(
+              |    $paramName: $concreteType
+              |  ): ${boundType.fqNameSafe}
+            """.trimMargin()
+          }
+
+      daggerModuleFiles.forEach { (file, psiClass) ->
+        file.writeText(
+            daggerModuleContent(
+                scope = scope.asString(),
+                psiClass = psiClass,
+                bindingMethods = bindingMethods
+            )
+        )
+      }
+    }
+  }
+
+  private fun findContributedBindingClasses(projectFiles: Collection<KtFile>) {
+    contributedBindingClasses += projectFiles
+        .filter {
+          it.packageFqName.asString()
+              .startsWith(HINT_BINDING_PACKAGE_PREFIX)
+        }
+        .flatMap {
+          it.findChildrenByClass(KtProperty::class.java)
+              .toList()
+        }
+        .mapNotNull { ktProperty ->
+          ((ktProperty.initializer as? KtClassLiteralExpression)
+              ?.firstChild as? KtDotQualifiedExpression)
+              ?.text
+              ?.let { FqName(it) }
+        }
+  }
+
+  private fun checkExtendsBoundType(
+    type: ClassDescriptor,
+    boundType: ClassDescriptor
+  ) {
+    val boundFqName = boundType.fqNameSafe
+    val hasSuperType = type.getAllSuperClassifiers()
+        .any { it.fqNameSafe == boundFqName }
+
+    if (!hasSuperType) {
+      throw AnvilCompilationException(
+          classDescriptor = type,
+          message = "${type.fqNameSafe} contributes a binding for $boundFqName, but doesn't " +
+              "extend this type."
+      )
+    }
+  }
+
+  private fun generateClassName(psiClass: KtClassOrObject): String =
+    psiClass.parentsWithSelf
+        .filterIsInstance<KtClassOrObject>()
+        .toList()
+        .reversed()
+        .joinToString(separator = "", postfix = "AnvilModule") {
+          it.requireFqName()
+              .shortName()
+              .asString()
+        }
+
+  private fun generatePackageName(psiClass: KtClassOrObject): String =
+    "$MODULE_PACKAGE_PREFIX.${psiClass.containingKtFile.packageFqName}"
+
+  private fun daggerModuleContent(
+    scope: String,
+    psiClass: KtClassOrObject,
+    bindingMethods: List<String>
+  ): String {
+    val packageName = generatePackageName(psiClass)
+    val className = generateClassName(psiClass)
+
+    return """
+      package $packageName
+
+      @$daggerModuleFqName
+      @$contributesToFqName($scope::class)
+      abstract class $className {
+      ${bindingMethods.joinToString(separator = "\n")}
+      }
+    """.trimIndent()
+  }
+}
diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesBindingGenerator.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesBindingGenerator.kt
index 53cbe173..25342c56 100644
--- a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesBindingGenerator.kt
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesBindingGenerator.kt
@@ -42,7 +42,7 @@ internal class ContributesBindingGenerator : CodeGenerator {
           val directory = File(codeGenDir, generatedPackage.replace('.', File.separatorChar))
           val file = File(directory, "${className.replace('.', '_')}.kt")
           check(file.parentFile.exists() || file.parentFile.mkdirs()) {
-            "Could not generate package directory: $this"
+            "Could not generate package directory: ${file.parentFile}"
           }
 
           val content = """


Test patch:
diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesToGenerator.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesToGenerator.kt
index bd2c6dbd..cba4ff67 100644
--- a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesToGenerator.kt
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesToGenerator.kt
@@ -54,7 +54,7 @@ internal class ContributesToGenerator : CodeGenerator {
           val directory = File(codeGenDir, generatedPackage.replace('.', File.separatorChar))
           val file = File(directory, "${className.replace('.', '_')}.kt")
           check(file.parentFile.exists() || file.parentFile.mkdirs()) {
-            "Could not generate package directory: $this"
+            "Could not generate package directory: ${file.parentFile}"
           }
 
           val content = """
diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/MergeModulesTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/MergeModulesTest.kt
index 071c2f79..e14e9efb 100644
--- a/compiler/src/test/java/com/squareup/anvil/compiler/MergeModulesTest.kt
+++ b/compiler/src/test/java/com/squareup/anvil/compiler/MergeModulesTest.kt
@@ -17,7 +17,7 @@ class MergeModulesTest {
         class DaggerModule1
     """
     ) {
-      assertThat(daggerModule1.daggerModule.includes).isEmpty()
+      assertThat(daggerModule1.daggerModule.includes.withoutAnvilModule()).isEmpty()
       assertThat(daggerModule1.daggerModule.subcomponents).isEmpty()
     }
   }
@@ -39,7 +39,7 @@ class MergeModulesTest {
         class DaggerModule1
     """
     ) {
-      assertThat(daggerModule1.daggerModule.includes.toList())
+      assertThat(daggerModule1.daggerModule.includes.withoutAnvilModule())
           .containsExactly(Boolean::class, Int::class)
     }
   }
@@ -66,7 +66,7 @@ class MergeModulesTest {
     """
     ) {
       val module = daggerModule1.daggerModule
-      assertThat(module.includes.toList()).containsExactly(Boolean::class, Int::class)
+      assertThat(module.includes.withoutAnvilModule()).containsExactly(Boolean::class, Int::class)
       assertThat(module.subcomponents.toList()).containsExactly(Boolean::class, Int::class)
     }
   }
@@ -105,7 +105,8 @@ class MergeModulesTest {
         class DaggerModule1
     """
     ) {
-      assertThat(daggerModule1.daggerModule.includes.toList()).containsExactly(daggerModule2.kotlin)
+      assertThat(daggerModule1.daggerModule.includes.withoutAnvilModule())
+          .containsExactly(daggerModule2.kotlin)
     }
   }
 
@@ -131,7 +132,7 @@ class MergeModulesTest {
         class DaggerModule1
     """
     ) {
-      assertThat(daggerModule1.daggerModule.includes.toList())
+      assertThat(daggerModule1.daggerModule.includes.withoutAnvilModule())
           .containsExactly(daggerModule2.kotlin, Int::class, Boolean::class)
     }
   }
@@ -180,7 +181,8 @@ class MergeModulesTest {
         class DaggerModule1
     """
     ) {
-      assertThat(daggerModule1.daggerModule.includes.toList()).containsExactly(daggerModule3.kotlin)
+      assertThat(daggerModule1.daggerModule.includes.withoutAnvilModule())
+          .containsExactly(daggerModule3.kotlin)
     }
   }
 
@@ -238,7 +240,7 @@ class MergeModulesTest {
         class DaggerModule1
     """
     ) {
-      assertThat(daggerModule1.daggerModule.includes.toList())
+      assertThat(daggerModule1.daggerModule.includes.withoutAnvilModule())
           .containsExactly(daggerModule2.kotlin, daggerModule3.kotlin)
     }
   }
@@ -268,7 +270,8 @@ class MergeModulesTest {
         class DaggerModule1
     """
     ) {
-      assertThat(daggerModule1.daggerModule.includes.toList()).containsExactly(daggerModule3.kotlin)
+      assertThat(daggerModule1.daggerModule.includes.withoutAnvilModule())
+          .containsExactly(daggerModule3.kotlin)
     }
   }
 
@@ -295,9 +298,9 @@ class MergeModulesTest {
         class DaggerModule2
     """
     ) {
-      assertThat(daggerModule1.daggerModule.includes.toList())
+      assertThat(daggerModule1.daggerModule.includes.withoutAnvilModule())
           .containsExactly(daggerModule3.kotlin)
-      assertThat(daggerModule2.daggerModule.includes.toList())
+      assertThat(daggerModule2.daggerModule.includes.withoutAnvilModule())
           .containsExactly(daggerModule4.kotlin)
     }
   }
@@ -348,7 +351,7 @@ class MergeModulesTest {
     ) {
       val innerModule = classLoader.loadClass("com.squareup.test.DaggerModule1\$InnerModule")
 
-      assertThat(daggerModule1.daggerModule.includes.toList())
+      assertThat(daggerModule1.daggerModule.includes.withoutAnvilModule())
           .containsExactly(innerModule.kotlin)
     }
   }
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 62ad181a..cb89b1d2 100644
--- a/compiler/src/test/java/com/squareup/anvil/compiler/ModuleMergerTest.kt
+++ b/compiler/src/test/java/com/squareup/anvil/compiler/ModuleMergerTest.kt
@@ -39,7 +39,7 @@ class ModuleMergerTest(
         interface ComponentInterface
     """
     ) {
-      assertThat(componentInterface.anyDaggerComponent.modules).isEmpty()
+      assertThat(componentInterface.anyDaggerComponent.modules.withoutAnvilModule()).isEmpty()
 
       if (annotationClass == MergeComponent::class) {
         assertThat(componentInterface.daggerComponent.dependencies).isEmpty()
@@ -65,7 +65,7 @@ class ModuleMergerTest(
     """
     ) {
       val component = componentInterface.anyDaggerComponent
-      assertThat(component.modules).containsExactly(Boolean::class, Int::class)
+      assertThat(component.modules.withoutAnvilModule()).containsExactly(Boolean::class, Int::class)
     }
   }
 
@@ -93,7 +93,7 @@ class ModuleMergerTest(
     """
     ) {
       val component = componentInterface.daggerComponent
-      assertThat(component.modules.toList()).containsExactly(Boolean::class, Int::class)
+      assertThat(component.modules.withoutAnvilModule()).containsExactly(Boolean::class, Int::class)
       assertThat(component.dependencies.toList()).containsExactly(Boolean::class, Int::class)
     }
   }
@@ -140,7 +140,7 @@ class ModuleMergerTest(
     """
     ) {
       val component = componentInterface.anyDaggerComponent
-      assertThat(component.modules).containsExactly(daggerModule1.kotlin)
+      assertThat(component.modules.withoutAnvilModule()).containsExactly(daggerModule1.kotlin)
     }
   }
 
@@ -161,7 +161,7 @@ class ModuleMergerTest(
     """
     ) {
       val component = componentInterface.anyDaggerComponent
-      assertThat(component.modules).containsExactly(daggerModule1.kotlin)
+      assertThat(component.modules.withoutAnvilModule()).containsExactly(daggerModule1.kotlin)
     }
   }
 
@@ -188,7 +188,7 @@ class ModuleMergerTest(
     """
     ) {
       val component = componentInterface.anyDaggerComponent
-      assertThat(component.modules)
+      assertThat(component.modules.withoutAnvilModule())
           .containsExactly(daggerModule1.kotlin, Int::class, Boolean::class)
     }
   }
@@ -238,7 +238,7 @@ class ModuleMergerTest(
     """
     ) {
       val component = componentInterface.anyDaggerComponent
-      assertThat(component.modules).containsExactly(daggerModule2.kotlin)
+      assertThat(component.modules.withoutAnvilModule()).containsExactly(daggerModule2.kotlin)
     }
   }
 
@@ -297,7 +297,7 @@ class ModuleMergerTest(
     """
     ) {
       val component = componentInterface.anyDaggerComponent
-      assertThat(component.modules)
+      assertThat(component.modules.withoutAnvilModule())
           .containsExactly(daggerModule2.kotlin, daggerModule1.kotlin)
     }
   }
@@ -328,7 +328,7 @@ class ModuleMergerTest(
     """
     ) {
       val component = componentInterface.anyDaggerComponent
-      assertThat(component.modules).containsExactly(daggerModule2.kotlin)
+      assertThat(component.modules.withoutAnvilModule()).containsExactly(daggerModule2.kotlin)
     }
   }
 
@@ -355,9 +355,9 @@ class ModuleMergerTest(
         interface SubcomponentInterface
     """
     ) {
-      assertThat(componentInterface.anyDaggerComponent.modules)
+      assertThat(componentInterface.anyDaggerComponent.modules.withoutAnvilModule())
           .containsExactly(daggerModule1.kotlin)
-      assertThat(subcomponentInterface.anyDaggerComponent.modules)
+      assertThat(subcomponentInterface.anyDaggerComponent.modules.withoutAnvilModule())
           .containsExactly(daggerModule2.kotlin)
     }
   }
@@ -388,9 +388,9 @@ class ModuleMergerTest(
         interface SubcomponentInterface
     """
     ) {
-      assertThat(componentInterface.daggerComponent.modules.toList())
+      assertThat(componentInterface.daggerComponent.modules.withoutAnvilModule())
           .containsExactly(daggerModule1.kotlin)
-      assertThat(subcomponentInterface.daggerSubcomponent.modules.toList())
+      assertThat(subcomponentInterface.daggerSubcomponent.modules.withoutAnvilModule())
           .containsExactly(daggerModule2.kotlin)
     }
   }
@@ -441,7 +441,7 @@ class ModuleMergerTest(
         interface SubcomponentInterface
     """
     ) {
-      assertThat(subcomponentInterface.anyDaggerComponent.modules.toList())
+      assertThat(subcomponentInterface.anyDaggerComponent.modules.withoutAnvilModule())
           .containsExactly(innerModule.kotlin)
     }
   }
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 a62e23a4..5b4f0dd2 100644
--- a/compiler/src/test/java/com/squareup/anvil/compiler/TestUtils.kt
+++ b/compiler/src/test/java/com/squareup/anvil/compiler/TestUtils.kt
@@ -59,9 +59,17 @@ internal val Result.parentInterface: Class<*>
 internal val Result.componentInterface: Class<*>
   get() = classLoader.loadClass("com.squareup.test.ComponentInterface")
 
+internal val Result.componentInterfaceAnvilModule: Class<*>
+  get() = classLoader
+      .loadClass("$MODULE_PACKAGE_PREFIX.com.squareup.test.ComponentInterfaceAnvilModule")
+
 internal val Result.subcomponentInterface: Class<*>
   get() = classLoader.loadClass("com.squareup.test.SubcomponentInterface")
 
+internal val Result.subcomponentInterfaceAnvilModule: Class<*>
+  get() = classLoader
+      .loadClass("$MODULE_PACKAGE_PREFIX.com.squareup.test.SubcomponentInterfaceAnvilModule")
+
 internal val Result.daggerModule1: Class<*>
   get() = classLoader.loadClass("com.squareup.test.DaggerModule1")
 
@@ -143,3 +151,7 @@ internal infix fun Class<*>.extends(other: Class<*>): Boolean = other.isAssignab
 internal fun assumeMergeComponent(annotationClass: KClass<*>) {
   assumeTrue(annotationClass == MergeComponent::class)
 }
+
+internal fun Array<KClass<*>>.withoutAnvilModule(): List<KClass<*>> = toList().withoutAnvilModule()
+internal fun Collection<KClass<*>>.withoutAnvilModule(): List<KClass<*>> =
+  filterNot { it.qualifiedName!!.startsWith(MODULE_PACKAGE_PREFIX) }
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
new file mode 100644
index 00000000..0abb9a4b
--- /dev/null
+++ b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModuleGeneratorTest.kt
@@ -0,0 +1,205 @@
+package com.squareup.anvil.compiler.codegen
+
+import com.google.common.truth.Truth.assertThat
+import com.squareup.anvil.annotations.MergeComponent
+import com.squareup.anvil.annotations.MergeSubcomponent
+import com.squareup.anvil.annotations.compat.MergeModules
+import com.squareup.anvil.compiler.AnyDaggerComponent
+import com.squareup.anvil.compiler.MODULE_PACKAGE_PREFIX
+import com.squareup.anvil.compiler.anyDaggerComponent
+import com.squareup.anvil.compiler.compile
+import com.squareup.anvil.compiler.componentInterface
+import com.squareup.anvil.compiler.componentInterfaceAnvilModule
+import com.squareup.anvil.compiler.contributingInterface
+import com.squareup.anvil.compiler.daggerModule
+import com.squareup.anvil.compiler.daggerModule1
+import com.squareup.anvil.compiler.parentInterface
+import com.squareup.anvil.compiler.subcomponentInterface
+import com.squareup.anvil.compiler.subcomponentInterfaceAnvilModule
+import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.COMPILATION_ERROR
+import dagger.Binds
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
+import java.lang.reflect.Modifier.isAbstract
+import kotlin.reflect.KClass
+
+@RunWith(Parameterized::class)
+class BindingModuleGeneratorTest(
+  private val annotationClass: KClass<*>
+) {
+
+  private val annotation = "@${annotationClass.simpleName}"
+  private val import = "import ${annotationClass.java.canonicalName}"
+
+  companion object {
+    @Parameters(name = "{0}")
+    @JvmStatic fun annotationClasses(): Collection<Any> {
+      return listOf(MergeComponent::class, MergeSubcomponent::class, MergeModules::class)
+    }
+  }
+
+  @Test fun `a Dagger module is generated for a merged class and added to the component`() {
+    compile(
+        """
+        package com.squareup.test
+        
+        $import
+        
+        $annotation(Any::class)
+        interface ComponentInterface
+    """
+    ) {
+      val modules = if (annotationClass == MergeModules::class) {
+        componentInterface.daggerModule.includes.toList()
+      } else {
+        componentInterface.anyDaggerComponent.modules
+      }
+      assertThat(modules).containsExactly(componentInterfaceAnvilModule.kotlin)
+    }
+  }
+
+  @Test fun `the Anvil module is merged with other modules`() {
+    compile(
+        """
+        package com.squareup.test
+        
+        import com.squareup.anvil.annotations.ContributesTo
+        $import
+        
+        @ContributesTo(Any::class)
+        @dagger.Module
+        abstract class DaggerModule1
+        
+        $annotation(Any::class)
+        interface ComponentInterface
+    """
+    ) {
+      val modules = if (annotationClass == MergeModules::class) {
+        componentInterface.daggerModule.includes.toList()
+      } else {
+        componentInterface.anyDaggerComponent.modules
+      }
+      assertThat(modules)
+          .containsExactly(componentInterfaceAnvilModule.kotlin, daggerModule1.kotlin)
+    }
+  }
+
+  @Test fun `there is an Anvil module for each component`() {
+    compile(
+        """
+        package com.squareup.test
+
+        import com.squareup.anvil.annotations.ContributesTo
+        $import
+
+        $annotation(Any::class)
+        interface ComponentInterface
+
+        $annotation(Unit::class)
+        interface SubcomponentInterface
+    """
+    ) {
+      if (annotationClass == MergeModules::class) {
+        assertThat(componentInterface.daggerModule.includes.toList())
+            .containsExactly(componentInterfaceAnvilModule.kotlin)
+        assertThat(subcomponentInterface.daggerModule.includes.toList())
+            .containsExactly(subcomponentInterfaceAnvilModule.kotlin)
+      } else {
+        assertThat(componentInterface.anyDaggerComponent.modules)
+            .containsExactly(componentInterfaceAnvilModule.kotlin)
+        assertThat(subcomponentInterface.anyDaggerComponent.modules)
+            .containsExactly(subcomponentInterfaceAnvilModule.kotlin)
+      }
+    }
+  }
+
+  @Test fun `Anvil modules for inner classes are generated`() {
+    compile(
+        """
+        package com.squareup.test
+
+        import com.squareup.anvil.annotations.ContributesTo
+        $import
+        
+        class SomeClass {
+          $annotation(Any::class)
+          interface ComponentInterface
+        }
+    """
+    ) {
+      val className =
+        "$MODULE_PACKAGE_PREFIX.com.squareup.test.SomeClassComponentInterfaceAnvilModule"
+      assertThat(classLoader.loadClass(className)).isNotNull()
+    }
+  }
+
+  @Test fun `the Dagger binding method is generated`() {
+    compile(
+        """
+        package com.squareup.test
+        
+        import com.squareup.anvil.annotations.ContributesBinding
+        $import
+
+        interface ParentInterface
+        
+        interface Middle : ParentInterface
+
+        @ContributesBinding(Any::class, ParentInterface::class)
+        interface ContributingInterface : Middle
+        
+        $annotation(Any::class)
+        interface ComponentInterface
+    """
+    ) {
+      val modules = if (annotationClass == MergeModules::class) {
+        componentInterface.daggerModule.includes.toList()
+      } else {
+        componentInterface.anyDaggerComponent.modules
+      }
+      assertThat(modules).containsExactly(componentInterfaceAnvilModule.kotlin)
+
+      val methods = modules.first().java.declaredMethods
+      assertThat(methods).hasLength(1)
+
+      with(methods[0]) {
+        assertThat(returnType).isEqualTo(parentInterface)
+        assertThat(parameterTypes.toList()).containsExactly(contributingInterface)
+        assertThat(isAbstract(modifiers)).isTrue()
+        assertThat(isAnnotationPresent(Binds::class.java)).isTrue()
+      }
+    }
+  }
+
+  @Test fun `the contributed binding class must extend the bound type`() {
+    compile(
+        """
+        package com.squareup.test
+        
+        import com.squareup.anvil.annotations.ContributesBinding
+        $import
+
+        interface ParentInterface
+
+        @ContributesBinding(Any::class, ParentInterface::class)
+        interface ContributingInterface
+        
+        $annotation(Any::class)
+        interface ComponentInterface
+    """
+    ) {
+      assertThat(exitCode).isEqualTo(COMPILATION_ERROR)
+
+      assertThat(messages).contains("Source.kt: (9, 11)")
+      assertThat(messages).contains(
+          "com.squareup.test.ContributingInterface contributes a binding for " +
+              "com.squareup.test.ParentInterface, but doesn't extend this type."
+      )
+    }
+  }
+
+  private val Class<*>.anyDaggerComponent: AnyDaggerComponent
+    get() = anyDaggerComponent(annotationClass)
+}
diff --git a/integration-tests/tests/src/test/java/com/squareup/anvil/test/MergeComponentTest.kt b/integration-tests/tests/src/test/java/com/squareup/anvil/test/MergeComponentTest.kt
index 29e0a8ec..970d5505 100644
--- a/integration-tests/tests/src/test/java/com/squareup/anvil/test/MergeComponentTest.kt
+++ b/integration-tests/tests/src/test/java/com/squareup/anvil/test/MergeComponentTest.kt
@@ -1,6 +1,6 @@
 package com.squareup.anvil.test
 
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
 import com.squareup.anvil.annotations.MergeComponent
 import com.squareup.anvil.annotations.MergeSubcomponent
 import dagger.Component
@@ -11,28 +11,22 @@ class MergeComponentTest {
 
   @Test fun `component merges modules and interfaces`() {
     val annotation = AppComponent::class.java.getAnnotation(Component::class.java)!!
-    Truth.assertThat(annotation.modules.toList())
+    assertThat(annotation.modules.withoutAnvilModule())
         .containsExactly(AppModule1::class, AppModule2::class)
 
-    Truth.assertThat(AppComponent::class extends AppComponentInterface::class)
-        .isTrue()
-    Truth.assertThat(AppComponent::class extends AppModule2::class)
-        .isFalse()
-    Truth.assertThat(AppComponent::class extends SubModule2::class)
-        .isFalse()
+    assertThat(AppComponent::class extends AppComponentInterface::class).isTrue()
+    assertThat(AppComponent::class extends AppModule2::class).isFalse()
+    assertThat(AppComponent::class extends SubModule2::class).isFalse()
   }
 
   @Test fun `subcomponent merges modules and interfaces`() {
     val annotation = SubComponent::class.java.getAnnotation(Subcomponent::class.java)!!
-    Truth.assertThat(annotation.modules.toList())
+    assertThat(annotation.modules.withoutAnvilModule())
         .containsExactly(SubModule1::class, SubModule2::class)
 
-    Truth.assertThat(SubComponent::class extends SubComponentInterface::class)
-        .isTrue()
-    Truth.assertThat(AppComponent::class extends AppModule2::class)
-        .isFalse()
-    Truth.assertThat(AppComponent::class extends SubModule2::class)
-        .isFalse()
+    assertThat(SubComponent::class extends SubComponentInterface::class).isTrue()
+    assertThat(AppComponent::class extends AppModule2::class).isFalse()
+    assertThat(AppComponent::class extends SubModule2::class).isFalse()
   }
 
   @MergeComponent(AppScope::class)
diff --git a/integration-tests/tests/src/test/java/com/squareup/anvil/test/MergeModulesTest.kt b/integration-tests/tests/src/test/java/com/squareup/anvil/test/MergeModulesTest.kt
index db9cd405..2dbc8f33 100644
--- a/integration-tests/tests/src/test/java/com/squareup/anvil/test/MergeModulesTest.kt
+++ b/integration-tests/tests/src/test/java/com/squareup/anvil/test/MergeModulesTest.kt
@@ -9,14 +9,16 @@ class MergeModulesTest {
 
   @Test fun `contributed modules are merged app scope`() {
     val annotation = CompositeAppModule::class.java.getAnnotation(Module::class.java)!!
-    assertThat(annotation.includes.toList()).containsExactly(AppModule1::class, AppModule2::class)
+    assertThat(annotation.includes.withoutAnvilModule())
+        .containsExactly(AppModule1::class, AppModule2::class)
     assertThat(annotation.includes.toList()).doesNotContain(SubModule1::class)
     assertThat(annotation.includes.toList()).doesNotContain(SubModule2::class)
   }
 
   @Test fun `contributed modules are merge sub scope`() {
     val annotation = CompositeSubModule::class.java.getAnnotation(Module::class.java)!!
-    assertThat(annotation.includes.toList()).containsExactly(SubModule1::class, SubModule2::class)
+    assertThat(annotation.includes.withoutAnvilModule())
+        .containsExactly(SubModule1::class, SubModule2::class)
     assertThat(annotation.includes.toList()).doesNotContain(AppModule1::class)
     assertThat(annotation.includes.toList()).doesNotContain(AppModule2::class)
   }
diff --git a/integration-tests/tests/src/test/java/com/squareup/anvil/test/Utils.kt b/integration-tests/tests/src/test/java/com/squareup/anvil/test/Utils.kt
index 2239930e..a339f2d8 100644
--- a/integration-tests/tests/src/test/java/com/squareup/anvil/test/Utils.kt
+++ b/integration-tests/tests/src/test/java/com/squareup/anvil/test/Utils.kt
@@ -4,3 +4,7 @@ import kotlin.reflect.KClass
 
 internal infix fun KClass<*>.extends(other: KClass<*>): Boolean =
   other.java.isAssignableFrom(this.java)
+
+internal fun Array<KClass<*>>.withoutAnvilModule(): List<KClass<*>> = toList().withoutAnvilModule()
+internal fun Collection<KClass<*>>.withoutAnvilModule(): List<KClass<*>> =
+  filterNot { it.qualifiedName!!.startsWith("anvil.module") }