Repository: square/anvil
Issue: BindsMethodValidator does not account for extension functions

When using `generateDaggerFactories`, this results in anvil failing to compile for a @Binds extension function with no parameters (when Dagger passes), and to pass for a @Binds extension function with a parameter (when Dagger fails).

Thanks @efemoney for [raising the extension use-case](https://github.com/square/anvil/pull/654#issuecomment-1324879737).

Changed files:
compiler/src/test/java/com/squareup/anvil/compiler/dagger/BindsMethodValidatorTest.kt
compiler/src/main/java/com/squareup/anvil/compiler/codegen/dagger/BindsMethodValidator.kt

diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/dagger/BindsMethodValidator.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/dagger/BindsMethodValidator.kt
index 40daa08..d0025a0 100644
--- a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/dagger/BindsMethodValidator.kt
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/dagger/BindsMethodValidator.kt
@@ -10,8 +10,10 @@ import com.squareup.anvil.compiler.internal.reference.AnvilCompilationExceptionF
 import com.squareup.anvil.compiler.internal.reference.FunctionReference
 import com.squareup.anvil.compiler.internal.reference.allSuperTypeClassReferences
 import com.squareup.anvil.compiler.internal.reference.classAndInnerClassReferences
+import com.squareup.anvil.compiler.internal.reference.toTypeReference
 import org.jetbrains.kotlin.descriptors.ModuleDescriptor
 import org.jetbrains.kotlin.psi.KtFile
+import org.jetbrains.kotlin.psi.psiUtil.isExtensionDeclaration
 import java.io.File
 
 /**
@@ -56,7 +58,10 @@ internal class BindsMethodValidator : PrivateCodeGenerator() {
       )
     }
 
-    if (function.parameters.size != 1) {
+    val hasSingleBindingParameter =
+      (function.parameters.size == 1 && !function.function.isExtensionDeclaration()) ||
+        (function.parameters.isEmpty() && function.function.isExtensionDeclaration())
+    if (!hasSingleBindingParameter) {
       throw AnvilCompilationExceptionFunctionReference(
         message = "@Binds methods must have exactly one parameter, " +
           "whose type is assignable to the return type",
@@ -64,22 +69,34 @@ internal class BindsMethodValidator : PrivateCodeGenerator() {
       )
     }
 
-    val returnType =
-      function.returnTypeOrNull() ?: throw AnvilCompilationExceptionFunctionReference(
-        message = "@Binds methods must return a value (not void)",
-        functionReference = function
-      )
-    val parameterMatchesReturnType = function.parameters.single()
-      .type()
-      .asClassReference()
-      .allSuperTypeClassReferences(includeSelf = true)
-      .contains(returnType.asClassReference())
+    function.returnTypeOrNull() ?: throw AnvilCompilationExceptionFunctionReference(
+      message = "@Binds methods must return a value (not void)",
+      functionReference = function
+    )
 
-    if (!parameterMatchesReturnType) {
+    if (!function.parameterMatchesReturnType() && !function.receiverMatchesReturnType()) {
       throw AnvilCompilationExceptionFunctionReference(
         message = "@Binds methods' parameter type must be assignable to the return type",
         functionReference = function
       )
     }
   }
+
+  private fun FunctionReference.Psi.parameterMatchesReturnType(): Boolean {
+    return parameters.singleOrNull()
+      ?.type()
+      ?.asClassReference()
+      ?.allSuperTypeClassReferences(includeSelf = true)
+      ?.contains(returnType().asClassReference())
+      ?: false
+  }
+
+  private fun FunctionReference.Psi.receiverMatchesReturnType(): Boolean {
+    return function.receiverTypeReference
+      ?.toTypeReference(declaringClass)
+      ?.asClassReference()
+      ?.allSuperTypeClassReferences(includeSelf = true)
+      ?.contains(returnType().asClassReference())
+      ?: false
+  }
 }
diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/BindsMethodValidatorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/BindsMethodValidatorTest.kt
index f15f5d0..e3955d5 100644
--- a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/BindsMethodValidatorTest.kt
+++ b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/BindsMethodValidatorTest.kt
@@ -6,6 +6,7 @@ import com.squareup.anvil.compiler.WARNINGS_AS_ERRORS
 import com.squareup.anvil.compiler.internal.testing.compileAnvil
 import com.squareup.anvil.compiler.isError
 import com.squareup.anvil.compiler.isFullTestRun
+import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK
 import com.tschuchort.compiletesting.KotlinCompilation.Result
 import org.intellij.lang.annotations.Language
 import org.junit.Test
@@ -137,6 +138,35 @@ class BindsMethodValidatorTest(
     }
   }
 
+  @Test
+  fun `an extension function binding with a parameter fails to compile`() {
+    compile(
+      """
+      package com.squareup.test
+ 
+      import dagger.Binds
+      import dagger.Module
+      import javax.inject.Inject
+
+      class Foo @Inject constructor() : Bar
+      class Hammer @Inject constructor() : Bar
+      interface Bar
+
+      @Module
+      abstract class BarModule {
+        @Binds
+        abstract fun Foo.bindsBar(impl2: Hammer): Bar
+      }
+      """
+    ) {
+      assertThat(exitCode).isError()
+      assertThat(messages).contains(
+        "@Binds methods must have exactly one parameter, " +
+          "whose type is assignable to the return type"
+      )
+    }
+  }
+
   @Test
   fun `a binding with no return type fails to compile`() {
     compile(
@@ -164,14 +194,58 @@ class BindsMethodValidatorTest(
     }
   }
 
+  @Test
+  fun `an extension function binding is valid`() {
+    val moduleResult = compile(
+      """
+      package com.squareup.test
+ 
+      import dagger.Binds
+      import dagger.Module
+      import javax.inject.Inject
+
+      class Foo @Inject constructor() : Bar
+      interface Bar
+
+      @Module
+      abstract class BarModule {
+        @Binds
+        abstract fun Foo.bindsBar(): Bar
+      }
+      """
+    ) {
+      assertThat(exitCode).isEqualTo(OK)
+    }
+
+    compile(
+      """
+      package com.squareup.test
+
+      import dagger.Component
+      import javax.inject.Singleton
+
+      @Component(modules = [BarModule::class])
+      @Singleton
+      interface ComponentInterface {
+        fun bar(): Bar
+      }
+      """,
+      previousCompilationResult = moduleResult,
+      enableDagger = true,
+    ) {
+      assertThat(exitCode).isEqualTo(OK)
+    }
+  }
+
   private fun compile(
     @Language("kotlin") vararg sources: String,
     previousCompilationResult: Result? = null,
+    enableDagger: Boolean = useDagger,
     block: Result.() -> Unit = { }
   ): Result = compileAnvil(
     sources = sources,
-    enableDaggerAnnotationProcessor = useDagger,
-    generateDaggerFactories = !useDagger,
+    enableDaggerAnnotationProcessor = enableDagger,
+    generateDaggerFactories = !enableDagger,
     useIR = USE_IR,
     allWarningsAsErrors = WARNINGS_AS_ERRORS,
     previousCompilationResult = previousCompilationResult,