Repository: square/anvil (kotlin)

Issue:
`Provider<Lazy<...>>` is not handled correctly
Given this example:

```kotlin
class Example @Inject constructor(
  private val injected: Provider<Lazy<String>>
)
```

Anvil generates this

```kotlin
public class Example_Factory(
  private val param0: Provider<@JvmSuppressWildcards Lazy<String>>
) : Factory<Example> {
```

This isn't correct though, as dagger components will try to just pass it `Provider<String>` and expect the factory to handle lazy-ing it.

```java
public final class Example_Factory implements Factory<Example> {
  private final Provider<String> stringProvider;

  public Example_Factory(
      Provider<String> stringProvider) {
    this.stringProvider = stringProvider;
  }

  @Override
  public Example get() {
    return newInstance(ProviderOfLazy.create(stringProvider));
  }

  public static Example_Factory create(
      Provider<String> stringProvider) {
    return new Example_Factory(stringProvider);
  }

  public static Example newInstance(
      Provider<Lazy<String>> stringProvider) {
    return new Example(stringProvider);
  }
}
```

Pull request:
Fixes #344

Patch:
diff --git a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/FqName.kt b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/FqName.kt
index e0a53d334..8958a9249 100644
--- a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/FqName.kt
+++ b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/FqName.kt
@@ -1,5 +1,6 @@
 package com.squareup.anvil.compiler.internal
 
+import dagger.Lazy
 import dagger.Provides
 import javax.inject.Inject
 import javax.inject.Qualifier
@@ -8,4 +9,5 @@ internal val jvmSuppressWildcardsFqName = JvmSuppressWildcards::class.fqName
 internal val publishedApiFqName = PublishedApi::class.fqName
 internal val qualifierFqName = Qualifier::class.fqName
 internal val daggerProvidesFqName = Provides::class.fqName
+internal val daggerLazyFqName = Lazy::class.fqName
 internal val injectFqName = Inject::class.fqName
diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/KotlinPoetUtils.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/KotlinPoetUtils.kt
index af3002a1f..dd0f94b03 100644
--- a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/KotlinPoetUtils.kt
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/KotlinPoetUtils.kt
@@ -15,10 +15,12 @@ import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
 import com.squareup.kotlinpoet.TypeName
 import com.squareup.kotlinpoet.asClassName
 import dagger.Lazy
+import dagger.internal.ProviderOfLazy
 import org.jetbrains.kotlin.descriptors.ModuleDescriptor
 import org.jetbrains.kotlin.psi.KtCallableDeclaration
 import org.jetbrains.kotlin.psi.KtStringTemplateExpression
 import org.jetbrains.kotlin.psi.KtTypeArgumentList
+import org.jetbrains.kotlin.psi.KtTypeElement
 import org.jetbrains.kotlin.psi.KtTypeProjection
 import org.jetbrains.kotlin.psi.KtTypeReference
 import org.jetbrains.kotlin.psi.KtValueArgument
@@ -31,6 +33,7 @@ internal data class Parameter(
   val lazyTypeName: ParameterizedTypeName,
   val isWrappedInProvider: Boolean,
   val isWrappedInLazy: Boolean,
+  val isLazyWrappedInProvider: Boolean,
   val isAssisted: Boolean,
   val assistedIdentifier: String,
   val assistedParameterKey: AssistedParameterKey = AssistedParameterKey(
@@ -39,6 +42,7 @@ internal data class Parameter(
   )
 ) {
   val originalTypeName: TypeName = when {
+    isLazyWrappedInProvider -> lazyTypeName.wrapInProvider()
     isWrappedInProvider -> providerTypeName
     isWrappedInLazy -> lazyTypeName
     else -> typeName
@@ -52,6 +56,18 @@ internal data class Parameter(
   )
 }
 
+private fun KtTypeElement.singleTypeArgument(): KtTypeReference {
+  return children
+    .filterIsInstance<KtTypeArgumentList>()
+    .single()
+    .children
+    .filterIsInstance<KtTypeProjection>()
+    .single()
+    .children
+    .filterIsInstance<KtTypeReference>()
+    .single()
+}
+
 internal fun List<KtCallableDeclaration>.mapToParameter(module: ModuleDescriptor): List<Parameter> =
   mapIndexed { index, parameter ->
     val typeElement = parameter.typeReference?.typeElement
@@ -60,21 +76,26 @@ internal fun List<KtCallableDeclaration>.mapToParameter(module: ModuleDescriptor
     val isWrappedInProvider = typeFqName == providerFqName
     val isWrappedInLazy = typeFqName == daggerLazyFqName
 
+    var isLazyWrappedInProvider = false
+
     val typeName = when {
       parameter.requireTypeReference(module).isNullable() ->
         parameter.requireTypeReference(module).requireTypeName(module).copy(nullable = true)
 
-      isWrappedInProvider || isWrappedInLazy ->
-        typeElement!!.children
-          .filterIsInstance<KtTypeArgumentList>()
-          .single()
-          .children
-          .filterIsInstance<KtTypeProjection>()
-          .single()
-          .children
-          .filterIsInstance<KtTypeReference>()
-          .single()
-          .requireTypeName(module)
+      isWrappedInProvider || isWrappedInLazy -> {
+        val typeParameterReference = typeElement!!.singleTypeArgument()
+
+        if (isWrappedInProvider &&
+          typeParameterReference.fqNameOrNull(module) == daggerLazyFqName
+        ) {
+          // This is a super rare case when someone injects Provider<Lazy<Type>> that requires
+          // special care.
+          isLazyWrappedInProvider = true
+          typeParameterReference.typeElement!!.singleTypeArgument().requireTypeName(module)
+        } else {
+          typeParameterReference.requireTypeName(module)
+        }
+      }
 
       else -> parameter.requireTypeReference(module).requireTypeName(module)
     }.withJvmSuppressWildcardsIfNeeded(parameter, module)
@@ -97,6 +118,7 @@ internal fun List<KtCallableDeclaration>.mapToParameter(module: ModuleDescriptor
       lazyTypeName = typeName.wrapInLazy(),
       isWrappedInProvider = isWrappedInProvider,
       isWrappedInLazy = isWrappedInLazy,
+      isLazyWrappedInProvider = isLazyWrappedInProvider,
       isAssisted = assistedAnnotation != null,
       assistedIdentifier = assistedIdentifier
     )
@@ -125,6 +147,8 @@ internal fun List<Parameter>.asArgumentList(
       if (asProvider) {
         list.map { parameter ->
           when {
+            parameter.isLazyWrappedInProvider ->
+              "${ProviderOfLazy::class.qualifiedName}.create(${parameter.name})"
             parameter.isWrappedInProvider -> parameter.name
             // Normally Dagger changes Lazy<Type> parameters to a Provider<Type> (usually the
             // container is a joined type), therefore we use `.lazy(..)` to convert the Provider


Test patch:
diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/InjectConstructorFactoryGeneratorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/InjectConstructorFactoryGeneratorTest.kt
index 9c3f4fe80..ef7cf6385 100644
--- a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/InjectConstructorFactoryGeneratorTest.kt
+++ b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/InjectConstructorFactoryGeneratorTest.kt
@@ -11,6 +11,7 @@ import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK
 import com.tschuchort.compiletesting.KotlinCompilation.Result
 import dagger.Lazy
 import dagger.internal.Factory
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -288,6 +289,183 @@ public final class InjectClass_Factory implements Factory<InjectClass> {
     }
   }
 
+  @Test
+  fun `a factory class is generated for an inject constructor with a lazy argument wrapped in a provider`() {
+    /*
+package com.squareup.test;
+
+import dagger.Lazy;
+import dagger.internal.DaggerGenerated;
+import dagger.internal.Factory;
+import dagger.internal.ProviderOfLazy;
+import javax.annotation.processing.Generated;
+import javax.inject.Provider;
+
+@DaggerGenerated
+@Generated(
+    value = "dagger.internal.codegen.ComponentProcessor",
+    comments = "https://dagger.dev"
+)
+@SuppressWarnings({
+    "unchecked",
+    "rawtypes"
+})
+public final class InjectClass_Factory implements Factory<InjectClass> {
+  private final Provider<String> stringProvider;
+
+  public InjectClass_Factory(Provider<String> stringProvider) {
+    this.stringProvider = stringProvider;
+  }
+
+  @Override
+  public InjectClass get() {
+    return newInstance(ProviderOfLazy.create(stringProvider));
+  }
+
+  public static InjectClass_Factory create(Provider<String> stringProvider) {
+    return new InjectClass_Factory(stringProvider);
+  }
+
+  public static InjectClass newInstance(Provider<Lazy<String>> string) {
+    return new InjectClass(string);
+  }
+}
+     */
+
+    compile(
+      """
+      package com.squareup.test
+      
+      import dagger.Lazy
+      import javax.inject.Inject
+      import javax.inject.Provider
+      
+      class InjectClass @Inject constructor(
+        val string: Provider<Lazy<String>>
+      ) {
+        override fun equals(other: Any?): Boolean {
+          return toString() == other.toString()
+        }
+        override fun toString(): String {
+         return string.get().get() 
+        }
+      }
+      """
+    ) {
+      val factoryClass = injectClass.factoryClass()
+
+      val constructor = factoryClass.declaredConstructors.single()
+      assertThat(constructor.parameterTypes.toList())
+        .containsExactly(Provider::class.java)
+
+      val staticMethods = factoryClass.declaredMethods.filter { it.isStatic }
+
+      val factoryInstance = staticMethods.single { it.name == "create" }
+        .invoke(null, Provider { "a" })
+      assertThat(factoryInstance::class.java).isEqualTo(factoryClass)
+
+      val newInstance = staticMethods.single { it.name == "newInstance" }
+        .invoke(null, Provider { dagger.Lazy { "a" } })
+      val getInstance = (factoryInstance as Factory<*>).get()
+
+      assertThat(newInstance).isNotNull()
+      assertThat(getInstance).isNotNull()
+
+      assertThat(newInstance).isEqualTo(getInstance)
+      assertThat(newInstance).isNotSameInstanceAs(getInstance)
+    }
+  }
+
+  @Test
+  @Ignore("This test is broken with Dagger as well.")
+  // Notice in the get() function Dagger creates a Lazy of a Provider of Provider instead of a
+  // Lazy of a Provider.
+  fun `a factory class is generated for an inject constructor with a provider argument wrapped in a lazy`() {
+    /*
+package com.squareup.test;
+
+import dagger.Lazy;
+import dagger.internal.DaggerGenerated;
+import dagger.internal.DoubleCheck;
+import dagger.internal.Factory;
+import javax.annotation.processing.Generated;
+import javax.inject.Provider;
+
+@DaggerGenerated
+@Generated(
+    value = "dagger.internal.codegen.ComponentProcessor",
+    comments = "https://dagger.dev"
+)
+@SuppressWarnings({
+    "unchecked",
+    "rawtypes"
+})
+public final class InjectClass_Factory implements Factory<InjectClass> {
+  private final Provider<Provider<String>> stringProvider;
+
+  public InjectClass_Factory(Provider<Provider<String>> stringProvider) {
+    this.stringProvider = stringProvider;
+  }
+
+  @Override
+  public InjectClass get() {
+    return newInstance(DoubleCheck.lazy(stringProvider));
+  }
+
+  public static InjectClass_Factory create(Provider<Provider<String>> stringProvider) {
+    return new InjectClass_Factory(stringProvider);
+  }
+
+  public static InjectClass newInstance(Lazy<Provider<String>> string) {
+    return new InjectClass(string);
+  }
+}
+     */
+
+    compile(
+      """
+      package com.squareup.test
+      
+      import dagger.Lazy
+      import javax.inject.Inject
+      import javax.inject.Provider
+      
+      class InjectClass @Inject constructor(
+        val string: Lazy<Provider<String>>
+      ) {
+        override fun equals(other: Any?): Boolean {
+          return toString() == other.toString()
+        }
+        override fun toString(): String {
+         return string.get().get() 
+        }
+      }
+      """
+    ) {
+      val factoryClass = injectClass.factoryClass()
+
+      val constructor = factoryClass.declaredConstructors.single()
+      assertThat(constructor.parameterTypes.toList())
+        .containsExactly(Provider::class.java)
+
+      val staticMethods = factoryClass.declaredMethods.filter { it.isStatic }
+
+      val factoryInstance = staticMethods.single { it.name == "create" }
+        .invoke(null, Provider { "a" })
+      assertThat(factoryInstance::class.java).isEqualTo(factoryClass)
+
+      val newInstance = staticMethods.single { it.name == "newInstance" }
+        .invoke(null, dagger.Lazy { Provider { "a" } })
+      val getInstance = (factoryInstance as Factory<*>).get()
+
+      assertThat(newInstance).isNotNull()
+      assertThat(getInstance).isNotNull()
+
+      assertThat(newInstance).isEqualTo(getInstance)
+      assertThat(newInstance).isNotSameInstanceAs(getInstance)
+    }
+  }
+
   @Test fun `a factory class is generated for an inject constructor with star imports`() {
     /*
 package com.squareup.test;