From 1741c56ffdfdf494dda1c06bcec5552b24e6821c Mon Sep 17 00:00:00 2001 From: Rick Busarow Date: Wed, 27 Mar 2024 16:15:45 -0500 Subject: [PATCH] use the resolved value of `const` arguments in propagated annotation arguments We have a good deal of logic around parsing out the primitive values of `const` parameters, but that logic is not recursive. So, if a `const` references another `const` in its initializer, we wound up just copying the text of that initializer into the generated annotations, as though it's just a String. This problem was isolated to the PSI side of parsing, since the Descriptor APIs are able to parse out the final, primitive value. Our type resolution logic always defaults to the PSI models. Now, when we need to parse out the primitive values from a `PropertyReference`, we first try to resolve the Descriptor version with a `PropertyDescriptor`. That isn't possible if the annotation is referencing a `const` property that was generated in the same round of compilation. In that event, the parsing will fall back to the PSI models. Anvil doesn't actually generate a `const` and then use it in an annotation, so this seems like a reasonable compromise. fixes #938 --- .editorconfig | 2 +- .../squareup/anvil/conventions/BasePlugin.kt | 26 +- compiler-utils/api/compiler-utils.api | 8 +- .../anvil/compiler/internal/FqName.kt | 23 ++ .../reference/AnnotationArgumentReference.kt | 18 +- .../reference/AnvilModuleDescriptor.kt | 9 +- .../reference/MemberPropertyReference.kt | 9 +- .../internal/reference/PropertyReference.kt | 19 +- .../reference/TopLevelPropertyReference.kt | 17 ++ compiler/api/compiler.api | 2 + compiler/build.gradle.kts | 4 + .../reference/RealAnvilModuleDescriptor.kt | 42 +++ .../com/squareup/anvil/compiler/TestUtils.kt | 8 +- .../ContributesBindingGeneratorTest.kt | 127 ++++++++ .../ContributesMultibindingGeneratorTest.kt | 128 +++++++++ .../reference/AnnotationReferenceTest.kt | 271 +++++++++++++----- .../reference/ReferencesTestEnvironment.kt | 174 +++++++++++ 17 files changed, 792 insertions(+), 95 deletions(-) create mode 100644 compiler/src/test/java/com/squareup/anvil/compiler/internal/reference/ReferencesTestEnvironment.kt diff --git a/.editorconfig b/.editorconfig index 6a6deb17f..072e5a3a1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -537,7 +537,7 @@ ij_kotlin_extends_list_wrap = on_every_item ij_kotlin_field_annotation_wrap = normal ij_kotlin_finally_on_new_line = false ij_kotlin_if_rparen_on_new_line = true -ij_kotlin_import_nested_classes = true +ij_kotlin_import_nested_classes = false ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ ij_kotlin_insert_whitespaces_in_simple_one_line_method = true ij_kotlin_keep_blank_lines_before_right_brace = 0 diff --git a/build-logic/conventions/src/main/kotlin/com/squareup/anvil/conventions/BasePlugin.kt b/build-logic/conventions/src/main/kotlin/com/squareup/anvil/conventions/BasePlugin.kt index e43247aeb..710ca7e91 100644 --- a/build-logic/conventions/src/main/kotlin/com/squareup/anvil/conventions/BasePlugin.kt +++ b/build-logic/conventions/src/main/kotlin/com/squareup/anvil/conventions/BasePlugin.kt @@ -3,7 +3,9 @@ package com.squareup.anvil.conventions import com.rickbusarow.kgx.buildDir import com.rickbusarow.kgx.extras import com.rickbusarow.kgx.fromInt +import com.rickbusarow.kgx.getValue import com.rickbusarow.kgx.javaExtension +import com.rickbusarow.kgx.provideDelegate import com.squareup.anvil.conventions.utils.isInAnvilBuild import com.squareup.anvil.conventions.utils.isInAnvilIncludedBuild import com.squareup.anvil.conventions.utils.isInAnvilRootBuild @@ -201,6 +203,29 @@ abstract class BasePlugin : Plugin { task.maxParallelForks = Runtime.getRuntime().availableProcessors() + task.useJUnitPlatform { + it.includeEngines("junit-jupiter", "junit-vintage") + } + + val testImplementation by target.configurations + + testImplementation.dependencies.addLater(target.libs.junit.jupiter.engine) + testImplementation.dependencies.addLater(target.libs.junit.vintage.engine) + + task.systemProperties.putAll( + mapOf( + // remove parentheses from test display names + "junit.jupiter.displayname.generator.default" to + "org.junit.jupiter.api.DisplayNameGenerator\$Simple", + + // Allow unit tests to run in parallel + // https://junit.org/junit5/docs/snapshot/user-guide/#writing-tests-parallel-execution-config-properties + "junit.jupiter.execution.parallel.enabled" to true, + "junit.jupiter.execution.parallel.mode.default" to "concurrent", + "junit.jupiter.execution.parallel.mode.classes.default" to "concurrent", + ), + ) + task.jvmArgs( // Fixes illegal reflective operation warnings during tests. It's a Kotlin issue. // https://github.com/pinterest/ktlint/issues/1618 @@ -218,7 +243,6 @@ abstract class BasePlugin : Plugin { "--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED", "--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", "--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", - "--illegal-access=permit", ) task.testLogging { logging -> diff --git a/compiler-utils/api/compiler-utils.api b/compiler-utils/api/compiler-utils.api index 8e56289ee..c58e10616 100644 --- a/compiler-utils/api/compiler-utils.api +++ b/compiler-utils/api/compiler-utils.api @@ -10,6 +10,8 @@ public final class com/squareup/anvil/compiler/internal/FqNameKt { public static final fun classIdBestGuess (Lorg/jetbrains/kotlin/name/FqName;)Lorg/jetbrains/kotlin/name/ClassId; public static final fun descendant (Lorg/jetbrains/kotlin/name/FqName;Ljava/lang/String;)Lorg/jetbrains/kotlin/name/FqName; public static final fun getFqName (Lkotlin/reflect/KClass;)Lorg/jetbrains/kotlin/name/FqName; + public static final fun parents (Lorg/jetbrains/kotlin/name/FqName;)Lkotlin/sequences/Sequence; + public static final fun parentsWithSelf (Lorg/jetbrains/kotlin/name/FqName;)Lkotlin/sequences/Sequence; public static final fun safePackageString (Ljava/lang/String;ZZZ)Ljava/lang/String; public static final fun safePackageString (Lorg/jetbrains/kotlin/name/FqName;ZZ)Ljava/lang/String; public static synthetic fun safePackageString$default (Ljava/lang/String;ZZZILjava/lang/Object;)Ljava/lang/String; @@ -147,9 +149,11 @@ public abstract interface class com/squareup/anvil/compiler/internal/reference/A public abstract fun getClassReference (Lorg/jetbrains/kotlin/psi/KtClassOrObject;)Lcom/squareup/anvil/compiler/internal/reference/ClassReference$Psi; public abstract fun getClassReferenceOrNull (Lorg/jetbrains/kotlin/name/FqName;)Lcom/squareup/anvil/compiler/internal/reference/ClassReference; public abstract fun getTopLevelFunctionReferences (Lorg/jetbrains/kotlin/psi/KtFile;)Ljava/util/List; + public abstract fun getTopLevelPropertyReferenceOrNull (Lorg/jetbrains/kotlin/name/FqName;)Lcom/squareup/anvil/compiler/internal/reference/PropertyReference; public abstract fun getTopLevelPropertyReferences (Lorg/jetbrains/kotlin/psi/KtFile;)Ljava/util/List; public abstract fun resolveClassIdOrNull (Lorg/jetbrains/kotlin/name/ClassId;)Lorg/jetbrains/kotlin/name/FqName; public abstract fun resolveFqNameOrNull (Lorg/jetbrains/kotlin/name/FqName;Lorg/jetbrains/kotlin/incremental/components/LookupLocation;)Lorg/jetbrains/kotlin/descriptors/ClassDescriptor; + public abstract fun resolvePropertyReferenceOrNull (Lorg/jetbrains/kotlin/name/FqName;)Lcom/squareup/anvil/compiler/internal/reference/PropertyReference; public abstract fun resolveTypeAliasFqNameOrNull (Lorg/jetbrains/kotlin/name/FqName;)Lorg/jetbrains/kotlin/descriptors/TypeAliasDescriptor; } @@ -358,7 +362,7 @@ public final class com/squareup/anvil/compiler/internal/reference/MemberFunction public abstract class com/squareup/anvil/compiler/internal/reference/MemberPropertyReference : com/squareup/anvil/compiler/internal/reference/AnnotatedReference, com/squareup/anvil/compiler/internal/reference/PropertyReference { public fun equals (Ljava/lang/Object;)Z public abstract fun getDeclaringClass ()Lcom/squareup/anvil/compiler/internal/reference/ClassReference; - public final fun getMemberName ()Lcom/squareup/kotlinpoet/MemberName; + public fun getMemberName ()Lcom/squareup/kotlinpoet/MemberName; public fun getModule ()Lcom/squareup/anvil/compiler/internal/reference/AnvilModuleDescriptor; protected abstract fun getType ()Lcom/squareup/anvil/compiler/internal/reference/TypeReference; public fun hashCode ()I @@ -449,6 +453,7 @@ public final class com/squareup/anvil/compiler/internal/reference/ParameterRefer public abstract interface class com/squareup/anvil/compiler/internal/reference/PropertyReference { public abstract fun getFqName ()Lorg/jetbrains/kotlin/name/FqName; public abstract fun getGetterAnnotations ()Ljava/util/List; + public abstract fun getMemberName ()Lcom/squareup/kotlinpoet/MemberName; public abstract fun getModule ()Lcom/squareup/anvil/compiler/internal/reference/AnvilModuleDescriptor; public abstract fun getName ()Ljava/lang/String; public abstract fun getSetterAnnotations ()Ljava/util/List; @@ -521,6 +526,7 @@ public final class com/squareup/anvil/compiler/internal/reference/TopLevelFuncti public abstract class com/squareup/anvil/compiler/internal/reference/TopLevelPropertyReference : com/squareup/anvil/compiler/internal/reference/AnnotatedReference, com/squareup/anvil/compiler/internal/reference/PropertyReference { public fun equals (Ljava/lang/Object;)Z + public fun getMemberName ()Lcom/squareup/kotlinpoet/MemberName; protected abstract fun getType ()Lcom/squareup/anvil/compiler/internal/reference/TypeReference; public fun hashCode ()I public fun isAnnotatedWith (Lorg/jetbrains/kotlin/name/FqName;)Z 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 47722d43a..51ce904f2 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 @@ -14,6 +14,7 @@ import dagger.MapKey import dagger.Provides import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.parentOrNull import javax.inject.Inject import javax.inject.Qualifier import javax.inject.Scope @@ -39,6 +40,28 @@ internal val mergeModulesFqName = MergeModules::class.fqName internal val anyFqName = Any::class.fqName +/** + * Generates a sequence of [FqName] starting from the current FqName and including its parents + * up to the root. The sequence will include the current FqName as well. + */ +@ExperimentalAnvilApi +public fun FqName.parentsWithSelf(): Sequence { + return generateSequence(this) { it.parentOrNull() } + .map { + it.toUnsafe() + // The top-most parent is an FqName with the text "", + // whereas the actual FqName.ROOT is an empty string. We want the empty string. + if (parent().isRoot) FqName.ROOT else it + } +} + +/** + * Generates a sequence of [FqName] starting from the current FqName and including its parents + * up to the root. The sequence will not include the current FqName. + */ +@ExperimentalAnvilApi +public fun FqName.parents(): Sequence = parentsWithSelf().drop(1) + @ExperimentalAnvilApi public fun FqName.descendant(segments: String): FqName = if (isRoot) FqName(segments) else FqName("${asString()}.$segments") diff --git a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/AnnotationArgumentReference.kt b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/AnnotationArgumentReference.kt index b6c9eb61f..8a76234f4 100644 --- a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/AnnotationArgumentReference.kt +++ b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/AnnotationArgumentReference.kt @@ -206,20 +206,18 @@ public sealed class AnnotationArgumentReference { } fun resolvePrimitiveConstant(fqName: FqName): Any? { - // This could be a constant from a primitive type, e.g. Int.MAX_VALUE - val classFqName = fqName.parent() - val constantName = fqName.shortName().asString() - // If this constant is coming from a companion object, then we'll find it this way. - classFqName.toClassReferenceOrNull(module) - ?.let { if (it.isObject()) listOf(it) else it.companionObjects() } - ?.flatMap { it.properties } - ?.singleOrNull { it.name == constantName } + module.resolvePropertyReferenceOrNull(fqName) + // Prefer descriptor types for this since the parsing is already done. + // We won't be able to resolve a descriptor + // if the reference was also generated in this round, + // but Anvil itself doesn't generate consts and then use them as annotation arguments. + ?.let { it.toDescriptorOrNull() ?: it } ?.let { property -> return when (property) { - is MemberPropertyReference.Descriptor -> + is PropertyReference.Descriptor -> property.property.compileTimeInitializer?.value - is MemberPropertyReference.Psi -> + is PropertyReference.Psi -> // A PropertyReference.property may also be a KtParameter if it's in a constructor, // but if we're here we're in an object, so the property must be a KtProperty. (property.property as KtProperty).initializer?.let { parsePrimitiveType(it.text) } diff --git a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/AnvilModuleDescriptor.kt b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/AnvilModuleDescriptor.kt index 68e4d3f69..1324b778a 100644 --- a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/AnvilModuleDescriptor.kt +++ b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/AnvilModuleDescriptor.kt @@ -33,6 +33,10 @@ public interface AnvilModuleDescriptor : ModuleDescriptor { public fun getTopLevelPropertyReferences(ktFile: KtFile): List + public fun getTopLevelPropertyReferenceOrNull(fqName: FqName): PropertyReference? + + public fun resolvePropertyReferenceOrNull(fqName: FqName): PropertyReference? + public fun getClassReference(clazz: KtClassOrObject): Psi public fun getClassReference(descriptor: ClassDescriptor): Descriptor @@ -52,7 +56,10 @@ internal inline fun ModuleDescriptor.asAnvilModuleDescriptor(): AnvilModuleDescr @ExperimentalAnvilApi public fun FqName.canResolveFqName( module: ModuleDescriptor, -): Boolean = module.asAnvilModuleDescriptor().resolveClassIdOrNull(classIdBestGuess()) != null +): Boolean = module.asAnvilModuleDescriptor().run { + resolveClassIdOrNull(this@canResolveFqName.classIdBestGuess()) != null || + resolvePropertyReferenceOrNull(this@canResolveFqName) != null +} @ExperimentalAnvilApi public fun Collection.classAndInnerClassReferences( diff --git a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/MemberPropertyReference.kt b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/MemberPropertyReference.kt index 21dc1a74e..39f54da21 100644 --- a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/MemberPropertyReference.kt +++ b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/MemberPropertyReference.kt @@ -32,7 +32,7 @@ public sealed class MemberPropertyReference : AnnotatedReference, PropertyRefere public override val module: AnvilModuleDescriptor get() = declaringClass.module - public val memberName: MemberName get() = MemberName(declaringClass.asClassName(), name) + public override val memberName: MemberName get() = MemberName(declaringClass.asClassName(), name) protected abstract val type: TypeReference? @@ -201,3 +201,10 @@ public fun KtProperty.toPropertyReference( public fun PropertyDescriptor.toPropertyReference( declaringClass: ClassReference.Descriptor, ): Descriptor = Descriptor(this, declaringClass) + +internal fun MemberPropertyReference.toDescriptorOrNull(): Descriptor? { + return when (this) { + is Descriptor -> this + is Psi -> declaringClass.toDescriptorReferenceOrNull()?.properties?.find { it.name == name } + } +} diff --git a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/PropertyReference.kt b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/PropertyReference.kt index 03c7db5ea..77ee8e68b 100644 --- a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/PropertyReference.kt +++ b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/PropertyReference.kt @@ -2,8 +2,7 @@ package com.squareup.anvil.compiler.internal.reference import com.squareup.anvil.annotations.ExperimentalAnvilApi import com.squareup.anvil.compiler.api.AnvilCompilationException -import com.squareup.anvil.compiler.internal.reference.PropertyReference.Descriptor -import com.squareup.anvil.compiler.internal.reference.PropertyReference.Psi +import com.squareup.kotlinpoet.MemberName import org.jetbrains.kotlin.descriptors.PropertyDescriptor import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.KtCallableDeclaration @@ -15,6 +14,7 @@ public sealed interface PropertyReference { public val module: AnvilModuleDescriptor public val name: String + public val memberName: MemberName public val setterAnnotations: List public val getterAnnotations: List @@ -38,6 +38,17 @@ public sealed interface PropertyReference { } } +internal fun ClassReference.Psi.toDescriptorReferenceOrNull(): ClassReference.Descriptor? { + return module.resolveFqNameOrNull(fqName)?.toClassReference(module) +} + +internal fun PropertyReference.toDescriptorOrNull(): PropertyReference.Descriptor? { + return when (this) { + is MemberPropertyReference -> toDescriptorOrNull() + is TopLevelPropertyReference -> toDescriptorOrNull() + } +} + @ExperimentalAnvilApi @Suppress("FunctionName") public fun AnvilCompilationExceptionPropertyReference( @@ -45,12 +56,12 @@ public fun AnvilCompilationExceptionPropertyReference( message: String, cause: Throwable? = null, ): AnvilCompilationException = when (propertyReference) { - is Psi -> AnvilCompilationException( + is PropertyReference.Psi -> AnvilCompilationException( element = propertyReference.property, message = message, cause = cause, ) - is Descriptor -> AnvilCompilationException( + is PropertyReference.Descriptor -> AnvilCompilationException( propertyDescriptor = propertyReference.property, message = message, cause = cause, diff --git a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/TopLevelPropertyReference.kt b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/TopLevelPropertyReference.kt index eefd57c2a..1caa915b2 100644 --- a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/TopLevelPropertyReference.kt +++ b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/TopLevelPropertyReference.kt @@ -2,6 +2,7 @@ package com.squareup.anvil.compiler.internal.reference import com.squareup.anvil.annotations.ExperimentalAnvilApi import com.squareup.anvil.compiler.api.AnvilCompilationException +import com.squareup.anvil.compiler.internal.getContributedPropertyOrNull import com.squareup.anvil.compiler.internal.reference.TopLevelPropertyReference.Descriptor import com.squareup.anvil.compiler.internal.reference.TopLevelPropertyReference.Psi import com.squareup.anvil.compiler.internal.reference.Visibility.INTERNAL @@ -9,6 +10,7 @@ import com.squareup.anvil.compiler.internal.reference.Visibility.PRIVATE import com.squareup.anvil.compiler.internal.reference.Visibility.PROTECTED import com.squareup.anvil.compiler.internal.reference.Visibility.PUBLIC import com.squareup.anvil.compiler.internal.requireFqName +import com.squareup.kotlinpoet.MemberName import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.descriptors.PropertyDescriptor import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget.PROPERTY_GETTER @@ -29,6 +31,13 @@ public sealed class TopLevelPropertyReference : AnnotatedReference, PropertyRefe protected abstract val type: TypeReference? + override val memberName: MemberName by lazy(NONE) { + MemberName( + packageName = fqName.parent().asString(), + simpleName = name, + ) + } + public override fun typeOrNull(): TypeReference? = type override fun toString(): String = "$fqName" @@ -194,3 +203,11 @@ public fun KtProperty.toTopLevelPropertyReference( public fun PropertyDescriptor.toTopLevelPropertyReference( module: AnvilModuleDescriptor, ): Descriptor = Descriptor(property = this, module = module) + +internal fun TopLevelPropertyReference.toDescriptorOrNull(): Descriptor? { + return when (this) { + is Descriptor -> this + is Psi -> fqName.getContributedPropertyOrNull(module) + ?.toTopLevelPropertyReference(module) + } +} diff --git a/compiler/api/compiler.api b/compiler/api/compiler.api index 1344653da..d479dd519 100644 --- a/compiler/api/compiler.api +++ b/compiler/api/compiler.api @@ -51,10 +51,12 @@ public final class com/squareup/anvil/compiler/codegen/reference/RealAnvilModule public fun getStableName ()Lorg/jetbrains/kotlin/name/Name; public fun getSubPackagesOf (Lorg/jetbrains/kotlin/name/FqName;Lkotlin/jvm/functions/Function1;)Ljava/util/Collection; public fun getTopLevelFunctionReferences (Lorg/jetbrains/kotlin/psi/KtFile;)Ljava/util/List; + public fun getTopLevelPropertyReferenceOrNull (Lorg/jetbrains/kotlin/name/FqName;)Lcom/squareup/anvil/compiler/internal/reference/PropertyReference; public fun getTopLevelPropertyReferences (Lorg/jetbrains/kotlin/psi/KtFile;)Ljava/util/List; public fun isValid ()Z public fun resolveClassIdOrNull (Lorg/jetbrains/kotlin/name/ClassId;)Lorg/jetbrains/kotlin/name/FqName; public fun resolveFqNameOrNull (Lorg/jetbrains/kotlin/name/FqName;Lorg/jetbrains/kotlin/incremental/components/LookupLocation;)Lorg/jetbrains/kotlin/descriptors/ClassDescriptor; + public fun resolvePropertyReferenceOrNull (Lorg/jetbrains/kotlin/name/FqName;)Lcom/squareup/anvil/compiler/internal/reference/PropertyReference; public fun resolveTypeAliasFqNameOrNull (Lorg/jetbrains/kotlin/name/FqName;)Lorg/jetbrains/kotlin/descriptors/TypeAliasDescriptor; public fun shouldSeeInternalsOf (Lorg/jetbrains/kotlin/descriptors/ModuleDescriptor;)Z } diff --git a/compiler/build.gradle.kts b/compiler/build.gradle.kts index e656af0a9..3a830d5f3 100644 --- a/compiler/build.gradle.kts +++ b/compiler/build.gradle.kts @@ -65,4 +65,8 @@ dependencies { testImplementation(libs.kotlin.reflect) testImplementation(libs.ksp.compilerPlugin) testImplementation(libs.truth) + + testRuntimeOnly(libs.kotest.assertions.core.jvm) + testRuntimeOnly(libs.junit.vintage.engine) + testRuntimeOnly(libs.junit.jupiter.engine) } diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/reference/RealAnvilModuleDescriptor.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/reference/RealAnvilModuleDescriptor.kt index 4711d26bc..e4dfddf4d 100644 --- a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/reference/RealAnvilModuleDescriptor.kt +++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/reference/RealAnvilModuleDescriptor.kt @@ -5,10 +5,13 @@ import com.squareup.anvil.compiler.codegen.reference.RealAnvilModuleDescriptor.C import com.squareup.anvil.compiler.codegen.reference.RealAnvilModuleDescriptor.ClassReferenceCacheKey.Type.DESCRIPTOR import com.squareup.anvil.compiler.codegen.reference.RealAnvilModuleDescriptor.ClassReferenceCacheKey.Type.PSI import com.squareup.anvil.compiler.internal.classIdBestGuess +import com.squareup.anvil.compiler.internal.getContributedPropertyOrNull +import com.squareup.anvil.compiler.internal.parentsWithSelf import com.squareup.anvil.compiler.internal.reference.AnvilModuleDescriptor import com.squareup.anvil.compiler.internal.reference.ClassReference import com.squareup.anvil.compiler.internal.reference.ClassReference.Descriptor import com.squareup.anvil.compiler.internal.reference.ClassReference.Psi +import com.squareup.anvil.compiler.internal.reference.PropertyReference import com.squareup.anvil.compiler.internal.reference.TopLevelFunctionReference import com.squareup.anvil.compiler.internal.reference.TopLevelPropertyReference import com.squareup.anvil.compiler.internal.reference.toTopLevelFunctionReference @@ -97,6 +100,45 @@ public class RealAnvilModuleDescriptor private constructor( } } + override fun getTopLevelPropertyReferenceOrNull(fqName: FqName): PropertyReference? { + val parent = fqName.parent() + + fun psiReference() = allFiles + .filter { it.packageFqName == parent } + .firstNotNullOfOrNull { file -> + getTopLevelPropertyReferences(file) + .firstOrNull { it.fqName == fqName } + } + + return psiReference() ?: fqName.getContributedPropertyOrNull(this) + ?.toTopLevelPropertyReference(this) + } + + override fun resolvePropertyReferenceOrNull(fqName: FqName): PropertyReference? { + + val shortName = fqName.shortName().asString() + + val containingClass = fqName.parentsWithSelf() + .filter { !it.isRoot } + .firstNotNullOfOrNull { fq -> getClassReferenceOrNull(fq) } + // If we can't find the containing class, it could be a top-level property. + ?: return getTopLevelPropertyReferenceOrNull(fqName) + + // If the containing class has a companion object, this `fqName` may be referencing one of its + // members without the `Companion` segment. This also applies to named companion objects. + // So we need to check the companion object as well. + val classAndCompanions = sequence { + // Add the normal class first, because the Kotlin compiler prioritizes resolving normal + // instance properties over companion object properties. + yield(containingClass) + yieldAll(containingClass.companionObjects()) + } + + return classAndCompanions.firstNotNullOfOrNull { clazz -> + clazz.properties.firstOrNull { it.name == shortName } + } + } + override fun resolveClassIdOrNull(classId: ClassId): FqName? = resolveClassIdCache.getOrPut(classId) { val fqName = classId.asSingleFqName() 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 7a85dd235..504ed563a 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/TestUtils.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/TestUtils.kt @@ -42,7 +42,8 @@ internal fun compile( trackSourceFiles: Boolean = true, codeGenerators: List = emptyList(), allWarningsAsErrors: Boolean = WARNINGS_AS_ERRORS, - mode: AnvilCompilationMode = Embedded(codeGenerators), + mode: AnvilCompilationMode = AnvilCompilationMode.Embedded(codeGenerators), + workingDir: File? = null, block: JvmCompilationResult.() -> Unit = { }, ): JvmCompilationResult = compileAnvil( sources = sources, @@ -51,6 +52,7 @@ internal fun compile( enableDaggerAnnotationProcessor = enableDaggerAnnotationProcessor, trackSourceFiles = trackSourceFiles, mode = mode, + workingDir = workingDir, block = block, ) @@ -193,7 +195,9 @@ internal val Class<*>.multibindingOriginClass: KClass<*>? get() = resolveOriginClass(ContributesMultibinding::class) internal fun Class<*>.resolveOriginClass(bindingAnnotation: KClass): KClass<*>? { - val generatedBindingModule = generatedBindingModules(bindingAnnotation).firstOrNull() ?: return null + val generatedBindingModule = generatedBindingModules( + bindingAnnotation, + ).firstOrNull() ?: return null val bindingFunction = generatedBindingModule.declaredMethods[0] val parameterImplType = bindingFunction.parameterTypes.firstOrNull() val internalBindingMarker: InternalBindingMarker = diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesBindingGeneratorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesBindingGeneratorTest.kt index 61715947a..cf7e687a5 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesBindingGeneratorTest.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesBindingGeneratorTest.kt @@ -9,13 +9,17 @@ import com.squareup.anvil.compiler.bindingModuleScopes import com.squareup.anvil.compiler.bindingOriginKClass import com.squareup.anvil.compiler.compile import com.squareup.anvil.compiler.contributingInterface +import com.squareup.anvil.compiler.generatedBindingModule import com.squareup.anvil.compiler.generatedBindingModules import com.squareup.anvil.compiler.generatedFileOrNull +import com.squareup.anvil.compiler.injectClass import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode import com.squareup.anvil.compiler.isError +import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized +import javax.inject.Named @Suppress("RemoveRedundantQualifierName") @RunWith(Parameterized::class) @@ -77,6 +81,129 @@ class ContributesBindingGeneratorTest( } } + @Test + fun `a Named annotation using a private top-level const property is inlined in the generated module`() { + + // https://github.com/square/anvil/issues/938 + + compile( + """ + package com.squareup.test + + import com.squareup.anvil.annotations.ContributesBinding + import com.squareup.test.other.OTHER_CONSTANT + import javax.inject.Inject + import javax.inject.Named + + interface ParentInterface + + private const val CONSTANT = "${'$'}OTHER_CONSTANT.foo" + + @Named(CONSTANT) + @ContributesBinding(Any::class) + class InjectClass @Inject constructor() : ParentInterface + """, + """ + package com.squareup.test.other + + const val OTHER_CONSTANT = "abc" + """.trimIndent(), + mode = mode, + ) { + + assertThat(exitCode).isEqualTo(OK) + + val stringKey = injectClass.generatedBindingModule.methods.single() + .getDeclaredAnnotation(Named::class.java) + + assertThat(stringKey.value).isEqualTo("abc.foo") + } + } + + @Test + fun `a Named annotation using a private object's const property is inlined in the generated module`() { + + // https://github.com/square/anvil/issues/938 + + compile( + """ + package com.squareup.test + + import com.squareup.anvil.annotations.ContributesBinding + import com.squareup.test.other.OTHER_CONSTANT + import javax.inject.Inject + import javax.inject.Named + + private object Constants { + const val CONSTANT = "${'$'}OTHER_CONSTANT.foo" + } + + interface ParentInterface + + @Named(Constants.CONSTANT) + @ContributesBinding(Any::class) + class InjectClass @Inject constructor() : ParentInterface + """, + """ + package com.squareup.test.other + + const val OTHER_CONSTANT = "abc" + """.trimIndent(), + mode = mode, + ) { + + assertThat(exitCode).isEqualTo(OK) + + val Named = injectClass.generatedBindingModule.methods.single() + .getDeclaredAnnotation(Named::class.java) + + assertThat(Named.value).isEqualTo("abc.foo") + } + } + + @Test + fun `a Named annotation using a private companion object's const property is inlined in the generated module`() { + + // https://github.com/square/anvil/issues/938 + + compile( + """ + package com.squareup.test + + import com.squareup.anvil.annotations.ContributesBinding + import com.squareup.test.other.OTHER_CONSTANT + import javax.inject.Inject + import javax.inject.Named + + private interface Settings { + companion object { + const val CONSTANT = "${'$'}OTHER_CONSTANT.foo" + } + } + + interface ParentInterface + + @Named(Settings.CONSTANT) + @ContributesBinding(Any::class) + class InjectClass @Inject constructor() : ParentInterface + """, + """ + package com.squareup.test.other + + const val OTHER_CONSTANT = "abc" + """.trimIndent(), + mode = mode, + ) { + + assertThat(exitCode).isEqualTo(OK) + + val Named = injectClass.generatedBindingModule.methods.single() + .getDeclaredAnnotation(Named::class.java) + + assertThat(Named.value).isEqualTo("abc.foo") + } + } + @Test fun `there is a binding module for a contributed binding for an object`() { compile( """ diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesMultibindingGeneratorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesMultibindingGeneratorTest.kt index 25c16b18a..e696537c9 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesMultibindingGeneratorTest.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesMultibindingGeneratorTest.kt @@ -6,7 +6,9 @@ import com.squareup.anvil.compiler.assertFileGenerated import com.squareup.anvil.compiler.codegen.ksp.simpleSymbolProcessor import com.squareup.anvil.compiler.compile import com.squareup.anvil.compiler.contributingInterface +import com.squareup.anvil.compiler.generatedMultiBindingModule import com.squareup.anvil.compiler.includeKspTests +import com.squareup.anvil.compiler.injectClass import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode import com.squareup.anvil.compiler.internal.testing.simpleCodeGenerator import com.squareup.anvil.compiler.isError @@ -15,6 +17,7 @@ import com.squareup.anvil.compiler.multibindingModuleScope import com.squareup.anvil.compiler.multibindingModuleScopes import com.squareup.anvil.compiler.multibindingOriginClass import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK +import dagger.multibindings.StringKey import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -208,6 +211,129 @@ class ContributesMultibindingGeneratorTest( } } + @Test + fun `a StringKey annotation using a top-level const property is inlined in the generated module`() { + + // https://github.com/square/anvil/issues/938 + + compile( + """ + package com.squareup.test + + import com.squareup.anvil.annotations.ContributesMultibinding + import dagger.multibindings.StringKey + import com.squareup.test.other.OTHER_CONSTANT + import javax.inject.Inject + + interface ParentInterface + + private const val CONSTANT = "${'$'}OTHER_CONSTANT.foo" + + @StringKey(CONSTANT) + @ContributesMultibinding(Any::class) + class InjectClass @Inject constructor() : ParentInterface + """, + """ + package com.squareup.test.other + + const val OTHER_CONSTANT = "abc" + """.trimIndent(), + mode = mode, + ) { + + assertThat(exitCode).isEqualTo(OK) + + val stringKey = injectClass.generatedMultiBindingModule.methods.single() + .getDeclaredAnnotation(StringKey::class.java) + + assertThat(stringKey.value).isEqualTo("abc.foo") + } + } + + @Test + fun `a StringKey annotation using an object's const property is inlined in the generated module`() { + + // https://github.com/square/anvil/issues/938 + + compile( + """ + package com.squareup.test + + import com.squareup.anvil.annotations.ContributesMultibinding + import dagger.multibindings.StringKey + import com.squareup.test.other.OTHER_CONSTANT + import javax.inject.Inject + + private object Constants { + const val CONSTANT = "${'$'}OTHER_CONSTANT.foo" + } + + interface ParentInterface + + @StringKey(Constants.CONSTANT) + @ContributesMultibinding(Any::class) + class InjectClass @Inject constructor() : ParentInterface + """, + """ + package com.squareup.test.other + + const val OTHER_CONSTANT = "abc" + """.trimIndent(), + mode = mode, + ) { + + assertThat(exitCode).isEqualTo(OK) + + val stringKey = injectClass.generatedMultiBindingModule.methods.single() + .getDeclaredAnnotation(StringKey::class.java) + + assertThat(stringKey.value).isEqualTo("abc.foo") + } + } + + @Test + fun `a StringKey annotation using a companion object's const property is inlined in the generated module`() { + + // https://github.com/square/anvil/issues/938 + + compile( + """ + package com.squareup.test + + import com.squareup.anvil.annotations.ContributesMultibinding + import dagger.multibindings.StringKey + import com.squareup.test.other.OTHER_CONSTANT + import javax.inject.Inject + + private interface Settings { + companion object { + const val CONSTANT = "${'$'}OTHER_CONSTANT.foo" + } + } + + interface ParentInterface + + @StringKey(Settings.CONSTANT) + @ContributesMultibinding(Any::class) + class InjectClass @Inject constructor() : ParentInterface + """, + """ + package com.squareup.test.other + + const val OTHER_CONSTANT = "abc" + """.trimIndent(), + mode = mode, + ) { + + assertThat(exitCode).isEqualTo(OK) + + val stringKey = injectClass.generatedMultiBindingModule.methods.single() + .getDeclaredAnnotation(StringKey::class.java) + + assertThat(stringKey.value).isEqualTo("abc.foo") + } + } + @Test fun `contributed multibindings aren't allowed to have more than one qualifier`() { compile( """ @@ -401,6 +527,7 @@ class ContributesMultibindingGeneratorTest( } AnvilCompilationMode.Embedded(listOf(codeGenerator)) } + is AnvilCompilationMode.Ksp -> { val processor = simpleSymbolProcessor { resolver -> resolver.getSymbolsWithAnnotation(MergeComponent::class.qualifiedName!!) @@ -465,6 +592,7 @@ class ContributesMultibindingGeneratorTest( } AnvilCompilationMode.Embedded(listOf(codeGenerator)) } + is AnvilCompilationMode.Ksp -> { val processor = simpleSymbolProcessor { resolver -> resolver.getSymbolsWithAnnotation(MergeComponent::class.qualifiedName!!) diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/internal/reference/AnnotationReferenceTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/internal/reference/AnnotationReferenceTest.kt index ab49c012f..12ccaf9bc 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/internal/reference/AnnotationReferenceTest.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/internal/reference/AnnotationReferenceTest.kt @@ -4,10 +4,19 @@ import com.google.common.truth.Truth.assertThat import com.squareup.anvil.compiler.compile import com.squareup.anvil.compiler.internal.testing.simpleCodeGenerator import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK +import io.kotest.data.blocking.forAll +import io.kotest.data.row +import io.kotest.matchers.shouldBe import org.jetbrains.kotlin.name.FqName -import org.junit.Test +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestFactory + +@Suppress("LocalVariableName") +class AnnotationReferenceTest : ReferenceTests { + + override val testEnvironmentFactory + get() = ReferencesTestEnvironment.Factory -class AnnotationReferenceTest { @Test fun `annotation references with different arguments aren't equal`() { compile( """ @@ -99,6 +108,137 @@ class AnnotationReferenceTest { } } + @TestFactory + fun `a const as annotation argument can be parsed`() = testFactory { + compile( + """ + @file:Suppress("RedundantCompanionReference", "RedundantSuppression") + package com.squareup.test + + annotation class IntQualifier(val num: Int) + annotation class StringQualifier(val str: String) + + private const val ONE = 1 + + private const val TWO = ONE + ONE + private const val ABC = "abc.${'$'}ONE" + + @IntQualifier(TWO) + @StringQualifier(ABC) + interface SomeClass1 + + private object SomeObject { + const val THREE = ONE + TWO + const val DEF = "def.${'$'}ONE" + } + + @IntQualifier(SomeObject.THREE) + @StringQualifier(SomeObject.DEF) + interface SomeClass2 + + private class DefaultCompanionClass { + companion object { + const val FOUR = ONE + SomeObject.THREE + const val GHI = "ghi.${'$'}ONE" + } + } + + @IntQualifier(DefaultCompanionClass.FOUR) + @StringQualifier(DefaultCompanionClass.GHI) + interface SomeClass3 + + @IntQualifier(DefaultCompanionClass.Companion.FOUR) + @StringQualifier(DefaultCompanionClass.Companion.GHI) + interface SomeClass4 + + private class NamedCompanionClass { + companion object SomeCompanion { + const val FIVE = ONE + DefaultCompanionClass.FOUR + const val JKL = "jkl.${'$'}ONE" + } + } + + @IntQualifier(NamedCompanionClass.FIVE) + @StringQualifier(NamedCompanionClass.JKL) + interface SomeClass5 + + @IntQualifier(NamedCompanionClass.FIVE) + @StringQualifier(NamedCompanionClass.SomeCompanion.JKL) + interface SomeClass6 + """, + allWarningsAsErrors = false, + ) { + + fun ClassReference.stringValue() = annotations + .single { it.shortName == "StringQualifier" } + .arguments.single() + .value() + + fun ClassReference.stringAnnotationSpec() = annotations + .single { it.shortName == "StringQualifier" } + .toAnnotationSpec() + .toString() + + fun ClassReference.intValue() = annotations + .single { it.shortName == "IntQualifier" } + .arguments.single() + .value() + + fun ClassReference.intAnnotationSpec() = annotations + .single { it.shortName == "IntQualifier" } + .toAnnotationSpec() + .toString() + + val SomeClass1 by classReferenceMap + + SomeClass1.stringValue() shouldBe "abc.1" + SomeClass1.stringAnnotationSpec() shouldBe "@com.squareup.test.StringQualifier(str = \"abc.1\")" + + SomeClass1.intValue() shouldBe 2 + SomeClass1.intAnnotationSpec() shouldBe "@com.squareup.test.IntQualifier(num = 2)" + + val SomeClass2 by classReferenceMap + + SomeClass2.stringValue() shouldBe "def.1" + SomeClass2.stringAnnotationSpec() shouldBe "@com.squareup.test.StringQualifier(str = \"def.1\")" + + SomeClass2.intValue() shouldBe 3 + SomeClass2.intAnnotationSpec() shouldBe "@com.squareup.test.IntQualifier(num = 3)" + + val SomeClass3 by classReferenceMap + + SomeClass3.stringValue() shouldBe "ghi.1" + SomeClass3.stringAnnotationSpec() shouldBe "@com.squareup.test.StringQualifier(str = \"ghi.1\")" + + SomeClass3.intValue() shouldBe 4 + SomeClass3.intAnnotationSpec() shouldBe "@com.squareup.test.IntQualifier(num = 4)" + + val SomeClass4 by classReferenceMap + + SomeClass4.stringValue() shouldBe "ghi.1" + SomeClass4.stringAnnotationSpec() shouldBe "@com.squareup.test.StringQualifier(str = \"ghi.1\")" + + SomeClass4.intValue() shouldBe 4 + SomeClass4.intAnnotationSpec() shouldBe "@com.squareup.test.IntQualifier(num = 4)" + + val SomeClass5 by classReferenceMap + + SomeClass5.stringValue() shouldBe "jkl.1" + SomeClass5.stringAnnotationSpec() shouldBe "@com.squareup.test.StringQualifier(str = \"jkl.1\")" + + SomeClass5.intValue() shouldBe 5 + SomeClass5.intAnnotationSpec() shouldBe "@com.squareup.test.IntQualifier(num = 5)" + + val SomeClass6 by classReferenceMap + + SomeClass6.stringValue() shouldBe "jkl.1" + SomeClass6.stringAnnotationSpec() shouldBe "@com.squareup.test.StringQualifier(str = \"jkl.1\")" + + SomeClass6.intValue() shouldBe 5 + SomeClass6.intAnnotationSpec() shouldBe "@com.squareup.test.IntQualifier(num = 5)" + } + } + @Test fun `a string annotation argument can be parsed`() { compile( """ @@ -138,7 +278,8 @@ class AnnotationReferenceTest { } } - @Test fun `an int annotation argument can be parsed`() { + @TestFactory + fun `an int annotation argument can be parsed`() = testFactory { @Suppress("RemoveRedundantQualifierName") compile( """ @@ -222,79 +363,61 @@ class AnnotationReferenceTest { const val CONSTANT_5 = 5 """, allWarningsAsErrors = false, - codeGenerators = listOf( - simpleCodeGenerator { psiRef -> - if (psiRef.shortName in listOf("BindingKey", "SomeObject", "Abc", "Companion")) { - return@simpleCodeGenerator null - } + ) { - listOf(psiRef, psiRef.toDescriptorReference()).forEach { ref -> - when (psiRef.shortName) { - "SomeClass1" -> { - val argument = ref.annotations.single().arguments.single() - assertThat(argument.value()).isEqualTo(1) - - assertThat(ref.annotations.single().toAnnotationSpec().toString()) - .isEqualTo("@com.squareup.test.BindingKey(value = 1)") - } - "SomeClass2" -> { - val argument = ref.annotations.single().arguments.single() - assertThat(argument.value()).isEqualTo(1) - - assertThat(ref.annotations.single().toAnnotationSpec().toString()) - .isEqualTo("@com.squareup.test.BindingKey(value = 1)") - } - "SomeClass3" -> { - val argument = ref.annotations.single().arguments.single() - assertThat(argument.value()).isEqualTo(-5) - - assertThat(ref.annotations.single().toAnnotationSpec().toString()) - .isEqualTo("@com.squareup.test.BindingKey(value = -5)") - } - "SomeClass4", "SomeClass5", "SomeClass6" -> { - val argument = ref.annotations.single().arguments.single() - assertThat(argument.value()).isEqualTo(Int.MAX_VALUE) - - assertThat(ref.annotations.single().toAnnotationSpec().toString()) - .isEqualTo("@com.squareup.test.BindingKey(value = 2147483647)") - } - "SomeClass7", "SomeClass8" -> { - val argument = ref.annotations.single().arguments.single() - assertThat(argument.value()).isEqualTo(2) - - assertThat(ref.annotations.single().toAnnotationSpec().toString()) - .isEqualTo("@com.squareup.test.BindingKey(value = 2)") - } - "SomeClass9", "SomeClass10", "SomeClass11" -> { - val argument = ref.annotations.single().arguments.single() - assertThat(argument.value()).isEqualTo(3) - - assertThat(ref.annotations.single().toAnnotationSpec().toString()) - .isEqualTo("@com.squareup.test.BindingKey(value = 3)") - } - "SomeClass12", "SomeClass13", "SomeClass14" -> { - val argument = ref.annotations.single().arguments.single() - assertThat(argument.value()).isEqualTo(4) - - assertThat(ref.annotations.single().toAnnotationSpec().toString()) - .isEqualTo("@com.squareup.test.BindingKey(value = 4)") - } - "SomeClass15" -> { - val argument = ref.annotations.single().arguments.single() - assertThat(argument.value()).isEqualTo(5) - - assertThat(ref.annotations.single().toAnnotationSpec().toString()) - .isEqualTo("@com.squareup.test.BindingKey(value = 5)") - } - else -> throw NotImplementedError(psiRef.shortName) - } - } + fun ClassReference.annotationArg() = annotations.single().arguments.single().value() + fun ClassReference.annotationSpec() = annotations.single().toAnnotationSpec().toString() - null - }, - ), - ) { - assertThat(exitCode).isEqualTo(OK) + val SomeClass1 by classReferenceMap + SomeClass1.annotationArg() shouldBe 1 + SomeClass1.annotationSpec() shouldBe "@com.squareup.test.BindingKey(value = 1)" + + val SomeClass2 by classReferenceMap + SomeClass2.annotationArg() shouldBe 1 + SomeClass2.annotationSpec() shouldBe "@com.squareup.test.BindingKey(value = 1)" + + val SomeClass3 by classReferenceMap + SomeClass3.annotationArg() shouldBe -5 + SomeClass3.annotationSpec() shouldBe "@com.squareup.test.BindingKey(value = -5)" + + forAll( + row(classReferenceMap.getValue("SomeClass4")), + row(classReferenceMap.getValue("SomeClass5")), + row(classReferenceMap.getValue("SomeClass6")), + ) { ref -> + ref.annotationArg() shouldBe Int.MAX_VALUE + ref.annotationSpec() shouldBe "@com.squareup.test.BindingKey(value = 2147483647)" + } + + forAll( + row(classReferenceMap.getValue("SomeClass7")), + row(classReferenceMap.getValue("SomeClass8")), + ) { ref -> + ref.annotationArg() shouldBe 2 + ref.annotationSpec() shouldBe "@com.squareup.test.BindingKey(value = 2)" + } + + forAll( + row(classReferenceMap.getValue("SomeClass9")), + row(classReferenceMap.getValue("SomeClass10")), + row(classReferenceMap.getValue("SomeClass11")), + ) { ref -> + ref.annotationArg() shouldBe 3 + ref.annotationSpec() shouldBe "@com.squareup.test.BindingKey(value = 3)" + } + + forAll( + row(classReferenceMap.getValue("SomeClass12")), + row(classReferenceMap.getValue("SomeClass13")), + row(classReferenceMap.getValue("SomeClass14")), + ) { ref -> + ref.annotationArg() shouldBe 4 + ref.annotationSpec() shouldBe "@com.squareup.test.BindingKey(value = 4)" + } + + val SomeClass15 by classReferenceMap + SomeClass15.annotationArg() shouldBe 5 + SomeClass15.annotationSpec() shouldBe "@com.squareup.test.BindingKey(value = 5)" } } diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/internal/reference/ReferencesTestEnvironment.kt b/compiler/src/test/java/com/squareup/anvil/compiler/internal/reference/ReferencesTestEnvironment.kt new file mode 100644 index 000000000..ddf0f5901 --- /dev/null +++ b/compiler/src/test/java/com/squareup/anvil/compiler/internal/reference/ReferencesTestEnvironment.kt @@ -0,0 +1,174 @@ +package com.squareup.anvil.compiler.internal.reference + +import com.rickbusarow.kase.DefaultTestEnvironment +import com.rickbusarow.kase.KaseTestFactory +import com.rickbusarow.kase.ParamTestEnvironmentFactory +import com.rickbusarow.kase.files.HasWorkingDir +import com.rickbusarow.kase.files.JavaFileFileInjection +import com.rickbusarow.kase.files.LanguageInjection +import com.rickbusarow.kase.files.TestLocation +import com.squareup.anvil.compiler.api.AnvilContext +import com.squareup.anvil.compiler.api.CodeGenerator +import com.squareup.anvil.compiler.api.FileWithContent +import com.squareup.anvil.compiler.compile +import com.squareup.anvil.compiler.internal.reference.ReferencesTestEnvironment.ReferenceType +import com.tschuchort.compiletesting.KotlinCompilation +import io.kotest.assertions.ErrorCollectionMode +import io.kotest.assertions.ErrorCollector +import io.kotest.assertions.collectiveError +import io.kotest.assertions.errorCollector +import io.kotest.assertions.pushErrors +import io.kotest.assertions.throwCollectedErrors +import io.kotest.matchers.shouldBe +import org.intellij.lang.annotations.Language +import org.jetbrains.kotlin.psi.KtFile +import java.io.File +import kotlin.reflect.KProperty + +interface ReferenceTests : + KaseTestFactory { + + override val testEnvironmentFactory get() = ReferencesTestEnvironment.Factory + + override val params: List + get() = listOf(ReferenceType.Psi, ReferenceType.Descriptor) +} + +class ReferencesTestEnvironment( + val referenceType: ReferenceType, + hasWorkingDir: HasWorkingDir, +) : DefaultTestEnvironment(hasWorkingDir = hasWorkingDir), + LanguageInjection by LanguageInjection(JavaFileFileInjection()) { + + lateinit var typeReferenceMap: Map + private set + + lateinit var classReferenceMap: Map + private set + + operator fun List.getValue( + thisRef: Any?, + property: KProperty<*>, + ): E = single { it.name == property.name } + + operator fun Map.getValue( + thisRef: Any?, + property: KProperty<*>, + ): E = getValue(property.name) + + fun compile( + @Language("kotlin") content: String, + @Language("kotlin") vararg additionalContent: String, + allWarningsAsErrors: Boolean = false, + testAction: CodeGenerator.() -> Unit, + ) { + + compile( + content, + *additionalContent, + allWarningsAsErrors = allWarningsAsErrors, + workingDir = workingDir, + codeGenerators = listOf( + object : CodeGenerator { + override fun isApplicable(context: AnvilContext): Boolean = true + override fun generateCode( + codeGenDir: File, + module: org.jetbrains.kotlin.descriptors.ModuleDescriptor, + projectFiles: Collection, + ): Collection { + + errorCollector.collectErrors { + val classes = projectFiles.classAndInnerClassReferences(module) + + classReferenceMap = classes.associateBy { it.shortName } + + classes.singleOrNull { it.shortName == "RefsContainer" } + ?.let { refsContainer -> + + val refsFun = when (referenceType) { + ReferenceType.Psi -> refsContainer.functions + ReferenceType.Descriptor -> refsContainer.toDescriptorReference().functions + } + .singleOrNull { it.name == "refs" } + ?: error { + "RefsContainer.refs not found. " + + "Existing functions: ${refsContainer.functions.map { it.name }}" + } + + typeReferenceMap = when (referenceType) { + ReferenceType.Psi -> refsFun.parameters.associate { it.name to it.type() } + ReferenceType.Descriptor -> refsFun.parameters.associate { it.name to it.type() } + } + } + } + + testAction() + + return emptyList() + } + }, + ), + ) { + errorCollector.throwCollectedErrors() + + exitCode shouldBe KotlinCompilation.ExitCode.OK + } + } + + enum class ReferenceType { + Psi, + Descriptor, + } + + companion object Factory : ParamTestEnvironmentFactory { + + private fun readResolve(): Any = Factory + override fun create( + params: ReferenceType, + names: List, + location: TestLocation, + ): ReferencesTestEnvironment = ReferencesTestEnvironment( + referenceType = params, + HasWorkingDir.invoke( + testVariantNames = names.map { name -> + // Any '?' characters are replaced with '_' for working directory names, + // so we preempt that with '__nullable_' + // so that tests that use type names still get unique working directories. + name.replace("?", "__nullable_") + }, + testLocation = location, + ), + ) + } +} + +inline fun ErrorCollector.collectErrors(assertions: () -> Unit) { + val errorCollector = this + + if (errorCollector.getCollectionMode() == ErrorCollectionMode.Soft) { + val oldErrors = errorCollector.errors() + errorCollector.clear() + errorCollector.depth++ + + return try { + assertions() + } catch (t: Throwable) { + errorCollector.pushError(t) + } finally { + val aggregated = errorCollector.collectiveError() + errorCollector.clear() + errorCollector.pushErrors(oldErrors) + aggregated?.let { errorCollector.pushError(it) } + errorCollector.depth-- + } + } + + errorCollector.setCollectionMode(ErrorCollectionMode.Soft) + return try { + assertions() + } catch (t: Throwable) { + errorCollector.pushError(t) + } finally { + errorCollector.setCollectionMode(ErrorCollectionMode.Hard) + } +}