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) + } +}