Repository: square/anvil (kotlin)

Issue:
Qualifiers on properties not preserved
Given this example:

```kotlin
class Example {
  @Inject
  @ForScope(UserScope::class)
  lateinit var activityNavigator: ActivityNavigator
}
```

The qualifier is not accounted for in Dagger. The fix is to add a field site target


```kotlin
class Example {
  @Inject
  @field:ForScope(UserScope::class)
  lateinit var activityNavigator: ActivityNavigator
}
```

Pull request:
Resolves #264

Patch:
diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/PsiUtils.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/PsiUtils.kt
index 36101981d..4519b4e06 100644
--- a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/PsiUtils.kt
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/PsiUtils.kt
@@ -7,6 +7,7 @@ import com.squareup.anvil.compiler.getAllSuperTypes
 import com.squareup.anvil.compiler.injectFqName
 import com.squareup.anvil.compiler.jvmSuppressWildcardsFqName
 import com.squareup.anvil.compiler.publishedApiFqName
+import com.squareup.anvil.compiler.qualifierFqName
 import com.squareup.anvil.compiler.safePackageString
 import com.squareup.kotlinpoet.TypeVariableName
 import org.jetbrains.kotlin.com.intellij.psi.PsiElement
@@ -545,3 +546,15 @@ fun FqName.requireClassDescriptor(module: ModuleDescriptor): ClassDescriptor {
   return module.resolveClassByFqName(this, FROM_BACKEND)
     ?: throw AnvilCompilationException("Couldn't resolve class for $this.")
 }
+
+fun KtAnnotationEntry.isQualifier(module: ModuleDescriptor): Boolean {
+  return typeReference
+    ?.requireFqName(module)
+    // Often entries are annotated with @Inject, in this case we know it's not a qualifier and we
+    // can stop early.
+    ?.takeIf { it != injectFqName }
+    ?.requireClassDescriptor(module)
+    ?.annotations
+    ?.hasAnnotation(qualifierFqName)
+    ?: false
+}
diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/dagger/MembersInjectorGenerator.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/dagger/MembersInjectorGenerator.kt
index eb6640826..ebf8fe745 100644
--- a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/dagger/MembersInjectorGenerator.kt
+++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/dagger/MembersInjectorGenerator.kt
@@ -8,11 +8,15 @@ import com.squareup.anvil.compiler.codegen.classesAndInnerClasses
 import com.squareup.anvil.compiler.codegen.createGeneratedFile
 import com.squareup.anvil.compiler.codegen.hasAnnotation
 import com.squareup.anvil.compiler.codegen.isGenericClass
+import com.squareup.anvil.compiler.codegen.isQualifier
 import com.squareup.anvil.compiler.codegen.mapToParameter
+import com.squareup.anvil.compiler.codegen.requireClassDescriptor
 import com.squareup.anvil.compiler.codegen.requireFqName
+import com.squareup.anvil.compiler.codegen.toAnnotationSpec
 import com.squareup.anvil.compiler.daggerDoubleCheckFqNameString
 import com.squareup.anvil.compiler.generateClassName
 import com.squareup.anvil.compiler.injectFqName
+import com.squareup.anvil.compiler.isQualifier
 import com.squareup.anvil.compiler.safePackageString
 import com.squareup.kotlinpoet.AnnotationSpec
 import com.squareup.kotlinpoet.ClassName
@@ -29,12 +33,15 @@ import com.squareup.kotlinpoet.jvm.jvmStatic
 import dagger.MembersInjector
 import dagger.internal.InjectedFieldSignature
 import org.jetbrains.kotlin.descriptors.ModuleDescriptor
+import org.jetbrains.kotlin.descriptors.PropertyDescriptor
 import org.jetbrains.kotlin.lexer.KtTokens
+import org.jetbrains.kotlin.psi.KtAnnotationEntry
 import org.jetbrains.kotlin.psi.KtClassBody
 import org.jetbrains.kotlin.psi.KtClassOrObject
 import org.jetbrains.kotlin.psi.KtFile
 import org.jetbrains.kotlin.psi.KtProperty
 import org.jetbrains.kotlin.psi.psiUtil.visibilityModifierTypeOrDefault
+import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter
 import java.io.File
 import java.util.Locale.US
 
@@ -58,7 +65,12 @@ internal class MembersInjectorGenerator : PrivateCodeGenerator() {
           .toList()
           .ifEmpty { return@forEach }
 
-        generateMembersInjectorClass(codeGenDir, module, clazz, injectProperties)
+        generateMembersInjectorClass(
+          codeGenDir = codeGenDir,
+          module = module,
+          clazz = clazz,
+          injectProperties = injectProperties
+        )
       }
   }
 
@@ -92,6 +104,15 @@ internal class MembersInjectorGenerator : PrivateCodeGenerator() {
 
     val memberInjectorClass = ClassName(packageName, className)
 
+    // Lazily evaluated, only computed if any properties have any non-inject annotations.
+    val propertyDescriptors by lazy {
+      clazz.requireClassDescriptor(module)
+        .unsubstitutedMemberScope
+        .getContributedDescriptors(DescriptorKindFilter.VARIABLES)
+        .filterIsInstance<PropertyDescriptor>()
+        .associateBy { it.name.asString() }
+    }
+
     val content = FileSpec.buildFile(packageName, className) {
       addType(
         TypeSpec
@@ -172,6 +193,19 @@ internal class MembersInjectorGenerator : PrivateCodeGenerator() {
                           .addMember("%S", property.requireFqName())
                           .build()
                       )
+                      .apply {
+                        val hasQualifier = property.annotationEntries
+                          .any { it.isQualifier(module) }
+
+                        if (hasQualifier) {
+                          addAnnotations(
+                            propertyDescriptors.getValue(propertyName)
+                              .annotations
+                              .filter { it.isQualifier() }
+                              .map { it.toAnnotationSpec(module) }
+                          )
+                        }
+                      }
                       .addParameter("instance", classType)
                       .addParameter(propertyName, parameter.originalTypeName)
                       .addStatement("instance.$propertyName = $propertyName")
@@ -187,4 +221,8 @@ internal class MembersInjectorGenerator : PrivateCodeGenerator() {
 
     return createGeneratedFile(codeGenDir, packageName, className, content)
   }
+
+  private fun KtAnnotationEntry.isInject(module: ModuleDescriptor): Boolean {
+    return requireFqName(module) == injectFqName
+  }
 }


Test patch:
diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/MembersInjectorGeneratorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/MembersInjectorGeneratorTest.kt
index efdd2bdd4..35425b1c6 100644
--- a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/MembersInjectorGeneratorTest.kt
+++ b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/MembersInjectorGeneratorTest.kt
@@ -2,6 +2,7 @@ package com.squareup.anvil.compiler.dagger
 
 import com.google.common.truth.Truth.assertThat
 import com.squareup.anvil.compiler.createInstance
+import com.squareup.anvil.compiler.getValue
 import com.squareup.anvil.compiler.injectClass
 import com.squareup.anvil.compiler.isStatic
 import com.squareup.anvil.compiler.membersInjector
@@ -15,6 +16,7 @@ import org.junit.runners.Parameterized.Parameters
 import java.io.File
 import java.lang.reflect.Method
 import java.util.Locale.US
+import javax.inject.Named
 import javax.inject.Provider
 
 @Suppress("UNCHECKED_CAST")
@@ -39,6 +41,7 @@ import dagger.internal.InjectedFieldSignature;
 import java.util.List;
 import java.util.Set;
 import javax.annotation.Generated;
+import javax.inject.Named;
 import javax.inject.Provider;
 import kotlin.Pair;
 import kotlin.jvm.functions.Function1;
@@ -54,6 +57,8 @@ import kotlin.jvm.functions.Function1;
 public final class InjectClass_MembersInjector implements MembersInjector<InjectClass> {
   private final Provider<String> stringProvider;
 
+  private final Provider<String> qualifiedStringProvider;
+
   private final Provider<CharSequence> charSequenceProvider;
 
   private final Provider<List<String>> listProvider;
@@ -63,10 +68,11 @@ public final class InjectClass_MembersInjector implements MembersInjector<Inject
   private final Provider<Set<Function1<List<String>, List<String>>>> setProvider;
 
   public InjectClass_MembersInjector(Provider<String> stringProvider,
-      Provider<CharSequence> charSequenceProvider, Provider<List<String>> listProvider,
-      Provider<Pair<Pair<String, Integer>, ? extends Set<String>>> pairProvider,
-      Provider<Set<Function1<List<String>, List<String>>>> setProvider) {
+      Provider<String> qualifiedStringProvider, Provider<CharSequence> charSequenceProvider,
+      Provider<List<String>> listProvider, Provider<Pair<Pair<String, Integer>,
+      ? extends Set<String>>> pairProvider, Provider<Set<Function1<List<String>, List<String>>>> setProvider) {
     this.stringProvider = stringProvider;
+    this.qualifiedStringProvider = qualifiedStringProvider;
     this.charSequenceProvider = charSequenceProvider;
     this.listProvider = listProvider;
     this.pairProvider = pairProvider;
@@ -74,14 +80,16 @@ public final class InjectClass_MembersInjector implements MembersInjector<Inject
   }
 
   public static MembersInjector<InjectClass> create(Provider<String> stringProvider,
-      Provider<CharSequence> charSequenceProvider, Provider<List<String>> listProvider,
-      Provider<Pair<Pair<String, Integer>, ? extends Set<String>>> pairProvider,
-      Provider<Set<Function1<List<String>, List<String>>>> setProvider) {
-    return new InjectClass_MembersInjector(stringProvider, charSequenceProvider, listProvider, pairProvider, setProvider);}
+      Provider<String> qualifiedStringProvider, Provider<CharSequence> charSequenceProvider,
+      Provider<List<String>> listProvider, Provider<Pair<Pair<String, Integer>,
+      ? extends Set<String>>> pairProvider, Provider<Set<Function1<List<String>,
+      List<String>>>> setProvider) {
+    return new InjectClass_MembersInjector(stringProvider, qualifiedStringProvider, charSequenceProvider, listProvider, pairProvider, setProvider);}
 
   @Override
   public void injectMembers(InjectClass instance) {
     injectString(instance, stringProvider.get());
+    injectQualifiedString(instance, qualifiedStringProvider.get());
     injectCharSequence(instance, charSequenceProvider.get());
     injectList(instance, listProvider.get());
     injectPair(instance, pairProvider.get());
@@ -93,6 +101,12 @@ public final class InjectClass_MembersInjector implements MembersInjector<Inject
     instance.string = string;
   }
 
+  @Named("qualified")
+  @InjectedFieldSignature("com.squareup.test.InjectClass.qualifiedString")
+  public static void injectQualifiedString(InjectClass instance, String qualifiedString) {
+    instance.qualifiedString = qualifiedString;
+  }
+
   @InjectedFieldSignature("com.squareup.test.InjectClass.charSequence")
   public static void injectCharSequence(InjectClass instance, CharSequence charSequence) {
     instance.charSequence = charSequence;
@@ -122,11 +136,13 @@ public final class InjectClass_MembersInjector implements MembersInjector<Inject
       package com.squareup.test
       
       import javax.inject.Inject
+      import javax.inject.Named
       
       typealias StringList = List<String>
       
       class InjectClass {
         @Inject lateinit var string: String
+        @Named("qualified") @Inject lateinit var qualifiedString: String
         @Inject lateinit var charSequence: CharSequence
         @Inject lateinit var list: List<String>
         @Inject lateinit var pair: Pair<Pair<String, Int>, Set<String>>
@@ -139,6 +155,7 @@ public final class InjectClass_MembersInjector implements MembersInjector<Inject
           other as InjectClass
       
           if (string != other.string) return false
+          if (qualifiedString != other.qualifiedString) return false
           if (charSequence != other.charSequence) return false
           if (list != other.list) return false
           if (pair != other.pair) return false
@@ -149,6 +166,7 @@ public final class InjectClass_MembersInjector implements MembersInjector<Inject
       
         override fun hashCode(): Int {
           var result = string.hashCode()
+          result = 31 * result + qualifiedString.hashCode()
           result = 31 * result + charSequence.hashCode()
           result = 31 * result + list.hashCode()
           result = 31 * result + pair.hashCode()
@@ -167,6 +185,7 @@ public final class InjectClass_MembersInjector implements MembersInjector<Inject
           Provider::class.java,
           Provider::class.java,
           Provider::class.java,
+          Provider::class.java,
           Provider::class.java
         )
 
@@ -174,10 +193,11 @@ public final class InjectClass_MembersInjector implements MembersInjector<Inject
       val membersInjectorInstance = constructor
         .newInstance(
           Provider { "a" },
-          Provider<CharSequence> { "b" },
-          Provider { listOf("c") },
-          Provider { Pair(Pair("a", 1), setOf("b")) },
-          Provider { setOf { _: List<String> -> listOf("d") } }
+          Provider { "b" },
+          Provider<CharSequence> { "c" },
+          Provider { listOf("d") },
+          Provider { Pair(Pair("e", 1), setOf("f")) },
+          Provider { setOf { _: List<String> -> listOf("g") } }
         )
         as MembersInjector<Any>
 
@@ -188,17 +208,23 @@ public final class InjectClass_MembersInjector implements MembersInjector<Inject
 
       membersInjector.staticInjectMethod("string")
         .invoke(null, injectInstanceStatic, "a")
+      membersInjector.staticInjectMethod("qualifiedString")
+        .invoke(null, injectInstanceStatic, "b")
       membersInjector.staticInjectMethod("charSequence")
-        .invoke(null, injectInstanceStatic, "b" as CharSequence)
+        .invoke(null, injectInstanceStatic, "c" as CharSequence)
       membersInjector.staticInjectMethod("list")
-        .invoke(null, injectInstanceStatic, listOf("c"))
+        .invoke(null, injectInstanceStatic, listOf("d"))
       membersInjector.staticInjectMethod("pair")
-        .invoke(null, injectInstanceStatic, Pair(Pair("a", 1), setOf("b")))
+        .invoke(null, injectInstanceStatic, Pair(Pair("e", 1), setOf("f")))
       membersInjector.staticInjectMethod("set")
-        .invoke(null, injectInstanceStatic, setOf { _: List<String> -> listOf("d") })
+        .invoke(null, injectInstanceStatic, setOf { _: List<String> -> listOf("g") })
 
       assertThat(injectInstanceConstructor).isEqualTo(injectInstanceStatic)
       assertThat(injectInstanceConstructor).isNotSameInstanceAs(injectInstanceStatic)
+
+      val namedAnnotation = membersInjector.staticInjectMethod("qualifiedString").annotations
+        .single { it.annotationClass == Named::class }
+      assertThat(namedAnnotation.getValue<String>()).isEqualTo("qualified")
     }
   }