diff --git a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/AnnotationReference.kt b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/AnnotationReference.kt index 5fbd583c4..bbc4b0869 100644 --- a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/AnnotationReference.kt +++ b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/AnnotationReference.kt @@ -172,7 +172,7 @@ public sealed class AnnotationReference { public class Psi internal constructor( public val annotation: KtAnnotationEntry, override val classReference: ClassReference, - override val declaringClass: ClassReference.Psi?, + override val declaringClass: ClassReference?, ) : AnnotationReference() { override val arguments: List by lazy(NONE) { @@ -198,7 +198,7 @@ public sealed class AnnotationReference { public class Descriptor internal constructor( public val annotation: AnnotationDescriptor, override val classReference: ClassReference, - override val declaringClass: ClassReference.Descriptor?, + override val declaringClass: ClassReference?, ) : AnnotationReference() { override val arguments: List by lazy(NONE) { @@ -221,10 +221,21 @@ public sealed class AnnotationReference { public fun KtAnnotationEntry.toAnnotationReference( declaringClass: ClassReference.Psi?, module: ModuleDescriptor, +): Psi { + return toAnnotationReference( + classReference = requireFqName(module).toClassReference(module), + declaringClass = declaringClass, + ) +} + +@ExperimentalAnvilApi +public fun KtAnnotationEntry.toAnnotationReference( + declaringClass: ClassReference?, + classReference: ClassReference, ): Psi { return Psi( annotation = this, - classReference = requireFqName(module).toClassReference(module), + classReference = classReference, declaringClass = declaringClass, ) } @@ -237,10 +248,17 @@ public fun AnnotationDescriptor.toAnnotationReference( val annotationClass = annotationClass ?: throw AnvilCompilationException( message = "Couldn't find the annotation class for $fqName", ) + return toAnnotationReference(declaringClass, annotationClass.toClassReference(module)) +} +@ExperimentalAnvilApi +public fun AnnotationDescriptor.toAnnotationReference( + declaringClass: ClassReference?, + classReference: ClassReference, +): Descriptor { return Descriptor( annotation = this, - classReference = annotationClass.toClassReference(module), + classReference = classReference, declaringClass = declaringClass, ) } diff --git a/compiler-utils/src/testFixtures/java/com/squareup/anvil/compiler/internal/testing/AnvilCompilation.kt b/compiler-utils/src/testFixtures/java/com/squareup/anvil/compiler/internal/testing/AnvilCompilation.kt index 272515e83..0a71a6eee 100644 --- a/compiler-utils/src/testFixtures/java/com/squareup/anvil/compiler/internal/testing/AnvilCompilation.kt +++ b/compiler-utils/src/testFixtures/java/com/squareup/anvil/compiler/internal/testing/AnvilCompilation.kt @@ -16,6 +16,7 @@ import com.tschuchort.compiletesting.kspArgs import com.tschuchort.compiletesting.kspWithCompilation import com.tschuchort.compiletesting.symbolProcessorProviders import dagger.internal.codegen.ComponentProcessor +import dagger.internal.codegen.KspComponentProcessor import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.config.JvmTarget import java.io.File @@ -39,7 +40,7 @@ public class AnvilCompilation internal constructor( @Suppress("SuspiciousCollectionReassignment") @ExperimentalAnvilApi public fun configureAnvil( - enableDaggerAnnotationProcessor: Boolean = false, + daggerAnnotationProcessingMode: DaggerAnnotationProcessingMode = DaggerAnnotationProcessingMode.NONE, generateDaggerFactories: Boolean = false, generateDaggerFactoriesOnly: Boolean = false, disableComponentMerging: Boolean = false, @@ -59,8 +60,25 @@ public class AnvilCompilation internal constructor( // Deprecation tracked in https://github.com/square/anvil/issues/672 @Suppress("DEPRECATION") componentRegistrars = listOf(anvilComponentRegistrar) - if (enableDaggerAnnotationProcessor) { - annotationProcessors = listOf(ComponentProcessor(), AutoAnnotationProcessor()) + + when (daggerAnnotationProcessingMode) { + DaggerAnnotationProcessingMode.KAPT -> { + annotationProcessors = listOf(ComponentProcessor(), AutoAnnotationProcessor()) + } + DaggerAnnotationProcessingMode.KSP -> { + @Suppress("invisible_reference", "invisible_member") + symbolProcessorProviders += + listOf( + com.squareup.anvil.compiler.ksp.MergeComponentSymbolProcessor.Provider(), + KspComponentProcessor.Provider(), + ) + // Run KSP in a single-pass + // https://kotlinlang.slack.com/archives/C013BA8EQSE/p1639462548225400?thread_ts=1639433474.224900&cid=C013BA8EQSE + kspWithCompilation = true + } + DaggerAnnotationProcessingMode.NONE -> { + // Do nothing + } } val anvilCommandLineProcessor = AnvilCommandLineProcessor() @@ -104,10 +122,13 @@ public class AnvilCompilation internal constructor( is Ksp -> { symbolProcessorProviders += buildList { addAll( - ServiceLoader.load( - SymbolProcessorProvider::class.java, - SymbolProcessorProvider::class.java.classLoader, - ), + ServiceLoader + .load( + SymbolProcessorProvider::class.java, + SymbolProcessorProvider::class.java.classLoader, + ) + // Exclude the Dagger KSP processor, we have special handling for that as we decorate it + .filterNot { it is KspComponentProcessor.Provider }, ) addAll(mode.symbolProcessorProviders) } @@ -219,6 +240,13 @@ public class AnvilCompilation internal constructor( } } +/** Available Dagger annotation processing modes. */ +enum class DaggerAnnotationProcessingMode { + KAPT, + KSP, + NONE, +} + /** * Helpful for testing code generators in unit tests end to end. * @@ -229,7 +257,7 @@ public class AnvilCompilation internal constructor( @ExperimentalAnvilApi public fun compileAnvil( @Language("kotlin") vararg sources: String, - enableDaggerAnnotationProcessor: Boolean = false, + daggerAnnotationProcessingMode: DaggerAnnotationProcessingMode = DaggerAnnotationProcessingMode.NONE, generateDaggerFactories: Boolean = false, generateDaggerFactoriesOnly: Boolean = false, disableComponentMerging: Boolean = false, @@ -243,7 +271,8 @@ public fun compileAnvil( jvmTarget: JvmTarget? = null, block: JvmCompilationResult.() -> Unit = { }, ): JvmCompilationResult { - return AnvilCompilation() + val compilation = AnvilCompilation() + return compilation .apply { kotlinCompilation.apply { this.allWarningsAsErrors = allWarningsAsErrors @@ -265,7 +294,7 @@ public fun compileAnvil( } } .configureAnvil( - enableDaggerAnnotationProcessor = enableDaggerAnnotationProcessor, + daggerAnnotationProcessingMode = daggerAnnotationProcessingMode, generateDaggerFactories = generateDaggerFactories, generateDaggerFactoriesOnly = generateDaggerFactoriesOnly, disableComponentMerging = disableComponentMerging, @@ -273,5 +302,7 @@ public fun compileAnvil( mode = mode, ) .compile(*sources) - .also(block) + .also { result -> + result.block() + } } diff --git a/compiler/build.gradle.kts b/compiler/build.gradle.kts index 262b96180..3239f4bca 100644 --- a/compiler/build.gradle.kts +++ b/compiler/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(project(":compiler-api")) implementation(project(":compiler-utils")) implementation(libs.dagger2) + implementation(libs.dagger2.compiler) implementation(libs.jsr250) implementation(libs.kotlinpoet) implementation(libs.kotlinpoet.ksp) @@ -47,16 +48,21 @@ dependencies { compileOnly(libs.kotlin.compiler) compileOnly(libs.ksp.compilerPlugin) compileOnly(libs.ksp.api) + // just for reference to get underlying APIs + compileOnly(libs.ksp.compilerPlugin) kapt(libs.auto.service.processor) testImplementation(testFixtures(project(":compiler-utils"))) - testImplementation(libs.dagger2.compiler) testImplementation(libs.kotlin.annotationProcessingEmbeddable) testImplementation(libs.kotlin.compileTesting) testImplementation(libs.kotlin.compileTesting.ksp) testImplementation(libs.ksp.compilerPlugin) testImplementation(libs.kotlin.compiler) testImplementation(libs.kotlin.test) + testImplementation(libs.ksp.api) + testImplementation(libs.ksp.compilerPlugin) testImplementation(libs.truth) + // Force latest guava version for alignment with dagger's versions + testImplementation(libs.guava) } diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/InterfaceMerger.kt b/compiler/src/main/java/com/squareup/anvil/compiler/InterfaceMerger.kt index 9d11e3088..fa7202ff8 100644 --- a/compiler/src/main/java/com/squareup/anvil/compiler/InterfaceMerger.kt +++ b/compiler/src/main/java/com/squareup/anvil/compiler/InterfaceMerger.kt @@ -7,6 +7,7 @@ import com.squareup.anvil.compiler.codegen.generatedAnvilSubcomponent import com.squareup.anvil.compiler.codegen.parentScope import com.squareup.anvil.compiler.codegen.reference.RealAnvilModuleDescriptor import com.squareup.anvil.compiler.internal.classDescriptor +import com.squareup.anvil.compiler.internal.reference.AnnotationReference import com.squareup.anvil.compiler.internal.reference.AnvilCompilationExceptionClassReference import com.squareup.anvil.compiler.internal.reference.ClassReference import com.squareup.anvil.compiler.internal.reference.Visibility.PUBLIC @@ -40,171 +41,216 @@ internal class InterfaceMerger( .findAll(mergeComponentFqName, mergeSubcomponentFqName, mergeInterfacesFqName) .ifEmpty { return } - if (!mergeAnnotatedClass.isInterface()) { - throw AnvilCompilationExceptionClassReference( - classReference = mergeAnnotatedClass, - message = "Dagger components must be interfaces.", - ) - } + val result = mergeInterfaces( + classScanner = classScanner, + module = module, + mergeAnnotatedClass = mergeAnnotatedClass, + annotations = mergeAnnotations, + supertypes = supertypes + .map { it.classDescriptor().toClassReference(module) }, + ) - val scopes = mergeAnnotations.map { - try { - it.scope() - } catch (e: AssertionError) { - // In some scenarios this error is thrown. Throw a new exception with a better explanation. - // Caused by: java.lang.AssertionError: Recursion detected in a lazy value under - // LockBasedStorageManager@420989af (TopDownAnalyzer for JVM) + @Suppress("ConvertCallChainIntoSequence") + supertypes += result.contributesAnnotations + .map { it.declaringClass() } + .filter { clazz -> + clazz !in result.replacedClasses && clazz !in result.excludedClasses + } + .plus(result.contributedSubcomponentInterfaces()) + // Avoids an error for repeated interfaces. + .distinct() + .map { (it as ClassReference.Descriptor).clazz.defaultType } + } + + data class MergeResult( + val contributesAnnotations: List, + val replacedClasses: Set, + val excludedClasses: List, + val contributedSubcomponentInterfaces: () -> Sequence, + ) + + companion object { + fun mergeInterfaces( + classScanner: ClassScanner, + module: RealAnvilModuleDescriptor, + mergeAnnotatedClass: ClassReference, + annotations: List, + supertypes: List, + ): MergeResult { + if (!mergeAnnotatedClass.isInterface()) { throw AnvilCompilationExceptionClassReference( classReference = mergeAnnotatedClass, - message = "It seems like you tried to contribute an inner class to its outer class. " + - "This is not supported and results in a compiler error.", - cause = e, + message = "Dagger components must be interfaces.", ) } - } - val contributesAnnotations = mergeAnnotations - .flatMap { annotation -> - classScanner - .findContributedClasses( - module = module, - annotation = contributesToFqName, - scope = annotation.scope().fqName, - ) - } - .filter { clazz -> - clazz.isInterface() && clazz.annotations.find(daggerModuleFqName).singleOrNull() == null - } - .flatMap { clazz -> - clazz.annotations - .find(annotationName = contributesToFqName) - .filter { it.scope() in scopes } - } - .onEach { contributeAnnotation -> - val contributedClass = contributeAnnotation.declaringClass() - if (contributedClass.visibility() != PUBLIC) { + val scopes = annotations.map { + try { + it.scope() + } catch (e: AssertionError) { + // In some scenarios this error is thrown. Throw a new exception with a better explanation. + // Caused by: java.lang.AssertionError: Recursion detected in a lazy value under + // LockBasedStorageManager@420989af (TopDownAnalyzer for JVM) throw AnvilCompilationExceptionClassReference( - classReference = contributedClass, - message = "${contributedClass.fqName} is contributed to the Dagger graph, but the " + - "interface is not public. Only public interfaces are supported.", + classReference = mergeAnnotatedClass, + message = "It seems like you tried to contribute an inner class to its outer class. " + + "This is not supported and results in a compiler error.", + cause = e, ) } } - // Convert the sequence to a list to avoid iterating it twice. We use the result twice - // for replaced classes and the final result. - .toList() - - val replacedClasses = contributesAnnotations - .flatMap { contributeAnnotation -> - val contributedClass = contributeAnnotation.declaringClass() - contributedClass - .atLeastOneAnnotation(contributeAnnotation.fqName) - .flatMap { it.replaces() } - .onEach { classToReplace -> - // Verify the other class is an interface. It doesn't make sense for a contributed - // interface to replace a class that is not an interface. - if (!classToReplace.isInterface()) { - throw AnvilCompilationExceptionClassReference( - classReference = contributedClass, - message = "${contributedClass.fqName} wants to replace " + - "${classToReplace.fqName}, but the class being " + - "replaced is not an interface.", - ) - } - val contributesToOurScope = classToReplace.annotations - .findAll( - contributesToFqName, - contributesBindingFqName, - contributesMultibindingFqName, - ) - .map { it.scope() } - .any { scope -> scope in scopes } - - if (!contributesToOurScope) { - throw AnvilCompilationExceptionClassReference( - classReference = contributedClass, - message = "${contributedClass.fqName} with scopes " + - "${scopes.joinToString(prefix = "[", postfix = "]") { it.fqName.asString() }} " + - "wants to replace ${classToReplace.fqName}, but the replaced class isn't " + - "contributed to the same scope.", - ) + val contributesAnnotations = annotations + .flatMap { annotation -> + classScanner + .findContributedClasses( + module = module, + annotation = contributesToFqName, + scope = annotation.scope().fqName, + ) + } + .filter { clazz -> + clazz.isInterface() && clazz.annotations.find(daggerModuleFqName).singleOrNull() == null + } + .flatMap { clazz -> + clazz.annotations + .find(annotationName = contributesToFqName) + .filter { it.scope() in scopes } + } + .onEach { contributeAnnotation -> + val contributedClass = contributeAnnotation.declaringClass() + if (contributedClass.visibility() != PUBLIC) { + throw AnvilCompilationExceptionClassReference( + classReference = contributedClass, + message = "${contributedClass.fqName} is contributed to the Dagger graph, but the " + + "interface is not public. Only public interfaces are supported.", + ) + } + } + // Convert the sequence to a list to avoid iterating it twice. We use the result twice + // for replaced classes and the final result. + .toList() + + val replacedClasses = contributesAnnotations + .flatMap { contributeAnnotation -> + val contributedClass = contributeAnnotation.declaringClass() + contributedClass + .atLeastOneAnnotation(contributeAnnotation.fqName) + .flatMap { it.replaces() } + .onEach { classToReplace -> + // Verify the other class is an interface. It doesn't make sense for a contributed + // interface to replace a class that is not an interface. + if (!classToReplace.isInterface()) { + throw AnvilCompilationExceptionClassReference( + classReference = contributedClass, + message = "${contributedClass.fqName} wants to replace " + + "${classToReplace.fqName}, but the class being " + + "replaced is not an interface.", + ) + } + + val contributesToOurScope = classToReplace.annotations + .findAll( + contributesToFqName, + contributesBindingFqName, + contributesMultibindingFqName, + ) + .map { it.scope() } + .any { scope -> scope in scopes } + + if (!contributesToOurScope) { + val scopesString = scopes.joinToString(prefix = "[", postfix = "]") { + it.fqName.asString() + } + throw AnvilCompilationExceptionClassReference( + classReference = contributedClass, + message = "${contributedClass.fqName} with scopes " + + "$scopesString " + + "wants to replace ${classToReplace.fqName}, but the replaced class isn't " + + "contributed to the same scope.", + ) + } } + } + .toSet() + + val excludedClasses = annotations + .flatMap { it.exclude() } + .filter { it.isInterface() } + .onEach { excludedClass -> + // Verify that the replaced classes use the same scope. + val contributesToOurScope = excludedClass.annotations + .findAll(contributesToFqName, contributesBindingFqName, contributesMultibindingFqName) + .map { it.scope() } + .plus( + excludedClass.annotations.find( + contributesSubcomponentFqName, + ).map { it.parentScope() }, + ) + .any { scope -> scope in scopes } + + if (!contributesToOurScope) { + throw AnvilCompilationExceptionClassReference( + message = "${mergeAnnotatedClass.fqName} with scopes " + + "${scopes.joinToString(prefix = "[", postfix = "]") { it.fqName.asString() }} " + + "wants to exclude ${excludedClass.fqName}, but the excluded class isn't " + + "contributed to the same scope.", + classReference = mergeAnnotatedClass, + ) } - } - .toSet() - - val excludedClasses = mergeAnnotations - .flatMap { it.exclude() } - .filter { it.isInterface() } - .onEach { excludedClass -> - // Verify that the replaced classes use the same scope. - val contributesToOurScope = excludedClass.annotations - .findAll(contributesToFqName, contributesBindingFqName, contributesMultibindingFqName) - .map { it.scope() } - .plus( - excludedClass.annotations.find(contributesSubcomponentFqName).map { it.parentScope() }, - ) - .any { scope -> scope in scopes } + } + + if (excludedClasses.isNotEmpty()) { + val intersect = supertypes + .flatMap { it.allSuperTypeClassReferences(includeSelf = true) } + .intersect(excludedClasses.toSet()) - if (!contributesToOurScope) { + if (intersect.isNotEmpty()) { throw AnvilCompilationExceptionClassReference( - message = "${mergeAnnotatedClass.fqName} with scopes " + - "${scopes.joinToString(prefix = "[", postfix = "]") { it.fqName.asString() }} " + - "wants to exclude ${excludedClass.fqName}, but the excluded class isn't " + - "contributed to the same scope.", classReference = mergeAnnotatedClass, + message = "${mergeAnnotatedClass.fqName} excludes types that it implements or " + + "extends. These types cannot be excluded. Look at all the super" + + " types to find these classes: " + + "${intersect.joinToString { it.fqName.asString() }}.", ) } } + return MergeResult( + contributesAnnotations = contributesAnnotations, + replacedClasses = replacedClasses, + excludedClasses = excludedClasses, + contributedSubcomponentInterfaces = { + findContributedSubcomponentParentInterfaces( + classScanner = classScanner, + clazz = mergeAnnotatedClass, + scopes = scopes, + module = module, + ) + }, + ) + } - if (excludedClasses.isNotEmpty()) { - val intersect = supertypes - .map { it.classDescriptor().toClassReference(module) } - .flatMap { it.allSuperTypeClassReferences(includeSelf = true) } - .intersect(excludedClasses.toSet()) - - if (intersect.isNotEmpty()) { - throw AnvilCompilationExceptionClassReference( - classReference = mergeAnnotatedClass, - message = "${mergeAnnotatedClass.fqName} excludes types that it implements or " + - "extends. These types cannot be excluded. Look at all the super types to find these " + - "classes: ${intersect.joinToString { it.fqName.asString() }}.", + private fun findContributedSubcomponentParentInterfaces( + classScanner: ClassScanner, + clazz: ClassReference, + scopes: Collection, + module: ModuleDescriptor, + ): Sequence { + return classScanner + .findContributedClasses( + module = module, + annotation = contributesSubcomponentFqName, + scope = null, ) - } + .filter { + it.atLeastOneAnnotation(contributesSubcomponentFqName).single().parentScope() in scopes + } + .mapNotNull { contributedSubcomponent -> + contributedSubcomponent.classId + .generatedAnvilSubcomponent(clazz.classId) + .createNestedClassId(Name.identifier(PARENT_COMPONENT)) + .classReferenceOrNull(module) + } } - - @Suppress("ConvertCallChainIntoSequence") - supertypes += contributesAnnotations - .map { it.declaringClass() } - .filter { clazz -> - clazz !in replacedClasses && clazz !in excludedClasses - } - .plus(findContributedSubcomponentParentInterfaces(mergeAnnotatedClass, scopes, module)) - // Avoids an error for repeated interfaces. - .distinct() - .map { it.clazz.defaultType } - } - - private fun findContributedSubcomponentParentInterfaces( - clazz: ClassReference, - scopes: Collection, - module: ModuleDescriptor, - ): Sequence { - return classScanner - .findContributedClasses( - module = module, - annotation = contributesSubcomponentFqName, - scope = null, - ) - .filter { - it.atLeastOneAnnotation(contributesSubcomponentFqName).single().parentScope() in scopes - } - .mapNotNull { contributedSubcomponent -> - contributedSubcomponent.classId - .generatedAnvilSubcomponent(clazz.classId) - .createNestedClassId(Name.identifier(PARENT_COMPONENT)) - .classReferenceOrNull(module) - } } } diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ksp/KSAnnotationExtensions.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ksp/KSAnnotationExtensions.kt index 1497e8fc0..88c5b58ee 100644 --- a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ksp/KSAnnotationExtensions.kt +++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ksp/KSAnnotationExtensions.kt @@ -10,6 +10,7 @@ import com.google.devtools.ksp.symbol.KSValueArgument import com.squareup.anvil.compiler.internal.daggerScopeFqName import com.squareup.anvil.compiler.internal.mapKeyFqName import com.squareup.anvil.compiler.isAnvilModule +import com.squareup.anvil.compiler.ksp.classDeclaration import com.squareup.anvil.compiler.qualifierFqName import com.squareup.kotlinpoet.ksp.toClassName import org.jetbrains.kotlin.name.FqName @@ -80,15 +81,15 @@ internal fun List.checkNoDuplicateScopeAndBoundType( } } -internal fun KSAnnotation.scope(): KSType = +internal fun KSAnnotation.scope(): KSClassDeclaration = scopeOrNull() ?: throw KspAnvilException( message = "Couldn't find scope for ${annotationType.resolve().declaration.qualifiedName}.", this, ) -internal fun KSAnnotation.scopeOrNull(): KSType? { - return argumentAt("scope")?.value as? KSType? +internal fun KSAnnotation.scopeOrNull(): KSClassDeclaration? { + return (argumentAt("scope")?.value as? KSType?)?.classDeclaration } internal fun KSAnnotation.boundTypeOrNull(): KSType? = argumentAt("boundType")?.value as? KSType? diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/ksp/ClassScannerKSP.kt b/compiler/src/main/java/com/squareup/anvil/compiler/ksp/ClassScannerKSP.kt new file mode 100644 index 000000000..b076eca63 --- /dev/null +++ b/compiler/src/main/java/com/squareup/anvil/compiler/ksp/ClassScannerKSP.kt @@ -0,0 +1,156 @@ +package com.squareup.anvil.compiler.ksp + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.impl.ResolverImpl +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.squareup.anvil.compiler.HINT_BINDING_PACKAGE_PREFIX +import com.squareup.anvil.compiler.HINT_CONTRIBUTES_PACKAGE_PREFIX +import com.squareup.anvil.compiler.HINT_MULTIBINDING_PACKAGE_PREFIX +import com.squareup.anvil.compiler.HINT_SUBCOMPONENTS_PACKAGE_PREFIX +import com.squareup.anvil.compiler.REFERENCE_SUFFIX +import com.squareup.anvil.compiler.SCOPE_SUFFIX +import com.squareup.anvil.compiler.api.AnvilCompilationException +import com.squareup.anvil.compiler.codegen.ksp.scope +import com.squareup.anvil.compiler.contributesBindingFqName +import com.squareup.anvil.compiler.contributesMultibindingFqName +import com.squareup.anvil.compiler.contributesSubcomponentFqName +import com.squareup.anvil.compiler.contributesToFqName +import com.squareup.anvil.compiler.ksp.GeneratedProperty.ReferenceProperty +import com.squareup.anvil.compiler.ksp.GeneratedProperty.ScopeProperty +import com.squareup.anvil.compiler.singleOrEmpty +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.descriptors.PackageViewDescriptor +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull +import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter + +@OptIn(KspExperimental::class) +internal class ClassScannerKSP { + + private val cache = mutableMapOf>>() + + /** + * Returns a sequence of contributed classes from the dependency graph. + */ + fun findContributedClasses( + resolver: Resolver, + annotation: FqName, + scope: KSClassDeclaration?, + ): Sequence { + val module = (resolver as ResolverImpl).module + val propertyGroups = cache.getOrPut(CacheKey(annotation, module.hashCode())) { + module.hintPackages(annotation) + .flatMap { resolver.getDeclarationsFromPackage(it) } + .filterIsInstance() + .mapNotNull { GeneratedProperty.fromDeclaration(it) } + .groupBy { property -> property.baseName } + .values + } + + return propertyGroups + .asSequence() + .mapNotNull { properties -> + val reference = properties.filterIsInstance() + // In some rare cases we can see a generated property for the same identifier. + // Filter them just in case, see https://github.com/square/anvil/issues/460 and + // https://github.com/square/anvil/issues/565 + .distinctBy { it.baseName } + .singleOrEmpty() + ?: throw AnvilCompilationException( + message = "Couldn't find the reference for a generated hint: ${properties[0].baseName}.", + ) + + val scopes = properties.filterIsInstance() + .ifEmpty { + throw AnvilCompilationException( + message = "Couldn't find any scope for a generated hint: ${properties[0].baseName}.", + ) + } + .map { it.declaration.type.singleArgumentType.classDeclaration } + + // Look for the right scope even before resolving the class and resolving all its super + // types. + if (scope != null && scope !in scopes) return@mapNotNull null + + reference.declaration.type.singleArgumentType + .classDeclaration + } + .filter { clazz -> + // Check that the annotation really is present. It should always be the case, but it's + // a safetynet in case the generated properties are out of sync. + clazz.annotations.any { + it.annotationType.resolve().classDeclaration.fqName == annotation && (scope == null || it.scope() == scope) + } + } + } + + private fun ModuleDescriptor.hintPackages(annotation: FqName): Sequence { + val packageName = when (annotation) { + contributesToFqName -> HINT_CONTRIBUTES_PACKAGE_PREFIX + contributesBindingFqName -> HINT_BINDING_PACKAGE_PREFIX + contributesMultibindingFqName -> HINT_MULTIBINDING_PACKAGE_PREFIX + contributesSubcomponentFqName -> HINT_SUBCOMPONENTS_PACKAGE_PREFIX + else -> throw AnvilCompilationException(message = "Cannot find hints for $annotation.") + } + + return generateSequence(listOf(getPackage(FqName(packageName)))) { subPackages -> + subPackages + .flatMap { it.subPackages() } + .ifEmpty { null } + } + .flatMap { it.asSequence() } + .mapNotNull { it.fqNameOrNull()?.asString() } + } +} + +private fun PackageViewDescriptor.subPackages(): List = memberScope + .getContributedDescriptors(DescriptorKindFilter.PACKAGES) + .filterIsInstance() + +private sealed class GeneratedProperty( + val declaration: KSPropertyDeclaration, + val baseName: String, +) { + class ReferenceProperty( + descriptor: KSPropertyDeclaration, + baseName: String, + ) : GeneratedProperty(descriptor, baseName) + + class ScopeProperty( + descriptor: KSPropertyDeclaration, + baseName: String, + ) : GeneratedProperty(descriptor, baseName) + + companion object { + fun fromDeclaration(declaration: KSPropertyDeclaration): GeneratedProperty? { + // For each contributed hint there are several properties, e.g. the reference itself + // and the scopes. Group them by their common name without the suffix. + val name = declaration.simpleName.asString() + + return when { + name.endsWith(REFERENCE_SUFFIX) -> + ReferenceProperty(declaration, name.substringBeforeLast(REFERENCE_SUFFIX)) + + name.contains(SCOPE_SUFFIX) -> { + // The old scope hint didn't have a number. Now that there can be multiple scopes + // we append a number for all scopes, but we still need to support the old format. + val indexString = name.substringAfterLast(SCOPE_SUFFIX) + if (indexString.toIntOrNull() != null || indexString.isEmpty()) { + ScopeProperty(declaration, name.substringBeforeLast(SCOPE_SUFFIX)) + } else { + null + } + } + + else -> null + } + } + } +} + +private data class CacheKey( + val name: FqName, + val moduleHash: Int, +) diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/ksp/InterfaceMergerKSP.kt b/compiler/src/main/java/com/squareup/anvil/compiler/ksp/InterfaceMergerKSP.kt new file mode 100644 index 000000000..87baeef75 --- /dev/null +++ b/compiler/src/main/java/com/squareup/anvil/compiler/ksp/InterfaceMergerKSP.kt @@ -0,0 +1,258 @@ +package com.squareup.anvil.compiler.ksp + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getAllSuperTypes +import com.google.devtools.ksp.isAnnotationPresent +import com.google.devtools.ksp.isPublic +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSType +import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.anvil.annotations.ContributesMultibinding +import com.squareup.anvil.annotations.ContributesSubcomponent +import com.squareup.anvil.annotations.ContributesTo +import com.squareup.anvil.annotations.MergeComponent +import com.squareup.anvil.annotations.MergeSubcomponent +import com.squareup.anvil.annotations.compat.MergeInterfaces +import com.squareup.anvil.compiler.PARENT_COMPONENT +import com.squareup.anvil.compiler.codegen.generatedAnvilSubcomponent +import com.squareup.anvil.compiler.codegen.ksp.KspAnvilException +import com.squareup.anvil.compiler.codegen.ksp.isInterface +import com.squareup.anvil.compiler.codegen.ksp.scope +import com.squareup.anvil.compiler.contributesSubcomponentFqName +import com.squareup.anvil.compiler.contributesToFqName +import dagger.Module +import org.jetbrains.kotlin.name.Name + +/** + * Finds all contributed component interfaces and adds them as super types to Dagger components + * annotated with `@MergeComponent` or `@MergeSubcomponent`. + */ +internal class InterfaceMergerKSP( + private val classScanner: ClassScannerKSP, +) { + fun computeSyntheticSupertypes( + mergeAnnotatedClass: KSClassDeclaration, + resolver: Resolver, + scopes: Set, + ): List { + if (mergeAnnotatedClass.shouldIgnore()) return emptyList() + + val mergeAnnotations = mergeAnnotatedClass + .findAllKSAnnotations(MergeComponent::class, MergeSubcomponent::class, MergeInterfaces::class) + .toList() + .ifEmpty { return emptyList() } + + val supertypes = mergeAnnotatedClass.getAllSuperTypes() + .toMutableList() + + val result = mergeInterfaces( + classScanner = classScanner, + resolver = resolver, + mergeAnnotatedClass = mergeAnnotatedClass, + annotations = mergeAnnotations, + supertypes = supertypes, + scopes = scopes, + ) + + supertypes += result.contributesAnnotations + .asSequence() + .map { it.declaringClass() } + .filter { clazz -> + clazz !in result.replacedClasses && clazz !in result.excludedClasses + } + .plus(result.contributedSubcomponentInterfaces()) + // Avoids an error for repeated interfaces. + .distinct() + .map { it.asType(emptyList()) } + .toList() + + return supertypes + } + + data class MergeResult( + val contributesAnnotations: List, + val replacedClasses: Set, + val excludedClasses: List, + val contributedSubcomponentInterfaces: () -> Sequence, + ) + + companion object { + @OptIn(KspExperimental::class) + private fun mergeInterfaces( + classScanner: ClassScannerKSP, + resolver: Resolver, + mergeAnnotatedClass: KSClassDeclaration, + annotations: List, + supertypes: List, + scopes: Set, + ): MergeResult { + if (!mergeAnnotatedClass.isInterface()) { + throw KspAnvilException( + node = mergeAnnotatedClass, + message = "Dagger components must be interfaces.", + ) + } + + val contributesAnnotations = annotations + .flatMap { annotation -> + classScanner + .findContributedClasses( + resolver = resolver, + annotation = contributesToFqName, + scope = annotation.scope(), + ) + } + .filter { clazz -> + clazz.isInterface() && !clazz.isAnnotationPresent(Module::class) + } + .flatMap { clazz -> + clazz.findAllKSAnnotations(ContributesTo::class) + .filter { it.scope() in scopes } + } + .onEach { contributeAnnotation -> + val contributedClass = contributeAnnotation.declaringClass() + if (!contributedClass.isPublic()) { + throw KspAnvilException( + node = contributedClass, + message = "${contributedClass.fqName} is contributed to the Dagger graph, but the " + + "interface is not public. Only public interfaces are supported.", + ) + } + } + // Convert the sequence to a list to avoid iterating it twice. We use the result twice + // for replaced classes and the final result. + .toList() + + val replacedClasses = contributesAnnotations + .flatMap { contributeAnnotation -> + val contributedClass = contributeAnnotation.declaringClass() + contributedClass + .atLeastOneAnnotation( + contributeAnnotation.annotationType.resolve().classDeclaration.qualifiedName!!.asString(), + ) + .flatMap { it.replaces() } + .map { it.classDeclaration } + .onEach { classToReplace -> + // Verify the other class is an interface. It doesn't make sense for a contributed + // interface to replace a class that is not an interface. + if (!classToReplace.isInterface()) { + throw KspAnvilException( + node = contributedClass, + message = "${contributedClass.fqName} wants to replace " + + "${classToReplace.fqName}, but the class being " + + "replaced is not an interface.", + ) + } + + val contributesToOurScope = classToReplace + .findAllKSAnnotations( + ContributesTo::class, + ContributesBinding::class, + ContributesMultibinding::class, + ) + .map { it.scope() } + .any { scope -> scope in scopes } + + if (!contributesToOurScope) { + val scopesString = scopes.joinToString(prefix = "[", postfix = "]") { + it.fqName.asString() + } + throw KspAnvilException( + node = contributedClass, + message = "${contributedClass.fqName} with scopes " + + "$scopesString " + + "wants to replace ${classToReplace.fqName}, but the replaced class isn't " + + "contributed to the same scope.", + ) + } + } + } + .toSet() + + val excludedClasses = annotations + .flatMap { it.exclude() } + .map { it.classDeclaration } + .filter { it.isInterface() } + .onEach { excludedClass -> + // Verify that the replaced classes use the same scope. + val contributesToOurScope = excludedClass + .findAllKSAnnotations( + ContributesTo::class, + ContributesBinding::class, + ContributesMultibinding::class, + ) + .map { it.scope() } + .plus( + excludedClass.findAllKSAnnotations( + ContributesSubcomponent::class, + ).map { it.parentScope() }, + ) + .any { scope -> scope in scopes } + + if (!contributesToOurScope) { + throw KspAnvilException( + message = "${mergeAnnotatedClass.fqName} with scopes " + + "${scopes.joinToString(prefix = "[", postfix = "]") { it.fqName.asString() }} " + + "wants to exclude ${excludedClass.fqName}, but the excluded class isn't " + + "contributed to the same scope.", + node = mergeAnnotatedClass, + ) + } + } + + if (excludedClasses.isNotEmpty()) { + val intersect = supertypes + .map { it.classDeclaration } + .intersect(excludedClasses.toSet()) + + if (intersect.isNotEmpty()) { + throw KspAnvilException( + node = mergeAnnotatedClass, + message = "${mergeAnnotatedClass.fqName} excludes types that it implements or " + + "extends. These types cannot be excluded. Look at all the super" + + " types to find these classes: " + + "${intersect.joinToString { it.fqName.asString() }}.", + ) + } + } + return MergeResult( + contributesAnnotations = contributesAnnotations, + replacedClasses = replacedClasses, + excludedClasses = excludedClasses, + contributedSubcomponentInterfaces = { + findContributedSubcomponentParentInterfaces( + classScanner = classScanner, + clazz = mergeAnnotatedClass, + scopes = scopes, + resolver = resolver, + ) + }, + ) + } + + private fun findContributedSubcomponentParentInterfaces( + classScanner: ClassScannerKSP, + clazz: KSClassDeclaration, + scopes: Collection, + resolver: Resolver, + ): Sequence { + return classScanner + .findContributedClasses( + resolver = resolver, + annotation = contributesSubcomponentFqName, + scope = null, + ) + .filter { + it.atLeastOneAnnotation(ContributesSubcomponent::class).single().parentScope() in scopes + } + .mapNotNull { contributedSubcomponent -> + contributedSubcomponent.classId + .generatedAnvilSubcomponent(clazz.classId) + .createNestedClassId(Name.identifier(PARENT_COMPONENT)) + .classDeclarationOrNull(resolver) + } + } + } +} diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/ksp/KspUtils.kt b/compiler/src/main/java/com/squareup/anvil/compiler/ksp/KspUtils.kt new file mode 100644 index 000000000..4ea85460c --- /dev/null +++ b/compiler/src/main/java/com/squareup/anvil/compiler/ksp/KspUtils.kt @@ -0,0 +1,172 @@ +package com.squareup.anvil.compiler.ksp + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.isAnnotationPresent +import com.google.devtools.ksp.isLocal +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSDeclaration +import com.google.devtools.ksp.symbol.KSName +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.KSTypeReference +import com.google.devtools.ksp.toKSName +import com.squareup.anvil.annotations.ExperimentalAnvilApi +import com.squareup.anvil.compiler.ANVIL_MODULE_SUFFIX +import com.squareup.anvil.compiler.MODULE_PACKAGE_PREFIX +import com.squareup.anvil.compiler.codegen.ksp.KspAnvilException +import com.squareup.anvil.compiler.codegen.ksp.argumentAt +import com.squareup.anvil.compiler.codegen.ksp.getKSAnnotationsByQualifiedName +import com.squareup.anvil.compiler.codegen.ksp.getKSAnnotationsByType +import com.squareup.anvil.compiler.codegen.ksp.scope +import com.squareup.anvil.compiler.codegen.ksp.scopeOrNull +import com.squareup.anvil.compiler.internal.classIdBestGuess +import com.squareup.anvil.compiler.internal.reference.argumentAt +import com.squareup.anvil.compiler.internal.reference.asClassId +import com.squareup.anvil.compiler.internal.reference.generateClassName +import com.squareup.anvil.compiler.internal.safePackageString +import com.squareup.kotlinpoet.ksp.toClassName +import dagger.MapKey +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import javax.inject.Qualifier +import javax.inject.Scope +import kotlin.reflect.KClass + +val KSClassDeclaration.fqName: FqName get() { + return qualifiedName?.let { + FqName(it.asString()) + } ?: throw KspAnvilException( + message = "Couldn't find qualified name for '$this'.", + node = this, + ) +} + +val KSClassDeclaration.classId: ClassId get() { + return fqName.classIdBestGuess() +} + +internal fun ClassId.classDeclarationOrNull( + resolver: Resolver, +): KSClassDeclaration? = resolver.getClassDeclarationByName(toKSName()) + +val KSTypeReference.singleArgumentType: KSType get() { + return resolve().arguments.single().type?.resolve() ?: throw KspAnvilException( + message = "Expected a single type argument, but found none.", + node = this, + ) +} + +val KSType.classDeclaration: KSClassDeclaration get() { + return declaration as? KSClassDeclaration ?: throw KspAnvilException( + message = "Expected declaration to be a class.", + node = declaration, + ) +} + +internal fun KSAnnotation.declaringClass(): KSClassDeclaration { + return parent as? KSClassDeclaration ?: throw KspAnvilException( + message = "Expected declaration to be a class.", + node = this, + ) +} + +internal fun KSAnnotated.atLeastOneAnnotation( + annotationClass: String, + scope: KSType? = null, +): Sequence { + return findAllKSAnnotations(annotationClass) + .filter { it.scopeOrNull() == scope } + .ifEmpty { + throw KspAnvilException( + message = "Class $this is not annotated with $annotationClass" + + "${if (scope == null) "" else " with scope ${scope.classDeclaration.fqName}"}.", + node = this, + ) + } +} + +internal fun KSAnnotated.atLeastOneAnnotation( + annotationClass: KClass, + scope: KSType? = null, +): Sequence { + return findAllKSAnnotations(annotationClass) + .filter { it.scopeOrNull() == scope } + .ifEmpty { + throw KspAnvilException( + message = "Class $this is not annotated with $annotationClass" + + "${if (scope == null) "" else " with scope ${scope.classDeclaration.fqName}"}.", + node = this, + ) + } +} + +// If we're evaluating an anonymous inner class, it cannot merge anything and will cause +// a failure if we try to resolve its [ClassId] +internal fun KSDeclaration.shouldIgnore(): Boolean { + return qualifiedName == null || isLocal() +} + +internal fun createAnvilModuleName(clazz: KSClassDeclaration): String { + return "$MODULE_PACKAGE_PREFIX." + + clazz.packageName.safePackageString() + + clazz.generateClassName( + separator = "", + suffix = ANVIL_MODULE_SUFFIX, + ).relativeClassName.toString() +} + +@ExperimentalAnvilApi +internal fun KSClassDeclaration.generateClassName( + separator: String = "_", + suffix: String = "", +): ClassId { + return toClassName().generateClassName(separator, suffix).asClassId() +} + +internal fun KSName.safePackageString( + dotPrefix: Boolean = false, + dotSuffix: Boolean = true, +): String = toString().safePackageString(isRoot, dotPrefix, dotSuffix) + +private val KSName.isRoot: Boolean get() = asString().isEmpty() + +fun KSAnnotated.findAllKSAnnotations(vararg annotations: KClass): Sequence { + return sequence { + for (annotation in annotations) { + yieldAll(getKSAnnotationsByType(annotation)) + } + } +} +fun KSAnnotated.findAllKSAnnotations(vararg annotations: String): Sequence { + return sequence { + for (annotation in annotations) { + yieldAll(getKSAnnotationsByQualifiedName(annotation)) + } + } +} + +internal fun KSAnnotation.replaces(): List = + argumentAt("replaces")?.value as? List ?: emptyList() + +internal fun KSAnnotation.exclude(): List = + argumentAt("exclude")?.value as? List ?: emptyList() + +internal fun KSAnnotation.parentScope(): KSClassDeclaration { + return argumentAt("parentScope") + ?.value as? KSClassDeclaration + ?: throw KspAnvilException( + message = "Couldn't find parentScope for $this.", + node = this, + ) +} + +@OptIn(KspExperimental::class) +internal fun KSAnnotated.isQualifier(): Boolean = isAnnotationPresent(Qualifier::class) + +@OptIn(KspExperimental::class) +internal fun KSAnnotated.isMapKey(): Boolean = isAnnotationPresent(MapKey::class) + +@OptIn(KspExperimental::class) +internal fun KSAnnotated.isDaggerScope(): Boolean = isAnnotationPresent(Scope::class) diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/ksp/MergeComponentSymbolProcessor.kt b/compiler/src/main/java/com/squareup/anvil/compiler/ksp/MergeComponentSymbolProcessor.kt new file mode 100644 index 000000000..8d5cbc50f --- /dev/null +++ b/compiler/src/main/java/com/squareup/anvil/compiler/ksp/MergeComponentSymbolProcessor.kt @@ -0,0 +1,352 @@ +@file:OptIn(KspExperimental::class) + +package com.squareup.anvil.compiler.ksp + +import com.google.auto.service.AutoService +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.isAnnotationPresent +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFile +import com.google.devtools.ksp.symbol.KSName +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.KSTypeAlias +import com.squareup.anvil.annotations.compat.MergeModules +import com.squareup.anvil.compiler.api.AnvilApplicabilityChecker +import com.squareup.anvil.compiler.api.AnvilContext +import com.squareup.anvil.compiler.codegen.ksp.AnvilSymbolProcessor +import com.squareup.anvil.compiler.codegen.ksp.AnvilSymbolProcessorProvider +import com.squareup.anvil.compiler.codegen.ksp.KspAnvilException +import com.squareup.anvil.compiler.codegen.ksp.argumentAt +import com.squareup.anvil.compiler.codegen.ksp.isInterface +import com.squareup.anvil.compiler.codegen.ksp.scope +import com.squareup.anvil.compiler.internal.capitalize +import com.squareup.anvil.compiler.internal.createAnvilSpec +import com.squareup.anvil.compiler.mergeComponentFqName +import com.squareup.anvil.compiler.mergeModulesFqName +import com.squareup.anvil.compiler.mergeSubcomponentFqName +import com.squareup.kotlinpoet.ANY +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.joinToCode +import com.squareup.kotlinpoet.jvm.jvmStatic +import com.squareup.kotlinpoet.ksp.addOriginatingKSFile +import com.squareup.kotlinpoet.ksp.toAnnotationSpec +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.writeTo +import dagger.Component +import dagger.Module +import dagger.Subcomponent + +internal class MergeComponentSymbolProcessor( + override val env: SymbolProcessorEnvironment, +) : AnvilSymbolProcessor() { + @AutoService(SymbolProcessorProvider::class) + class Provider : AnvilSymbolProcessorProvider( + applicabilityChecker = ApplicabilityChecker, + delegate = ::MergeComponentSymbolProcessor, + ) + + object ApplicabilityChecker : AnvilApplicabilityChecker { + override fun isApplicable(context: AnvilContext) = !context.disableComponentMerging + } + + private val classScanner = ClassScannerKSP() + + override fun processChecked(resolver: Resolver): List { + // Look for merge annotations + // Process merged modules + // Process merged interfaces + // Generated merged component with contributed interfaces and merged modules + // generate DaggerComponent shim + + val deferred = mutableListOf() + + sequence { + yieldAll(resolver.getSymbolsWithAnnotation(mergeComponentFqName.asString())) + yieldAll(resolver.getSymbolsWithAnnotation(mergeSubcomponentFqName.asString())) + yieldAll(resolver.getSymbolsWithAnnotation(mergeModulesFqName.asString())) + } + .filterIsInstance() + .filterNot { it.shouldIgnore() } + // Elements could have multiple annotations, we'll only process them once + .distinct() + .forEach { generateComponents(resolver, it) } + + return deferred + } + + @OptIn(KspExperimental::class) + private fun generateComponents( + resolver: Resolver, + clazz: KSClassDeclaration, + ) { + val isMergeModules = clazz.isAnnotationPresent(MergeModules::class) + val annotations = clazz.annotations.toList() + val originalAnnotation = annotations.first() + val daggerAnnotationFqName = originalAnnotation.daggerAnnotationFqName + val scopes = annotations.mapTo(LinkedHashSet()) { it.scope() } + + val contributedModules = + ModuleMergerKSP.mergeModules( + classScanner, + resolver, + clazz, + annotations, + scopes, + ) + + val daggerAnnotation = createAnnotation( + originalAnnotation, + daggerAnnotationFqName, + contributedModules, + ) + + val typesToAdd = mutableListOf() + val originClassName = clazz.toClassName() + val generatedClassName = "Anvil${clazz.simpleName.asString().capitalize()}" + val originatingFile = clazz.containingFile ?: throw KspAnvilException( + "No containing file found for ${clazz.qualifiedName?.asString()}", + node = clazz, + ) + if (isMergeModules) { + // Generate a module with the annotation and be done + typesToAdd += TypeSpec.interfaceBuilder(generatedClassName) + .addAnnotation(daggerAnnotation) + .addOriginatingKSFile(originatingFile) + .build() + } else { + val anvilClassName = ClassName(originClassName.packageName, generatedClassName) + val generatedDaggerComponentName = anvilClassName.generatedDaggerComponentName() + + val creator = createFactoryOrBuilderFunSpec(clazz, generatedDaggerComponentName) + + val mergedInterfaces = InterfaceMergerKSP(classScanner) + .computeSyntheticSupertypes( + clazz, + resolver, + scopes, + ) + + // Build the component + typesToAdd += TypeSpec.interfaceBuilder(generatedClassName) + .addAnnotation(daggerAnnotation) + .addSuperinterface(originClassName) + .addSuperinterfaces(mergedInterfaces.map { it.toClassName() }.filterNot { it == ANY }) + .apply { + creator?.let { + // Add the creator function + addType( + TypeSpec.companionObjectBuilder() + .addFunction(it) + .build(), + ) + } + } + // TODO generate extension of creator interface? + .addOriginatingKSFile(originatingFile) + .build() + + if (creator != null) { + typesToAdd += generateDaggerComponentShim( + originClassName, + creator, + originatingFile, + ) + } + } + + val fileSpec = FileSpec.createAnvilSpec( + originClassName.packageName, + generatedClassName, + ) { + for (type in typesToAdd) { + addType(type) + } + } + + fileSpec.writeTo(env.codeGenerator, aggregating = true) + } + + @OptIn(KspExperimental::class) + private fun createFactoryOrBuilderFunSpec( + origin: KSClassDeclaration, + generatedAnvilClassName: ClassName, + ): FunSpec? { + val (className, functionName) = origin.declarations + .filterIsInstance() + .filter { it.isInterface() } + .mapNotNull { declaration -> + if (declaration.isAnnotationPresent(Component.Factory::class)) { + val className = declaration.toClassName() + className to "factory" + } else if (declaration.isAnnotationPresent(Component.Builder::class)) { + val className = declaration.toClassName() + className to "builder" + } else { + null + } + } + .firstOrNull() + ?: return null + + return FunSpec.builder(functionName) + .jvmStatic() + .returns(className) + .addStatement("return·%T.$functionName()", generatedAnvilClassName) + .build() + } + + /** + * Adapted from how Dagger generates component class file names: https://github.com/google/dagger/blob/f6ddcc3cdd7ca2983497612b987153929fe7f32c/java/dagger/internal/codegen/writing/ComponentNames.java#L52-L57 + */ + private fun ClassName.generatedDaggerComponentName(): ClassName { + return ClassName(packageName, "Dagger${simpleNames.joinToString("_")}") + } + + private fun createAnnotation( + originalAnnotation: KSAnnotation, + daggerAnnotation: ClassName, + contributedModules: Set, + ): AnnotationSpec { + val modulesParamName = if (daggerAnnotation == Module::class.asClassName()) { + "includes" + } else { + "modules" + } + + val builder = AnnotationSpec.builder(daggerAnnotation) + .addMember( + "$modulesParamName = [%L]", + contributedModules.map { CodeBlock.of("%T::class", it.toClassName()) } + .joinToCode(), + ) + + fun copyArrayValue(name: String) { + originalAnnotation.toAnnotationSpec() + val member = CodeBlock.builder() + member.add("%N = ", name) + val value = originalAnnotation.argumentAt(name) ?: return + addValueToBlock(value, member) + builder.addMember(member.build()) + } + + when (daggerAnnotation) { + Component::class.asClassName() -> { + copyArrayValue("dependencies") + } + + MergeModules::class.asClassName() -> { + copyArrayValue("subcomponents") + } + } + + return builder.build() + } + + // TODO generate a kdoc? + private fun generateDaggerComponentShim( + originClassName: ClassName, + creator: FunSpec, + originatingFile: KSFile, + ): TypeSpec { + return TypeSpec.objectBuilder(originClassName.generatedDaggerComponentName()) + .addFunction(creator) + .addOriginatingKSFile(originatingFile) + .build() + } +} + +private fun addValueToBlock( + value: Any, + member: CodeBlock.Builder, +) { + when (value) { + is List<*> -> { + // Array type + val arrayType = when (value.firstOrNull()) { + is Boolean -> "booleanArrayOf" + is Byte -> "byteArrayOf" + is Char -> "charArrayOf" + is Short -> "shortArrayOf" + is Int -> "intArrayOf" + is Long -> "longArrayOf" + is Float -> "floatArrayOf" + is Double -> "doubleArrayOf" + else -> "arrayOf" + } + member.add("$arrayType(⇥⇥") + value.forEachIndexed { index, innerValue -> + if (index > 0) member.add(", ") + addValueToBlock(innerValue!!, member) + } + member.add("⇤⇤)") + } + + is KSType -> { + val unwrapped = value.unwrapTypeAlias() + val isEnum = (unwrapped.declaration as KSClassDeclaration).classKind == ClassKind.ENUM_ENTRY + if (isEnum) { + val parent = unwrapped.declaration.parentDeclaration as KSClassDeclaration + val entry = unwrapped.declaration.simpleName.getShortName() + member.add("%T.%L", parent.toClassName(), entry) + } else { + member.add("%T::class", unwrapped.toClassName()) + } + } + + is KSName -> + member.add( + "%T.%L", + ClassName.bestGuess(value.getQualifier()), + value.getShortName(), + ) + + is KSAnnotation -> member.add("%L", value.toAnnotationSpec()) + else -> member.add(memberForValue(value)) + } +} + +private fun KSType.unwrapTypeAlias(): KSType { + return if (this.declaration is KSTypeAlias) { + (this.declaration as KSTypeAlias).type.resolve() + } else { + this + } +} + +/** + * Creates a [CodeBlock] with parameter `format` depending on the given `value` object. + * Handles a number of special cases, such as appending "f" to `Float` values, and uses + * `%L` for other types. + */ +private fun memberForValue(value: Any) = when (value) { + is Class<*> -> CodeBlock.of("%T::class", value) + is Enum<*> -> CodeBlock.of("%T.%L", value.javaClass, value.name) + is String -> CodeBlock.of("%S", value) + is Float -> CodeBlock.of("%Lf", value) + is Double -> CodeBlock.of("%L", value) + is Char -> CodeBlock.of("'%L'", value) + is Byte -> CodeBlock.of("$value.toByte()") + is Short -> CodeBlock.of("$value.toShort()") + // Int or Boolean + else -> CodeBlock.of("%L", value) +} + +private val KSAnnotation.daggerAnnotationFqName: ClassName + get() = when (annotationType.resolve().classDeclaration.fqName) { + mergeComponentFqName -> Component::class.asClassName() + mergeSubcomponentFqName -> Subcomponent::class.asClassName() + mergeModulesFqName -> Module::class.asClassName() + else -> throw NotImplementedError("Don't know how to handle $this.") + } diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/ksp/ModuleMergerKSP.kt b/compiler/src/main/java/com/squareup/anvil/compiler/ksp/ModuleMergerKSP.kt new file mode 100644 index 000000000..4fc8e36f1 --- /dev/null +++ b/compiler/src/main/java/com/squareup/anvil/compiler/ksp/ModuleMergerKSP.kt @@ -0,0 +1,292 @@ +package com.squareup.anvil.compiler.ksp + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.isAnnotationPresent +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSType +import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.anvil.annotations.ContributesMultibinding +import com.squareup.anvil.annotations.ContributesSubcomponent +import com.squareup.anvil.annotations.ContributesTo +import com.squareup.anvil.annotations.compat.MergeModules +import com.squareup.anvil.compiler.ANVIL_MODULE_SUFFIX +import com.squareup.anvil.compiler.MODULE_PACKAGE_PREFIX +import com.squareup.anvil.compiler.SUBCOMPONENT_MODULE +import com.squareup.anvil.compiler.codegen.generatedAnvilSubcomponent +import com.squareup.anvil.compiler.codegen.ksp.KspAnvilException +import com.squareup.anvil.compiler.codegen.ksp.argumentAt +import com.squareup.anvil.compiler.codegen.ksp.checkClassIsPublic +import com.squareup.anvil.compiler.codegen.ksp.getKSAnnotationsByQualifiedName +import com.squareup.anvil.compiler.codegen.ksp.getKSAnnotationsByType +import com.squareup.anvil.compiler.codegen.ksp.isInterface +import com.squareup.anvil.compiler.codegen.ksp.scope +import com.squareup.anvil.compiler.contributesBindingFqName +import com.squareup.anvil.compiler.contributesMultibindingFqName +import com.squareup.anvil.compiler.contributesSubcomponentFqName +import com.squareup.anvil.compiler.contributesToFqName +import com.squareup.anvil.compiler.internal.reference.ClassReference +import com.squareup.anvil.compiler.isAnvilModule +import com.squareup.kotlinpoet.ksp.toClassName +import dagger.Module +import org.jetbrains.kotlin.codegen.ImplementationBodyCodegen +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.org.objectweb.asm.Type + +/** + * Finds all contributed Dagger modules and adds them to Dagger components annotated with + * `@MergeComponent` or `@MergeSubcomponent`. This class is responsible for generating the + * `@Component` and `@Subcomponent` annotation required for Dagger. + */ +@OptIn(KspExperimental::class) +internal object ModuleMergerKSP { + + fun mergeModules( + classScanner: ClassScannerKSP, + resolver: Resolver, + clazz: KSClassDeclaration, + annotations: List, + scopes: Set, + ): Set { + @Suppress("UNCHECKED_CAST") + val predefinedModules = annotations.flatMap { + it.argumentAt("modules")?.value as? List ?: emptyList() + }.map { it.classDeclaration } + val anvilModuleName = createAnvilModuleName(clazz) + + val contributesAnnotations = scopes + .flatMap { scope -> + classScanner + .findContributedClasses( + resolver = resolver, + annotation = contributesToFqName, + scope = scope, + ) + } + .filter { + // We generate a Dagger module for each merged component. We use Anvil itself to + // contribute this generated module. It's possible that there are multiple components + // merging the same scope or the same scope is merged in different Gradle modules which + // depend on each other. This would cause duplicate bindings, because the generated + // modules contain the same bindings and are contributed to the same scope. To avoid this + // issue we filter all generated Anvil modules except for the one that was generated for + // this specific class. + !it.fqName.isAnvilModule() || it.fqName == anvilModuleName + } + .flatMap { contributedClass -> + contributedClass + .getKSAnnotationsByType(ContributesTo::class) + .filter { it.scope() in scopes } + } + .filter { contributesAnnotation -> + val contributedClass = contributesAnnotation.declaringClass() + val moduleAnnotation = contributedClass.getKSAnnotationsByType(Module::class) + .singleOrNull() + val mergeModulesAnnotation = + contributedClass.getKSAnnotationsByType(MergeModules::class) + .singleOrNull() + + if (!contributedClass.isInterface() && + moduleAnnotation == null && + mergeModulesAnnotation == null + ) { + throw KspAnvilException( + "${contributedClass.fqName} is annotated with " + + "@${ContributesTo::class.simpleName}, but this class is neither an interface " + + "nor a Dagger module. Did you forget to add @${Module::class.simpleName}?", + contributedClass, + ) + } + + contributedClass.checkClassIsPublic { + "${contributedClass.fqName} is contributed to the Dagger graph, but the " + + "module is not public. Only public modules are supported." + } + + moduleAnnotation != null || mergeModulesAnnotation != null + } + // Convert the sequence to a list to avoid iterating it twice. We use the result twice + // for replaced classes and the final result. + .toList() + + val excludedModules = annotations.flatMap { it.exclude() } + .map { it.classDeclaration } + .onEach { excludedClass -> + // Verify that the excluded classes use the same scope. + val contributesToOurScope = excludedClass + .findAllKSAnnotations( + ContributesTo::class, + ContributesBinding::class, + ContributesMultibinding::class, + ) + .map { it.scope() } + .plus( + excludedClass.findAllKSAnnotations(ContributesSubcomponent::class) + .map { it.parentScope() }, + ) + .any { scope -> scope in scopes } + + if (!contributesToOurScope) { + throw KspAnvilException( + message = "${clazz.fqName} with scopes " + + "${scopes.joinToString(prefix = "[", postfix = "]") { it.fqName.asString() }} " + + "wants to exclude ${excludedClass.fqName}, but the excluded class isn't " + + "contributed to the same scope.", + node = clazz, + ) + } + } + + val replacedModules = contributesAnnotations + // Ignore replaced modules or bindings specified by excluded modules. + .filter { contributesAnnotation -> + contributesAnnotation.declaringClass() !in excludedModules + } + .flatMap { contributesAnnotation -> + val contributedClass = contributesAnnotation.declaringClass() + contributesAnnotation.replaces() + .map { it.classDeclaration } + .onEach { classToReplace -> + // Verify has @Module annotation. It doesn't make sense for a Dagger module to + // replace a non-Dagger module. + if (!classToReplace.isAnnotationPresent(Module::class) && + !classToReplace.isAnnotationPresent(ContributesBinding::class) && + !classToReplace.isAnnotationPresent(ContributesMultibinding::class) + ) { + throw KspAnvilException( + message = "${contributedClass.fqName} wants to replace " + + "${classToReplace.fqName}, but the class being replaced is not a Dagger module.", + node = contributedClass, + ) + } + + checkSameScope(contributedClass, classToReplace, scopes) + } + } + + fun replacedModulesByContributedBinding( + annotationFqName: FqName, + ): Sequence { + return scopes.asSequence() + .flatMap { scope -> + classScanner + .findContributedClasses( + resolver = resolver, + annotation = annotationFqName, + scope = scope, + ) + } + .flatMap { contributedClass -> + contributedClass.getKSAnnotationsByQualifiedName(annotationFqName.asString()) + .filter { it.scope() in scopes } + .flatMap { it.replaces() } + .onEach { classToReplace -> + checkSameScope(contributedClass, classToReplace.classDeclaration, scopes) + } + } + .map { it.classDeclaration } + } + + val replacedModulesByContributedBindings = replacedModulesByContributedBinding( + annotationFqName = contributesBindingFqName, + ) + + val replacedModulesByContributedMultibindings = replacedModulesByContributedBinding( + annotationFqName = contributesMultibindingFqName, + ) + + val intersect = predefinedModules.intersect(excludedModules.toSet()) + if (intersect.isNotEmpty()) { + throw KspAnvilException( + "${clazz.toClassName()} includes and excludes modules " + + "at the same time: ${intersect.joinToString { it.classId.relativeClassName.asString() }}", + clazz, + ) + } + + val contributedSubcomponentModules = + findContributedSubcomponentModules(classScanner, clazz, scopes, resolver) + + val contributedModuleTypes = contributesAnnotations + .asSequence() + .map { it.declaringClass() } + .minus(replacedModules.toSet()) + .minus(replacedModulesByContributedBindings.toSet()) + .minus(replacedModulesByContributedMultibindings.toSet()) + .minus(excludedModules.toSet()) + .plus(predefinedModules) + .plus(contributedSubcomponentModules) + .distinct() + .toSet() + + return contributedModuleTypes + } + + @Suppress("UNCHECKED_CAST") + private fun Collection.types(codegen: ImplementationBodyCodegen): List { + return (this as Collection) + .map { codegen.typeMapper.mapType(it.clazz) } + } + + private fun createAnvilModuleName(clazz: KSClassDeclaration): FqName { + val name = "$MODULE_PACKAGE_PREFIX." + + clazz.packageName.safePackageString() + + clazz.generateClassName( + separator = "", + suffix = ANVIL_MODULE_SUFFIX, + ).relativeClassName.toString() + return FqName(name) + } + + private fun checkSameScope( + contributedClass: KSClassDeclaration, + classToReplace: KSClassDeclaration, + scopes: Set, + ) { + val contributesToOurScope = classToReplace + .findAllKSAnnotations( + ContributesTo::class, + ContributesBinding::class, + ContributesMultibinding::class, + ) + .map { it.scope() } + .any { scope -> scope in scopes } + + if (!contributesToOurScope) { + throw KspAnvilException( + node = contributedClass, + message = "${contributedClass.fqName} with scopes " + + "${scopes.joinToString(prefix = "[", postfix = "]") { it.fqName.asString() }} " + + "wants to replace ${classToReplace.fqName}, but the replaced class isn't " + + "contributed to the same scope.", + ) + } + } + + private fun findContributedSubcomponentModules( + classScanner: ClassScannerKSP, + clazz: KSClassDeclaration, + scopes: Set, + resolver: Resolver, + ): Sequence { + return classScanner + .findContributedClasses( + resolver = resolver, + annotation = contributesSubcomponentFqName, + scope = null, + ) + .filter { contributedClass -> + contributedClass + .atLeastOneAnnotation(ContributesSubcomponent::class) + .any { it.parentScope() in scopes } + } + .mapNotNull { contributedSubcomponent -> + contributedSubcomponent.classId + .generatedAnvilSubcomponent(clazz.classId) + .createNestedClassId(Name.identifier(SUBCOMPONENT_MODULE)) + .classDeclarationOrNull(resolver) + } + } +} 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 891ce8524..00a3db0a9 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/TestUtils.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/TestUtils.kt @@ -9,12 +9,14 @@ import com.squareup.anvil.compiler.internal.capitalize import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode.Embedded import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode.Ksp +import com.squareup.anvil.compiler.internal.testing.DaggerAnnotationProcessingMode import com.squareup.anvil.compiler.internal.testing.compileAnvil import com.squareup.anvil.compiler.internal.testing.generatedClassesString import com.squareup.anvil.compiler.internal.testing.packageName import com.squareup.anvil.compiler.internal.testing.use import com.tschuchort.compiletesting.CompilationResult import com.tschuchort.compiletesting.JvmCompilationResult +import com.tschuchort.compiletesting.KotlinCompilation import com.tschuchort.compiletesting.KotlinCompilation.ExitCode import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.COMPILATION_ERROR import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.INTERNAL_ERROR @@ -26,16 +28,16 @@ import kotlin.reflect.KClass internal fun compile( @Language("kotlin") vararg sources: String, previousCompilationResult: JvmCompilationResult? = null, - enableDaggerAnnotationProcessor: Boolean = false, + daggerAnnotationProcessingMode: DaggerAnnotationProcessingMode = DaggerAnnotationProcessingMode.NONE, codeGenerators: List = emptyList(), allWarningsAsErrors: Boolean = WARNINGS_AS_ERRORS, - mode: AnvilCompilationMode = AnvilCompilationMode.Embedded(codeGenerators), + mode: AnvilCompilationMode = Embedded(codeGenerators), block: JvmCompilationResult.() -> Unit = { }, ): JvmCompilationResult = compileAnvil( sources = sources, allWarningsAsErrors = allWarningsAsErrors, previousCompilationResult = previousCompilationResult, - enableDaggerAnnotationProcessor = enableDaggerAnnotationProcessor, + daggerAnnotationProcessingMode = daggerAnnotationProcessingMode, mode = mode, block = block, ) @@ -183,11 +185,44 @@ internal fun isFullTestRun(): Boolean = FULL_TEST_RUN internal fun checkFullTestRun() = assumeTrue(isFullTestRun()) internal fun includeKspTests(): Boolean = INCLUDE_KSP_TESTS +internal fun daggerProcessingModesForTests(includeNone: Boolean = true) = buildList { + if (isFullTestRun()) { + add(DaggerAnnotationProcessingMode.KSP) + add(DaggerAnnotationProcessingMode.KAPT) + } + if (includeNone) { + add(DaggerAnnotationProcessingMode.NONE) + } +} + +/** + * Dagger KSP does not support wildcard types on the DI graph. Dagger 2.47 added a flag to ignore these, but it doesn't + * appear to actually enforce things. KSP code gen does behave as expected though, so we disable tests that require + * wildcards support when using KSP. + * + * See https://dagger.dev/dev-guide/compiler-options#ignore-provision-key-wildcards. + * + * This function calls [assumeTrue] under the hood and should be used to ignore tests that require wildcards. + */ +internal fun testRequiresWildcards(mode: DaggerAnnotationProcessingMode?) = + assumeTrue(mode != DaggerAnnotationProcessingMode.KSP) + +/** + * Dagger KSP is a work in progress and there may occasionally be bugs that are not working yet upstream. This function + * is here to track such cases. The second parameter is solely for documentation purposes. + * + * This function calls [assumeTrue] under the hood and should be used to ignore tests that are not ready for KSP yet. + */ +internal fun testIsNotYetCompatibleWithKsp( + mode: DaggerAnnotationProcessingMode?, + @Suppress("UNUSED_PARAMETER") reason: String, +) = assumeTrue(mode != DaggerAnnotationProcessingMode.KSP) + internal fun JvmCompilationResult.walkGeneratedFiles(mode: AnvilCompilationMode): Sequence { val dirToSearch = when (mode) { - is AnvilCompilationMode.Embedded -> + is Embedded -> outputDirectory.parentFile.resolve("build${File.separator}anvil") - is AnvilCompilationMode.Ksp -> outputDirectory.parentFile.resolve("ksp${File.separator}sources") + is Ksp -> outputDirectory.parentFile.resolve("ksp${File.separator}sources") } return dirToSearch.walkTopDown() .filter { it.isFile && it.extension == "kt" } @@ -199,22 +234,24 @@ internal fun JvmCompilationResult.walkGeneratedFiles(mode: AnvilCompilationMode) internal fun useDaggerAndKspParams( embeddedCreator: () -> Embedded? = { Embedded() }, kspCreator: () -> Ksp? = { Ksp() }, + includeNullDaggerProcessingMode: Boolean = true, ): Collection { return cartesianProduct( - listOf( - isFullTestRun(), - false, - ), + daggerProcessingModesForTests(includeNullDaggerProcessingMode), listOfNotNull( embeddedCreator(), kspCreator(), ), - ).mapNotNull { (useDagger, mode) -> - if (useDagger == true && mode is Ksp) { - // TODO Dagger is not supported with KSP in Anvil's tests yet + ).mapNotNull { (daggerAnnotationProcessingMode, mode) -> + if (daggerAnnotationProcessingMode == DaggerAnnotationProcessingMode.KSP && mode is Embedded) { + // Cannot use embedded anvil with dagger KSP + null + } else if (daggerAnnotationProcessingMode == DaggerAnnotationProcessingMode.KAPT && mode is Ksp) { + // Cannot use KSP anvil with dagger kapt + // TODO revisit later, this may be possible actually null } else { - arrayOf(useDagger, mode) + arrayOf(daggerAnnotationProcessingMode, mode) } }.distinct() } @@ -227,3 +264,12 @@ internal fun CompilationResult.compilationErrorLine(): String { .lineSequence() .first { it.startsWith("e:") && KSP_ERROR_HEADER !in it } } + +/** Provides reflective access to the original [KotlinCompilation] that produced this result. */ +internal fun JvmCompilationResult.compilation(): KotlinCompilation { + return JvmCompilationResult::class.java.getDeclaredField("compilation") + .apply { + isAccessible = true + } + .get(this) as KotlinCompilation +} diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesSubcomponentHandlerGeneratorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesSubcomponentHandlerGeneratorTest.kt index cfd79751f..a5281667d 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesSubcomponentHandlerGeneratorTest.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesSubcomponentHandlerGeneratorTest.kt @@ -12,6 +12,7 @@ import com.squareup.anvil.compiler.compile import com.squareup.anvil.compiler.componentInterface import com.squareup.anvil.compiler.contributingInterface import com.squareup.anvil.compiler.daggerModule1 +import com.squareup.anvil.compiler.internal.testing.DaggerAnnotationProcessingMode import com.squareup.anvil.compiler.internal.testing.extends import com.squareup.anvil.compiler.internal.testing.packageName import com.squareup.anvil.compiler.internal.testing.simpleCodeGenerator @@ -648,7 +649,7 @@ class ContributesSubcomponentHandlerGeneratorTest { @MergeComponent(Unit::class) interface ComponentInterface """, - enableDaggerAnnotationProcessor = true, + daggerAnnotationProcessingMode = DaggerAnnotationProcessingMode.KAPT, ) { val daggerComponent = componentInterface.daggerComponent.declaredMethods .single { it.name == "create" } @@ -706,7 +707,7 @@ class ContributesSubcomponentHandlerGeneratorTest { interface ComponentInterface2 """, // Keep Dagger enabled, because it complained initially. - enableDaggerAnnotationProcessor = true, + daggerAnnotationProcessingMode = DaggerAnnotationProcessingMode.KAPT, ) { assertThat(componentInterface1 extends subcomponentInterface1.anyParentComponentInterface) assertThat( @@ -1038,7 +1039,7 @@ class ContributesSubcomponentHandlerGeneratorTest { @MergeComponent(Unit::class) interface ComponentInterface """, - enableDaggerAnnotationProcessor = true, + daggerAnnotationProcessingMode = DaggerAnnotationProcessingMode.KAPT, ) { val daggerComponent = componentInterface.daggerComponent.declaredMethods .single { it.name == "create" } @@ -1098,7 +1099,7 @@ class ContributesSubcomponentHandlerGeneratorTest { class TestClass @Inject constructor(val factory: SubcomponentInterface.ComponentFactory) """, - enableDaggerAnnotationProcessor = true, + daggerAnnotationProcessingMode = DaggerAnnotationProcessingMode.KAPT, ) { val daggerComponent = componentInterface.daggerComponent.declaredMethods .single { it.name == "create" } @@ -1160,7 +1161,7 @@ class ContributesSubcomponentHandlerGeneratorTest { class TestClass @Inject constructor(val factory: SubcomponentInterface.ComponentFactory) """, - enableDaggerAnnotationProcessor = true, + daggerAnnotationProcessingMode = DaggerAnnotationProcessingMode.KAPT, ) { val daggerComponent = componentInterface2.daggerComponent.declaredMethods .single { it.name == "create" } @@ -1225,7 +1226,7 @@ class ContributesSubcomponentHandlerGeneratorTest { @MergeComponent(Unit::class) interface ComponentInterface1 """.trimIndent(), - enableDaggerAnnotationProcessor = true, + daggerAnnotationProcessingMode = DaggerAnnotationProcessingMode.KAPT, ) { assertThat(exitCode).isEqualTo(OK) @@ -1244,7 +1245,7 @@ class ContributesSubcomponentHandlerGeneratorTest { interface ComponentInterface2 """.trimIndent(), previousCompilationResult = firstCompilationResult, - enableDaggerAnnotationProcessor = true, + daggerAnnotationProcessingMode = DaggerAnnotationProcessingMode.KAPT, ) { assertThat(exitCode).isEqualTo(OK) @@ -1358,7 +1359,7 @@ class ContributesSubcomponentHandlerGeneratorTest { class TestClass @Inject constructor(val factory: SubcomponentInterface.ComponentFactory) """, - enableDaggerAnnotationProcessor = true, + daggerAnnotationProcessingMode = DaggerAnnotationProcessingMode.KAPT, ) { val daggerComponent1 = componentInterface1.daggerComponent.declaredMethods .single { it.name == "create" } @@ -1489,7 +1490,7 @@ class ContributesSubcomponentHandlerGeneratorTest { @MergeComponent(Unit::class) interface ComponentInterface1 """, - enableDaggerAnnotationProcessor = true, + daggerAnnotationProcessingMode = DaggerAnnotationProcessingMode.KAPT, ) { val daggerComponent = componentInterface1.daggerComponent.declaredMethods .single { it.name == "create" } @@ -1535,7 +1536,7 @@ class ContributesSubcomponentHandlerGeneratorTest { @MergeComponent(Unit::class) interface ComponentInterface2 """, - enableDaggerAnnotationProcessor = true, + daggerAnnotationProcessingMode = DaggerAnnotationProcessingMode.KAPT, previousCompilationResult = firstResult, ) { val daggerComponent = componentInterface2.daggerComponent.declaredMethods @@ -1625,7 +1626,7 @@ class ContributesSubcomponentHandlerGeneratorTest { @ContributesSubcomponent(scope = Any::class, parentScope = Unit::class) interface SubcomponentInterfacewithVeryVeryVeryVeryVeryVeryVeryLongName """, - enableDaggerAnnotationProcessor = true, + daggerAnnotationProcessingMode = DaggerAnnotationProcessingMode.KAPT, ) { assertThat(exitCode).isEqualTo(OK) } diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/AssistedFactoryGeneratorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/AssistedFactoryGeneratorTest.kt index 2e51eb719..65fb0076e 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/AssistedFactoryGeneratorTest.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/AssistedFactoryGeneratorTest.kt @@ -9,6 +9,7 @@ import com.squareup.anvil.compiler.daggerModule1 import com.squareup.anvil.compiler.internal.testing.AnvilCompilation import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode.Embedded +import com.squareup.anvil.compiler.internal.testing.DaggerAnnotationProcessingMode import com.squareup.anvil.compiler.internal.testing.createInstance import com.squareup.anvil.compiler.internal.testing.factoryClass import com.squareup.anvil.compiler.internal.testing.getPropertyValue @@ -30,12 +31,12 @@ import javax.inject.Provider @RunWith(Parameterized::class) class AssistedFactoryGeneratorTest( - private val useDagger: Boolean, + private val daggerProcessingMode: DaggerAnnotationProcessingMode, private val mode: AnvilCompilationMode, ) { companion object { - @Parameters(name = "Use Dagger: {0}, mode: {1}") + @Parameters(name = "Dagger Processing Mode: {0}, mode: {1}") @JvmStatic fun params() = useDaggerAndKspParams() } @@ -1318,8 +1319,8 @@ public final class AssistedServiceFactory_Impl implements AssistedServiceFactory } @Override - public AssistedService create(long p0_1663806, String other) { - return delegateFactory.get(other, p0_1663806); + public AssistedService create(long longValue, String other) { + return delegateFactory.get(other, longValue); } public static Provider create(AssistedService_Factory delegateFactory) { @@ -1338,12 +1339,12 @@ public final class AssistedServiceFactory_Impl implements AssistedServiceFactory data class AssistedService @AssistedInject constructor( val int: Int, @Assisted val string: String, - @Assisted val long: Long + @Assisted val longValue: Long ) @AssistedFactory interface AssistedServiceFactory { - fun create(long: Long, other: String): AssistedService + fun create(longValue: Long, other: String): AssistedService } """, ) { @@ -2064,8 +2065,8 @@ public final class AssistedServiceFactory_Impl implements AssistedServiceFactory kotlinCompilation.allWarningsAsErrors = WARNINGS_AS_ERRORS } .configureAnvil( - enableDaggerAnnotationProcessor = useDagger, - generateDaggerFactories = !useDagger, + daggerAnnotationProcessingMode = daggerProcessingMode, + generateDaggerFactories = daggerProcessingMode == DaggerAnnotationProcessingMode.NONE, mode = mode, ) } diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/AssistedInjectGeneratorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/AssistedInjectGeneratorTest.kt index 00b15b07b..7c466a2f2 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/AssistedInjectGeneratorTest.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/AssistedInjectGeneratorTest.kt @@ -5,11 +5,13 @@ import com.squareup.anvil.compiler.WARNINGS_AS_ERRORS import com.squareup.anvil.compiler.assistedService import com.squareup.anvil.compiler.compilationErrorLine import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode +import com.squareup.anvil.compiler.internal.testing.DaggerAnnotationProcessingMode import com.squareup.anvil.compiler.internal.testing.compileAnvil import com.squareup.anvil.compiler.internal.testing.factoryClass import com.squareup.anvil.compiler.internal.testing.invokeGet import com.squareup.anvil.compiler.internal.testing.isStatic import com.squareup.anvil.compiler.isError +import com.squareup.anvil.compiler.testIsNotYetCompatibleWithKsp import com.squareup.anvil.compiler.useDaggerAndKspParams import com.tschuchort.compiletesting.JvmCompilationResult import org.intellij.lang.annotations.Language @@ -21,12 +23,12 @@ import javax.inject.Provider @RunWith(Parameterized::class) class AssistedInjectGeneratorTest( - private val useDagger: Boolean, + private val daggerProcessingMode: DaggerAnnotationProcessingMode, private val mode: AnvilCompilationMode, ) { companion object { - @Parameters(name = "Use Dagger: {0}, mode: {1}") + @Parameters(name = "Dagger Processing Mode: {0}, mode: {1}") @JvmStatic fun params() = useDaggerAndKspParams() } @@ -616,6 +618,10 @@ public final class AssistedService_Factory { } @Test fun `two assisted inject constructors aren't supported`() { + testIsNotYetCompatibleWithKsp( + daggerProcessingMode, + "https://github.com/google/dagger/issues/3992", + ) compile( """ package com.squareup.test @@ -644,6 +650,10 @@ public final class AssistedService_Factory { } @Test fun `one inject and one assisted inject constructor aren't supported`() { + testIsNotYetCompatibleWithKsp( + daggerProcessingMode, + "https://github.com/google/dagger/issues/3991", + ) compile( """ package com.squareup.test @@ -677,8 +687,8 @@ public final class AssistedService_Factory { block: JvmCompilationResult.() -> Unit = { }, ): JvmCompilationResult = compileAnvil( sources = sources, - enableDaggerAnnotationProcessor = useDagger, - generateDaggerFactories = !useDagger, + daggerAnnotationProcessingMode = daggerProcessingMode, + generateDaggerFactories = daggerProcessingMode == DaggerAnnotationProcessingMode.NONE, allWarningsAsErrors = WARNINGS_AS_ERRORS, mode = mode, block = block, diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/BindsMethodValidatorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/BindsMethodValidatorTest.kt index 90f2438eb..0494399cd 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/BindsMethodValidatorTest.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/BindsMethodValidatorTest.kt @@ -2,12 +2,15 @@ package com.squareup.anvil.compiler.dagger import com.google.common.truth.Truth.assertThat import com.squareup.anvil.compiler.WARNINGS_AS_ERRORS +import com.squareup.anvil.compiler.daggerProcessingModesForTests +import com.squareup.anvil.compiler.internal.testing.DaggerAnnotationProcessingMode import com.squareup.anvil.compiler.internal.testing.compileAnvil import com.squareup.anvil.compiler.isError -import com.squareup.anvil.compiler.isFullTestRun +import com.squareup.anvil.compiler.testIsNotYetCompatibleWithKsp import com.tschuchort.compiletesting.JvmCompilationResult import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK import org.intellij.lang.annotations.Language +import org.junit.Assume.assumeTrue import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -15,14 +18,14 @@ import org.junit.runners.Parameterized.Parameters @RunWith(Parameterized::class) class BindsMethodValidatorTest( - private val useDagger: Boolean, + private val daggerProcessingMode: DaggerAnnotationProcessingMode, ) { companion object { - @Parameters(name = "Use Dagger: {0}") + @Parameters(name = "Dagger Processing Mode: {0}") @JvmStatic - fun useDagger(): Collection { - return listOf(isFullTestRun(), false).distinct() + fun daggerProcessingMode(): Collection { + return daggerProcessingModesForTests() } } @@ -52,7 +55,7 @@ class BindsMethodValidatorTest( assertThat(messages).contains( "@Binds methods' parameter type must be assignable to the return type", ) - if (!useDagger) { + if (daggerProcessingMode == DaggerAnnotationProcessingMode.NONE) { assertThat(messages).contains( "Expected binding of type Bar but impl parameter of type Foo only has the following " + "supertypes: [Ipsum, Lorem]", @@ -85,7 +88,7 @@ class BindsMethodValidatorTest( assertThat(messages).contains( "@Binds methods' parameter type must be assignable to the return type", ) - if (!useDagger) { + if (daggerProcessingMode == DaggerAnnotationProcessingMode.NONE) { assertThat(messages).contains( "Expected binding of type Bar but impl parameter of type Foo has no supertypes.", ) @@ -235,6 +238,7 @@ class BindsMethodValidatorTest( @Test fun `an extension function binding is valid`() { + assumeTrue(daggerProcessingMode != DaggerAnnotationProcessingMode.NONE) val moduleResult = compile( """ package com.squareup.test @@ -269,7 +273,7 @@ class BindsMethodValidatorTest( } """, previousCompilationResult = moduleResult, - enableDagger = true, + daggerProcessingMode = daggerProcessingMode, ) { assertThat(exitCode).isEqualTo(OK) } @@ -277,6 +281,7 @@ class BindsMethodValidatorTest( @Test fun `a binding for a back-ticked-package type is valid`() { + assumeTrue(daggerProcessingMode != DaggerAnnotationProcessingMode.NONE) val moduleResult = compile( """ package com.squareup.`impl` @@ -315,7 +320,7 @@ class BindsMethodValidatorTest( } """, previousCompilationResult = moduleResult, - enableDagger = true, + daggerProcessingMode = daggerProcessingMode, ) { assertThat(exitCode).isEqualTo(OK) } @@ -323,6 +328,11 @@ class BindsMethodValidatorTest( @Test fun `an extension function binding with a qualifier is valid`() { + assumeTrue(daggerProcessingMode != DaggerAnnotationProcessingMode.NONE) + testIsNotYetCompatibleWithKsp( + daggerProcessingMode, + "https://github.com/google/dagger/issues/3990", + ) val moduleResult = compile( """ package com.squareup.test @@ -370,7 +380,7 @@ class BindsMethodValidatorTest( } """, previousCompilationResult = moduleResult, - enableDagger = true, + daggerProcessingMode = daggerProcessingMode, ) { assertThat(exitCode).isEqualTo(OK) } @@ -379,12 +389,12 @@ class BindsMethodValidatorTest( private fun compile( @Language("kotlin") vararg sources: String, previousCompilationResult: JvmCompilationResult? = null, - enableDagger: Boolean = useDagger, + daggerProcessingMode: DaggerAnnotationProcessingMode = DaggerAnnotationProcessingMode.NONE, block: JvmCompilationResult.() -> Unit = { }, ): JvmCompilationResult = compileAnvil( sources = sources, - enableDaggerAnnotationProcessor = enableDagger, - generateDaggerFactories = !enableDagger, + daggerAnnotationProcessingMode = daggerProcessingMode, + generateDaggerFactories = daggerProcessingMode == DaggerAnnotationProcessingMode.NONE, allWarningsAsErrors = WARNINGS_AS_ERRORS, previousCompilationResult = previousCompilationResult, block = block, diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/InjectConstructorFactoryGeneratorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/InjectConstructorFactoryGeneratorTest.kt index cd5c8fc04..ff2ae8990 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/InjectConstructorFactoryGeneratorTest.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/InjectConstructorFactoryGeneratorTest.kt @@ -4,12 +4,14 @@ import com.google.common.truth.Truth.assertThat import com.squareup.anvil.compiler.compilationErrorLine import com.squareup.anvil.compiler.injectClass import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode +import com.squareup.anvil.compiler.internal.testing.DaggerAnnotationProcessingMode import com.squareup.anvil.compiler.internal.testing.compileAnvil import com.squareup.anvil.compiler.internal.testing.createInstance import com.squareup.anvil.compiler.internal.testing.factoryClass import com.squareup.anvil.compiler.internal.testing.getPropertyValue import com.squareup.anvil.compiler.internal.testing.isStatic import com.squareup.anvil.compiler.isError +import com.squareup.anvil.compiler.testRequiresWildcards import com.squareup.anvil.compiler.useDaggerAndKspParams import com.tschuchort.compiletesting.JvmCompilationResult import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK @@ -26,12 +28,12 @@ import javax.inject.Provider @RunWith(Parameterized::class) class InjectConstructorFactoryGeneratorTest( - private val useDagger: Boolean, + private val daggerProcessingMode: DaggerAnnotationProcessingMode, private val mode: AnvilCompilationMode, ) { companion object { - @Parameters(name = "Use Dagger: {0}, mode: {1}") + @Parameters(name = "Dagger Processing Mode: {0}, mode: {1}") @JvmStatic fun params() = useDaggerAndKspParams() } @@ -2328,6 +2330,7 @@ public class InjectClass_Factory>( } */ + testRequiresWildcards(daggerProcessingMode) compile( """ package com.squareup.test @@ -2413,6 +2416,7 @@ public class InjectClass_Factory( // empty list. So, improperly handling `TypeVariableName` can result in a constraint like: // `where T : Any?, T : Appendable, T : Other` // This won't compile since a type can only have one bound which is a class. + testRequiresWildcards(daggerProcessingMode) compile( """ package com.squareup.test @@ -2444,6 +2448,7 @@ public class InjectClass_Factory( } @Test fun `a factory class is generated for a type parameter which extends a generic`() { + testRequiresWildcards(daggerProcessingMode) /* package com.squareup.test @@ -2740,8 +2745,8 @@ public final class InjectClass_Factory implements Factory { block: JvmCompilationResult.() -> Unit = { }, ): JvmCompilationResult = compileAnvil( sources = sources, - enableDaggerAnnotationProcessor = useDagger, - generateDaggerFactories = !useDagger, + daggerAnnotationProcessingMode = daggerProcessingMode, + generateDaggerFactories = daggerProcessingMode == DaggerAnnotationProcessingMode.NONE, // Many constructor parameters are unused. allWarningsAsErrors = false, previousCompilationResult = previousCompilationResult, diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/MapKeyCreatorGeneratorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/MapKeyCreatorGeneratorTest.kt index 997905e2c..16fad129b 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/MapKeyCreatorGeneratorTest.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/MapKeyCreatorGeneratorTest.kt @@ -3,8 +3,10 @@ package com.squareup.anvil.compiler.dagger import com.google.common.truth.Truth.assertThat import com.squareup.anvil.compiler.WARNINGS_AS_ERRORS import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode +import com.squareup.anvil.compiler.internal.testing.DaggerAnnotationProcessingMode import com.squareup.anvil.compiler.internal.testing.compileAnvil import com.squareup.anvil.compiler.internal.testing.isStatic +import com.squareup.anvil.compiler.testIsNotYetCompatibleWithKsp import com.squareup.anvil.compiler.useDaggerAndKspParams import com.tschuchort.compiletesting.JvmCompilationResult import org.intellij.lang.annotations.Language @@ -17,17 +19,21 @@ import org.junit.runners.Parameterized.Parameters @RunWith(Parameterized::class) class MapKeyCreatorGeneratorTest( - private val useDagger: Boolean, + private val daggerProcessingMode: DaggerAnnotationProcessingMode, private val mode: AnvilCompilationMode, ) { companion object { - @Parameters(name = "Use Dagger: {0}, mode: {1}") + @Parameters(name = "Dagger Processing Mode: {0}, mode: {1}") @JvmStatic fun params() = useDaggerAndKspParams() } @Test fun `a creator class is generated`() { + testIsNotYetCompatibleWithKsp( + daggerProcessingMode, + "https://github.com/google/dagger/issues/3993", + ) compile( """ package com.squareup.test @@ -140,6 +146,10 @@ class MapKeyCreatorGeneratorTest( } @Test fun `a recursive annotation still works`() { + testIsNotYetCompatibleWithKsp( + daggerProcessingMode, + "https://github.com/google/dagger/issues/3993", + ) compile( """ package com.squareup.test @@ -248,8 +258,8 @@ class MapKeyCreatorGeneratorTest( block: JvmCompilationResult.() -> Unit = { }, ): JvmCompilationResult = compileAnvil( sources = sources, - enableDaggerAnnotationProcessor = useDagger, - generateDaggerFactories = !useDagger, + daggerAnnotationProcessingMode = daggerProcessingMode, + generateDaggerFactories = daggerProcessingMode == DaggerAnnotationProcessingMode.NONE, allWarningsAsErrors = WARNINGS_AS_ERRORS, mode = mode, block = block, diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/MembersInjectorGeneratorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/MembersInjectorGeneratorTest.kt index 7e7784942..1ec5884f4 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/MembersInjectorGeneratorTest.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/MembersInjectorGeneratorTest.kt @@ -5,6 +5,7 @@ import com.squareup.anvil.compiler.WARNINGS_AS_ERRORS import com.squareup.anvil.compiler.injectClass import com.squareup.anvil.compiler.internal.capitalize import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode +import com.squareup.anvil.compiler.internal.testing.DaggerAnnotationProcessingMode import com.squareup.anvil.compiler.internal.testing.compileAnvil import com.squareup.anvil.compiler.internal.testing.createInstance import com.squareup.anvil.compiler.internal.testing.getPropertyValue @@ -19,6 +20,7 @@ import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK import dagger.Lazy import dagger.MembersInjector import org.intellij.lang.annotations.Language +import org.junit.Assume.assumeTrue import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -33,12 +35,12 @@ import kotlin.test.assertFailsWith @Suppress("UNCHECKED_CAST") @RunWith(Parameterized::class) class MembersInjectorGeneratorTest( - private val useDagger: Boolean, + private val daggerProcessingMode: DaggerAnnotationProcessingMode, private val mode: AnvilCompilationMode, ) { companion object { - @Parameters(name = "Use Dagger: {0}, mode: {1}") + @Parameters(name = "Dagger Processing Mode: {0}, mode: {1}") @JvmStatic fun params() = useDaggerAndKspParams() } @@ -2261,6 +2263,8 @@ public final class InjectClass_MembersInjector implements MembersInject """, ) { + // TODO regressed in Dagger 2.50 https://github.com/google/dagger/issues/4199 + assumeTrue(daggerProcessingMode != DaggerAnnotationProcessingMode.KSP) val actualBaseMembersInjector = classLoader.loadClass("com.squareup.test.ActualBase") .membersInjector() @@ -2343,7 +2347,8 @@ public final class InjectClass_MembersInjector implements MembersInject """, previousCompilationResult = otherModuleResult, ) { - + // TODO regressed in Dagger 2.50 https://github.com/google/dagger/issues/4199 + assumeTrue(daggerProcessingMode != DaggerAnnotationProcessingMode.KSP) val actualBaseMembersInjector = classLoader.loadClass("com.squareup.test.ActualBase") .membersInjector() @@ -2509,8 +2514,8 @@ public final class InjectClass_MembersInjector implements MembersInject block: JvmCompilationResult.() -> Unit = { }, ): JvmCompilationResult = compileAnvil( sources = sources, - enableDaggerAnnotationProcessor = useDagger, - generateDaggerFactories = !useDagger, + daggerAnnotationProcessingMode = daggerProcessingMode, + generateDaggerFactories = daggerProcessingMode == DaggerAnnotationProcessingMode.NONE, allWarningsAsErrors = WARNINGS_AS_ERRORS, previousCompilationResult = previousCompilationResult, mode = mode, diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/ProvidesMethodFactoryGeneratorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/ProvidesMethodFactoryGeneratorTest.kt index 6b40a3aa1..c0b1600ff 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/dagger/ProvidesMethodFactoryGeneratorTest.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/dagger/ProvidesMethodFactoryGeneratorTest.kt @@ -10,6 +10,7 @@ import com.squareup.anvil.compiler.dagger.UppercasePackage.lowerCaseClassInUpper import com.squareup.anvil.compiler.daggerModule1 import com.squareup.anvil.compiler.innerModule import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode +import com.squareup.anvil.compiler.internal.testing.DaggerAnnotationProcessingMode import com.squareup.anvil.compiler.internal.testing.compileAnvil import com.squareup.anvil.compiler.internal.testing.createInstance import com.squareup.anvil.compiler.internal.testing.isStatic @@ -22,6 +23,7 @@ import dagger.Lazy import dagger.internal.Factory import org.intellij.lang.annotations.Language import org.junit.Assume.assumeFalse +import org.junit.Assume.assumeTrue import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -33,12 +35,12 @@ import javax.inject.Provider @Suppress("UNCHECKED_CAST") @RunWith(Parameterized::class) class ProvidesMethodFactoryGeneratorTest( - private val useDagger: Boolean, + private val daggerProcessingMode: DaggerAnnotationProcessingMode, private val mode: AnvilCompilationMode, ) { companion object { - @Parameters(name = "Use Dagger: {0}, mode: {1}") + @Parameters(name = "Dagger Processing Mode: {0}, mode: {1}") @JvmStatic fun params() = useDaggerAndKspParams() } @@ -1777,7 +1779,7 @@ public final class DaggerModule1_ProvideStringFactory implements Factory } """, ) { - if (useDagger) { + if (daggerProcessingMode != DaggerAnnotationProcessingMode.NONE) { assertThat(sourcesGeneratedByAnnotationProcessor).isEmpty() } } @@ -2024,7 +2026,7 @@ public final class ComponentInterface_InnerModule_Companion_ProvideStringFactory } """, ) { - if (useDagger) { + if (daggerProcessingMode != DaggerAnnotationProcessingMode.NONE) { assertThat(sourcesGeneratedByAnnotationProcessor).isEmpty() } } @@ -2558,7 +2560,7 @@ public final class DaggerComponentInterface implements ComponentInterface { } """, ) { - assumeFalse(useDagger) + assumeFalse(daggerProcessingMode != DaggerAnnotationProcessingMode.NONE) if (mode is AnvilCompilationMode.Ksp) { // KSP always resolves the inferred return type assertThat(exitCode).isEqualTo(ExitCode.OK) @@ -3225,6 +3227,7 @@ public final class DaggerModule1_ProvideFunctionFactory implements Factory> } """, - enableDagger = true, + daggerProcessingMode = daggerProcessingMode, previousCompilationResult = otherModuleResult, ) { // TODO component generation isn't possible with KSP yet @@ -3316,6 +3319,7 @@ public final class DaggerModule1_ProvideFunctionFactory implements Factory } """, - enableDagger = true, + daggerProcessingMode = daggerProcessingMode, previousCompilationResult = otherModuleResult, ) { // TODO component generation isn't possible with KSP yet @@ -3512,14 +3516,14 @@ public final class DaggerModule1_ProvideFunctionFactory implements Factory Unit = { }, ): JvmCompilationResult = compileAnvil( sources = sources, - enableDaggerAnnotationProcessor = enableDagger, - generateDaggerFactories = !enableDagger, + daggerAnnotationProcessingMode = daggerProcessingMode, + generateDaggerFactories = daggerProcessingMode == DaggerAnnotationProcessingMode.NONE, allWarningsAsErrors = WARNINGS_AS_ERRORS, block = block, mode = mode, diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts index 588b307e9..184671c08 100644 --- a/gradle-plugin/build.gradle.kts +++ b/gradle-plugin/build.gradle.kts @@ -83,6 +83,7 @@ dependencies { compileOnly(libs.kotlin.gradlePlugin) compileOnly(libs.kotlin.gradlePluginApi) compileOnly(libs.agp) + compileOnly(libs.ksp.gradlePlugin) testImplementation(libs.junit) testImplementation(libs.truth) diff --git a/gradle-plugin/src/main/java/com/squareup/anvil/plugin/AnvilExtension.kt b/gradle-plugin/src/main/java/com/squareup/anvil/plugin/AnvilExtension.kt index 2f12d0325..f252b2c7a 100644 --- a/gradle-plugin/src/main/java/com/squareup/anvil/plugin/AnvilExtension.kt +++ b/gradle-plugin/src/main/java/com/squareup/anvil/plugin/AnvilExtension.kt @@ -57,6 +57,13 @@ public abstract class AnvilExtension @Inject constructor(objects: ObjectFactory) public val addOptionalAnnotations: Property = objects.property(Boolean::class.java) .convention(false) + /** + * Enables experimental KSP support in component merging. This is only + * useful if using Dagger with KSP and if [disableComponentMerging] is set to `false`. + */ + public val enableKspComponentMerging: Property = objects.property(Boolean::class.java) + .convention(false) + @Suppress("PropertyName") internal var _variantFilter: Action? = null diff --git a/gradle-plugin/src/main/java/com/squareup/anvil/plugin/AnvilPlugin.kt b/gradle-plugin/src/main/java/com/squareup/anvil/plugin/AnvilPlugin.kt index 7cb20bcb4..31a166cda 100644 --- a/gradle-plugin/src/main/java/com/squareup/anvil/plugin/AnvilPlugin.kt +++ b/gradle-plugin/src/main/java/com/squareup/anvil/plugin/AnvilPlugin.kt @@ -8,6 +8,7 @@ import com.android.build.gradle.BaseExtension import com.android.build.gradle.LibraryExtension import com.android.build.gradle.TestExtension import com.android.build.gradle.TestedExtension +import com.google.devtools.ksp.gradle.KspExtension import org.gradle.api.Action import org.gradle.api.GradleException import org.gradle.api.Project @@ -49,7 +50,21 @@ internal open class AnvilPlugin : KotlinCompilerPluginSupportPlugin { private val variantCache = ConcurrentHashMap() override fun apply(target: Project) { - target.extensions.create("anvil", AnvilExtension::class.java) + val anvilExtension = target.extensions.create("anvil", AnvilExtension::class.java) + + target.pluginManager.withPlugin("com.google.devtools.ksp") { + // TODO would be nice if KSP made this a property so these + // could be chained nicely without afterEvaluate + target.afterEvaluate { + if (anvilExtension.enableKspComponentMerging.get()) { + target.extensions.configure(KspExtension::class.java) { kspExtension -> + kspExtension.excludeProcessor("dagger.internal.codegen.KspComponentProcessor") + } + + target.dependencies.add("ksp", "$GROUP:compiler:$VERSION") + } + } + } // Create a configuration for collecting CodeGenerator dependencies. We need to create all // configurations eagerly and cannot wait for applyToCompilation(..) below, because this @@ -136,9 +151,11 @@ internal open class AnvilPlugin : KotlinCompilerPluginSupportPlugin { project.configurations.getByName(variant.compilerPluginClasspathName) .extendsFrom(getConfiguration(project, variant.name)) - disableIncrementalKotlinCompilation(variant) + if (!variant.variantFilter.enableKspComponentMerging) { + disableIncrementalKotlinCompilation(variant) + } - if (!variant.variantFilter.generateDaggerFactoriesOnly) { + if (!variant.variantFilter.generateDaggerFactoriesOnly || !variant.variantFilter.enableKspComponentMerging) { disableCorrectErrorTypes(variant) kotlinCompilation.dependencies { diff --git a/gradle-plugin/src/main/java/com/squareup/anvil/plugin/VariantFilter.kt b/gradle-plugin/src/main/java/com/squareup/anvil/plugin/VariantFilter.kt index 72b5d59c0..3f5714e5a 100644 --- a/gradle-plugin/src/main/java/com/squareup/anvil/plugin/VariantFilter.kt +++ b/gradle-plugin/src/main/java/com/squareup/anvil/plugin/VariantFilter.kt @@ -42,6 +42,12 @@ public interface VariantFilter : Named { * details. */ public var addOptionalAnnotations: Boolean + + /** + * Enables experimental KSP support in component merging. This is only + * useful if using Dagger with KSP and if [disableComponentMerging] is set to `false`. + */ + public var enableKspComponentMerging: Boolean } internal class CommonFilter( @@ -85,6 +91,13 @@ internal class CommonFilter( set(value) { addOptionalAnnotationsOverride = value } + + private var useKspOverride: Boolean? = null + override var enableKspComponentMerging: Boolean + get() = useKspOverride ?: extension.enableKspComponentMerging.get() + set(value) { + useKspOverride = value + } } public class JvmVariantFilter internal constructor( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0ef21b9e1..d9bcfb421 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ androidx-test-ext = "1.1.5" autoService = "1.1.1" autoValue = "1.10.4" buildconfig = "4.2.0" -dagger = "2.46.1" +dagger = "2.49" dropbox-dependencyGuard = "0.4.3" dokka = "1.9.10" espresso = "3.5.1" @@ -26,7 +26,7 @@ jvm-toolchain = "11" jvm-target-library = "8" jvm-target-minimal = "11" kase = "0.4.0" -kct = "0.3.1" +kct = "0.3.2" kgx = "0.1.9" kotlin = "1.9.10" kotlinx-binaryCompatibility = "0.13.2" @@ -92,6 +92,8 @@ dropbox-dependencyGuard = { module = "com.dropbox.dependency-guard:dependency-gu gradlePublishRaw = { module = "com.gradle.publish:plugin-publish-plugin", version.ref = "gradlePublish" } +guava = "com.google.guava:guava:33.0.0-jre" + inject = "javax.inject:javax.inject:1" jsr250 = "javax.annotation:jsr250-api:1.0" junit = "junit:junit:4.13.2"