diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesSubcomponentCodeGen.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesSubcomponentCodeGen.kt new file mode 100644 index 000000000..d314b3c63 --- /dev/null +++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesSubcomponentCodeGen.kt @@ -0,0 +1,463 @@ +package com.squareup.anvil.compiler.codegen + +import com.google.auto.service.AutoService +import com.google.devtools.ksp.getVisibility +import com.google.devtools.ksp.isAbstract +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.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.squareup.anvil.annotations.ContributesSubcomponent +import com.squareup.anvil.annotations.ContributesTo +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.AnvilApplicabilityChecker +import com.squareup.anvil.compiler.api.AnvilContext +import com.squareup.anvil.compiler.api.CodeGenerator +import com.squareup.anvil.compiler.api.GeneratedFile +import com.squareup.anvil.compiler.api.createGeneratedFile +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.getKSAnnotationsByType +import com.squareup.anvil.compiler.codegen.ksp.isAnnotationPresent +import com.squareup.anvil.compiler.codegen.ksp.isInterface +import com.squareup.anvil.compiler.codegen.ksp.parentScope +import com.squareup.anvil.compiler.codegen.ksp.replaces +import com.squareup.anvil.compiler.codegen.ksp.resolveKSClassDeclaration +import com.squareup.anvil.compiler.codegen.ksp.scope +import com.squareup.anvil.compiler.contributesSubcomponentFactoryFqName +import com.squareup.anvil.compiler.contributesSubcomponentFqName +import com.squareup.anvil.compiler.contributesToFqName +import com.squareup.anvil.compiler.daggerSubcomponentBuilderFqName +import com.squareup.anvil.compiler.daggerSubcomponentFactoryFqName +import com.squareup.anvil.compiler.internal.createAnvilSpec +import com.squareup.anvil.compiler.internal.reference.AnvilCompilationExceptionClassReference +import com.squareup.anvil.compiler.internal.reference.ClassReference +import com.squareup.anvil.compiler.internal.reference.Visibility +import com.squareup.anvil.compiler.internal.reference.asClassName +import com.squareup.anvil.compiler.internal.reference.classAndInnerClassReferences +import com.squareup.anvil.compiler.internal.reference.generateClassName +import com.squareup.anvil.compiler.internal.safePackageString +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.KModifier.PUBLIC +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.writeTo +import dagger.Subcomponent +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.psi.KtFile +import java.io.File +import kotlin.reflect.KClass + +/** + * Generates a hint for each contributed subcomponent in the `anvil.hint.subcomponent` packages. + * This allows the compiler plugin to find all contributed classes a lot faster. + */ +internal object ContributesSubcomponentCodeGen : AnvilApplicabilityChecker { + override fun isApplicable(context: AnvilContext) = !context.generateFactoriesOnly + + internal class KspGenerator( + override val env: SymbolProcessorEnvironment, + ) : AnvilSymbolProcessor() { + @AutoService(SymbolProcessorProvider::class) + class Provider : AnvilSymbolProcessorProvider(ContributesSubcomponentCodeGen, ::KspGenerator) + + override fun processChecked(resolver: Resolver): List { + resolver.getSymbolsWithAnnotation(contributesSubcomponentFqName.asString()) + .filterIsInstance() + .onEach { clazz -> + if (!clazz.isInterface() && !clazz.isAbstract()) { + throw KspAnvilException( + message = "${clazz.qualifiedName?.asString()} is annotated with " + + "@${ContributesSubcomponent::class.simpleName}, but this class is not an interface.", + node = clazz, + ) + } + + if (clazz.getVisibility() != com.google.devtools.ksp.symbol.Visibility.PUBLIC) { + throw KspAnvilException( + message = "${clazz.qualifiedName?.asString()} is contributed to the Dagger graph, but the " + + "interface is not public. Only public interfaces are supported.", + node = clazz, + ) + } + + clazz.getKSAnnotationsByType(ContributesSubcomponent::class) + .forEach { annotation -> + for (it in annotation.replaces()) { + val scope = annotation.scope().resolveKSClassDeclaration() ?: throw KspAnvilException( + message = "Couldn't resolve the scope for ${clazz.qualifiedName?.asString()}.", + node = clazz, + ) + it.checkUsesSameScope(scope, clazz) + } + } + } + .forEach { clazz -> + clazz.checkFactory(clazz.declarations.filterIsInstance()) + val className = clazz.toClassName() + val parentScopeDeclaration = clazz.getKSAnnotationsByType(ContributesSubcomponent::class) + .single() + .parentScope() + clazz.checkParentComponentInterface( + clazz.declarations.filterIsInstance(), + parentScopeDeclaration, + ) + val parentScope = parentScopeDeclaration.toClassName() + + createSpec(className, parentScope) + .writeTo( + env.codeGenerator, + aggregating = false, + originatingKSFiles = listOf(clazz.containingFile!!), + ) + } + + return emptyList() + } + + private fun KSClassDeclaration.checkParentComponentInterface( + innerClasses: Sequence, + parentScope: KSClassDeclaration, + ) { + val parentComponents = innerClasses + .filter { + it.getKSAnnotationsByType(ContributesTo::class) + .any { annotation -> + annotation.scope().resolveKSClassDeclaration() == parentScope + } + } + .toList() + + val componentInterface = when (parentComponents.size) { + 0 -> return + 1 -> parentComponents[0] + else -> throw KspAnvilException( + node = this, + message = "Expected zero or one parent component interface within " + + "${qualifiedName?.asString()} being contributed to the parent scope.", + ) + } + + // TODO could just declared functions work? + val functions = componentInterface.getAllFunctions() + .filter { + it.returnType?.resolve()?.resolveKSClassDeclaration() == this + } + .toList() + + if (functions.size >= 2) { + throw KspAnvilException( + node = componentInterface, + message = "Expected zero or one function returning the subcomponent ${qualifiedName?.asString()}.", + ) + } + } + + private fun KSClassDeclaration.checkFactory(innerClasses: Sequence) { + innerClasses + .firstOrNull { it.isAnnotationPresent() } + ?.let { factoryClass -> + throw KspAnvilException( + node = factoryClass, + message = "Within a class using @${ContributesSubcomponent::class.simpleName} you " + + "must use $contributesSubcomponentFactoryFqName and not " + + "$daggerSubcomponentFactoryFqName.", + ) + } + + innerClasses + .firstOrNull { it.isAnnotationPresent() } + ?.let { factoryClass -> + throw KspAnvilException( + node = factoryClass, + message = "Within a class using @${ContributesSubcomponent::class.simpleName} you " + + "must use $contributesSubcomponentFactoryFqName and not " + + "$daggerSubcomponentBuilderFqName. Builders aren't supported.", + ) + } + + val factories = innerClasses + .filter { it.isAnnotationPresent() } + .toList() + + val factory = when (factories.size) { + 0 -> return + 1 -> factories[0] + else -> throw KspAnvilException( + node = this, + message = "Expected zero or one factory within ${qualifiedName?.asString()}.", + ) + } + + if (!factory.isInterface() && !factory.isAbstract()) { + throw KspAnvilException( + node = factory, + message = "A factory must be an interface or an abstract class.", + ) + } + + val functions = factory.getAllFunctions() + .let { functions -> + if (factory.isInterface()) { + functions + } else { + functions.filter { it.isAbstract } + } + } + .toList() + + if (functions.size != 1 || functions[0].returnType?.resolve()?.resolveKSClassDeclaration() != this) { + throw KspAnvilException( + node = factory, + message = "A factory must have exactly one abstract function returning the " + + "subcomponent ${qualifiedName?.asString()}.", + ) + } + } + + private fun KSClassDeclaration.checkUsesSameScope( + scope: KSClassDeclaration, + subcomponent: KSClassDeclaration, + ) { + getKSAnnotationsByType(ContributesSubcomponent::class) + .ifEmpty { + throw KspAnvilException( + node = subcomponent, + message = "Couldn't find the annotation @ContributesSubcomponent for ${qualifiedName?.asString()}.", + ) + } + .forEach { annotation -> + val otherScope = annotation.scope().resolveKSClassDeclaration() ?: return@forEach + if (otherScope != scope) { + throw KspAnvilException( + node = subcomponent, + message = "${subcomponent.qualifiedName?.asString()} with scope ${scope.qualifiedName?.asString()} wants to replace " + + "${qualifiedName?.asString()} with scope ${otherScope.qualifiedName?.asString()}. The replacement must use the same scope.", + ) + } + } + } + } + + @AutoService(CodeGenerator::class) + internal class Embedded : CodeGenerator { + + override fun isApplicable(context: AnvilContext) = ContributesSubcomponentCodeGen.isApplicable( + context, + ) + + override fun generateCode( + codeGenDir: File, + module: ModuleDescriptor, + projectFiles: Collection, + ): Collection { + return projectFiles + .classAndInnerClassReferences(module) + .filter { it.isAnnotatedWith(contributesSubcomponentFqName) } + .onEach { clazz -> + if (!clazz.isInterface() && !clazz.isAbstract()) { + throw AnvilCompilationExceptionClassReference( + message = "${clazz.fqName} is annotated with " + + "@${ContributesSubcomponent::class.simpleName}, but this class is not an interface.", + classReference = clazz, + ) + } + + if (clazz.visibility() != Visibility.PUBLIC) { + throw AnvilCompilationExceptionClassReference( + message = "${clazz.fqName} is contributed to the Dagger graph, but the " + + "interface is not public. Only public interfaces are supported.", + classReference = clazz, + ) + } + + clazz.annotations + .filter { it.fqName == contributesSubcomponentFqName } + .forEach { annotation -> + annotation.replaces().forEach { + it.checkUsesSameScope(annotation.scope(), clazz) + } + } + } + .map { clazz -> + clazz.checkFactory(clazz.innerClasses()) + val className = clazz.asClassName() + val parentScopeReference = clazz.annotations + .single { it.fqName == contributesSubcomponentFqName } + .parentScope() + clazz.checkParentComponentInterface(clazz.innerClasses(), parentScopeReference) + val parentScope = parentScopeReference.asClassName() + + val spec = createSpec(className, parentScope) + + createGeneratedFile( + codeGenDir = codeGenDir, + packageName = spec.packageName, + fileName = spec.name, + content = spec.toString(), + ) + } + .toList() + } + + private fun ClassReference.checkParentComponentInterface( + innerClasses: List, + parentScope: ClassReference, + ) { + val parentComponents = innerClasses + .filter { + it.annotations.any { annotation -> + annotation.fqName == contributesToFqName && annotation.scope() == parentScope + } + } + + val componentInterface = when (parentComponents.size) { + 0 -> return + 1 -> parentComponents[0] + else -> throw AnvilCompilationExceptionClassReference( + classReference = this, + message = "Expected zero or one parent component interface within " + + "$fqName being contributed to the parent scope.", + ) + } + + val functions = componentInterface.functions + .filter { it.returnType().asClassReference() == this } + + if (functions.size >= 2) { + throw AnvilCompilationExceptionClassReference( + classReference = componentInterface, + message = "Expected zero or one function returning the subcomponent $fqName.", + ) + } + } + + private fun ClassReference.checkFactory(innerClasses: List) { + innerClasses + .firstOrNull { it.isAnnotatedWith(daggerSubcomponentFactoryFqName) } + ?.let { factoryClass -> + throw AnvilCompilationExceptionClassReference( + classReference = factoryClass, + message = "Within a class using @${ContributesSubcomponent::class.simpleName} you " + + "must use $contributesSubcomponentFactoryFqName and not " + + "$daggerSubcomponentFactoryFqName.", + ) + } + + innerClasses + .firstOrNull { it.isAnnotatedWith(daggerSubcomponentBuilderFqName) } + ?.let { factoryClass -> + throw AnvilCompilationExceptionClassReference( + classReference = factoryClass, + message = "Within a class using @${ContributesSubcomponent::class.simpleName} you " + + "must use $contributesSubcomponentFactoryFqName and not " + + "$daggerSubcomponentBuilderFqName. Builders aren't supported.", + ) + } + + val factories = innerClasses + .filter { it.isAnnotatedWith(contributesSubcomponentFactoryFqName) } + + val factory = when (factories.size) { + 0 -> return + 1 -> factories[0] + else -> throw AnvilCompilationExceptionClassReference( + classReference = this, + message = "Expected zero or one factory within $fqName.", + ) + } + + if (!factory.isInterface() && !factory.isAbstract()) { + throw AnvilCompilationExceptionClassReference( + classReference = factory, + message = "A factory must be an interface or an abstract class.", + ) + } + + val functions = factory.functions + .let { functions -> + if (factory.isInterface()) { + functions + } else { + functions.filter { it.isAbstract() } + } + } + + if (functions.size != 1 || functions[0].returnType().asClassReference() != this) { + throw AnvilCompilationExceptionClassReference( + classReference = factory, + message = "A factory must have exactly one abstract function returning the " + + "subcomponent $fqName.", + ) + } + } + + private fun ClassReference.checkUsesSameScope( + scope: ClassReference, + subcomponent: ClassReference, + ) { + annotations + .filter { it.fqName == contributesSubcomponentFqName } + .ifEmpty { + throw AnvilCompilationExceptionClassReference( + classReference = subcomponent, + message = "Couldn't find the annotation @ContributesSubcomponent for $fqName.", + ) + } + .forEach { annotation -> + val otherScope = annotation.scope() + if (otherScope != scope) { + throw AnvilCompilationExceptionClassReference( + classReference = subcomponent, + message = "${subcomponent.fqName} with scope ${scope.fqName} wants to replace " + + "$fqName with scope ${otherScope.fqName}. The replacement must use the same scope.", + ) + } + } + } + } + + private fun createSpec( + className: ClassName, + parentScope: ClassName, + ): FileSpec { + val fileName = className.generateClassName().simpleName + val generatedPackage = HINT_SUBCOMPONENTS_PACKAGE_PREFIX + + className.packageName.safePackageString(dotPrefix = true) + val classFqName = className.canonicalName + val propertyName = classFqName.replace('.', '_') + + val spec = + FileSpec.createAnvilSpec(generatedPackage, fileName) { + addProperty( + PropertySpec + .builder( + name = propertyName + REFERENCE_SUFFIX, + type = KClass::class.asClassName().parameterizedBy(className), + ) + .initializer("%T::class", className) + .addModifiers(PUBLIC) + .build(), + ) + + addProperty( + PropertySpec + .builder( + name = propertyName + SCOPE_SUFFIX, + type = KClass::class.asClassName().parameterizedBy(parentScope), + ) + .initializer("%T::class", parentScope) + .addModifiers(PUBLIC) + .build(), + ) + } + return spec + } +} diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesSubcomponentGenerator.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesSubcomponentGenerator.kt deleted file mode 100644 index 54b4e2987..000000000 --- a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesSubcomponentGenerator.kt +++ /dev/null @@ -1,242 +0,0 @@ -package com.squareup.anvil.compiler.codegen - -import com.google.auto.service.AutoService -import com.squareup.anvil.annotations.ContributesSubcomponent -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.AnvilContext -import com.squareup.anvil.compiler.api.CodeGenerator -import com.squareup.anvil.compiler.api.GeneratedFile -import com.squareup.anvil.compiler.api.createGeneratedFile -import com.squareup.anvil.compiler.contributesSubcomponentFactoryFqName -import com.squareup.anvil.compiler.contributesSubcomponentFqName -import com.squareup.anvil.compiler.contributesToFqName -import com.squareup.anvil.compiler.daggerSubcomponentBuilderFqName -import com.squareup.anvil.compiler.daggerSubcomponentFactoryFqName -import com.squareup.anvil.compiler.internal.buildFile -import com.squareup.anvil.compiler.internal.reference.AnvilCompilationExceptionClassReference -import com.squareup.anvil.compiler.internal.reference.ClassReference -import com.squareup.anvil.compiler.internal.reference.Visibility -import com.squareup.anvil.compiler.internal.reference.asClassName -import com.squareup.anvil.compiler.internal.reference.classAndInnerClassReferences -import com.squareup.anvil.compiler.internal.reference.generateClassName -import com.squareup.anvil.compiler.internal.safePackageString -import com.squareup.kotlinpoet.FileSpec -import com.squareup.kotlinpoet.KModifier.PUBLIC -import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy -import com.squareup.kotlinpoet.PropertySpec -import com.squareup.kotlinpoet.asClassName -import org.jetbrains.kotlin.descriptors.ModuleDescriptor -import org.jetbrains.kotlin.psi.KtFile -import java.io.File -import kotlin.reflect.KClass - -/** - * Generates a hint for each contributed subcomponent in the `anvil.hint.subcomponent` packages. - * This allows the compiler plugin to find all contributed classes a lot faster. - */ -@AutoService(CodeGenerator::class) -internal class ContributesSubcomponentGenerator : CodeGenerator { - - override fun isApplicable(context: AnvilContext) = !context.generateFactoriesOnly - - override fun generateCode( - codeGenDir: File, - module: ModuleDescriptor, - projectFiles: Collection, - ): Collection { - return projectFiles - .classAndInnerClassReferences(module) - .filter { it.isAnnotatedWith(contributesSubcomponentFqName) } - .onEach { clazz -> - if (!clazz.isInterface() && !clazz.isAbstract()) { - throw AnvilCompilationExceptionClassReference( - message = "${clazz.fqName} is annotated with " + - "@${ContributesSubcomponent::class.simpleName}, but this class is not an interface.", - classReference = clazz, - ) - } - - if (clazz.visibility() != Visibility.PUBLIC) { - throw AnvilCompilationExceptionClassReference( - message = "${clazz.fqName} is contributed to the Dagger graph, but the " + - "interface is not public. Only public interfaces are supported.", - classReference = clazz, - ) - } - - clazz.annotations - .filter { it.fqName == contributesSubcomponentFqName } - .forEach { annotation -> - annotation.replaces().forEach { - it.checkUsesSameScope(annotation.scope(), clazz) - } - } - } - .map { clazz -> - val fileName = clazz.generateClassName().relativeClassName.asString() - val generatedPackage = HINT_SUBCOMPONENTS_PACKAGE_PREFIX + - clazz.packageFqName.safePackageString(dotPrefix = true) - val className = clazz.asClassName() - val classFqName = clazz.fqName.toString() - val propertyName = classFqName.replace('.', '_') - val parentScopeReference = clazz.annotations - .single { it.fqName == contributesSubcomponentFqName } - .parentScope() - val parentScope = parentScopeReference.asClassName() - - clazz.checkParentComponentInterface(clazz.innerClasses(), parentScopeReference) - clazz.checkFactory(clazz.innerClasses()) - - val content = - FileSpec.buildFile(generatedPackage, fileName) { - addProperty( - PropertySpec - .builder( - name = propertyName + REFERENCE_SUFFIX, - type = KClass::class.asClassName().parameterizedBy(className), - ) - .initializer("%T::class", className) - .addModifiers(PUBLIC) - .build(), - ) - - addProperty( - PropertySpec - .builder( - name = propertyName + SCOPE_SUFFIX, - type = KClass::class.asClassName().parameterizedBy(parentScope), - ) - .initializer("%T::class", parentScope) - .addModifiers(PUBLIC) - .build(), - ) - } - - createGeneratedFile( - codeGenDir = codeGenDir, - packageName = generatedPackage, - fileName = fileName, - content = content, - ) - } - .toList() - } - - private fun ClassReference.checkParentComponentInterface( - innerClasses: List, - parentScope: ClassReference, - ) { - val parentComponents = innerClasses - .filter { - it.annotations.any { annotation -> - annotation.fqName == contributesToFqName && annotation.scope() == parentScope - } - } - - val componentInterface = when (parentComponents.size) { - 0 -> return - 1 -> parentComponents[0] - else -> throw AnvilCompilationExceptionClassReference( - classReference = this, - message = "Expected zero or one parent component interface within " + - "$fqName being contributed to the parent scope.", - ) - } - - val functions = componentInterface.functions - .filter { it.returnType().asClassReference() == this } - - if (functions.size >= 2) { - throw AnvilCompilationExceptionClassReference( - classReference = componentInterface, - message = "Expected zero or one function returning the subcomponent $fqName.", - ) - } - } - - private fun ClassReference.checkFactory(innerClasses: List) { - innerClasses - .firstOrNull { it.isAnnotatedWith(daggerSubcomponentFactoryFqName) } - ?.let { factoryClass -> - throw AnvilCompilationExceptionClassReference( - classReference = factoryClass, - message = "Within a class using @${ContributesSubcomponent::class.simpleName} you " + - "must use $contributesSubcomponentFactoryFqName and not " + - "$daggerSubcomponentFactoryFqName.", - ) - } - - innerClasses - .firstOrNull { it.isAnnotatedWith(daggerSubcomponentBuilderFqName) } - ?.let { factoryClass -> - throw AnvilCompilationExceptionClassReference( - classReference = factoryClass, - message = "Within a class using @${ContributesSubcomponent::class.simpleName} you " + - "must use $contributesSubcomponentFactoryFqName and not " + - "$daggerSubcomponentBuilderFqName. Builders aren't supported.", - ) - } - - val factories = innerClasses - .filter { it.isAnnotatedWith(contributesSubcomponentFactoryFqName) } - - val factory = when (factories.size) { - 0 -> return - 1 -> factories[0] - else -> throw AnvilCompilationExceptionClassReference( - classReference = this, - message = "Expected zero or one factory within $fqName.", - ) - } - - if (!factory.isInterface() && !factory.isAbstract()) { - throw AnvilCompilationExceptionClassReference( - classReference = factory, - message = "A factory must be an interface or an abstract class.", - ) - } - - val functions = factory.functions - .let { functions -> - if (factory.isInterface()) { - functions - } else { - functions.filter { it.isAbstract() } - } - } - - if (functions.size != 1 || functions[0].returnType().asClassReference() != this) { - throw AnvilCompilationExceptionClassReference( - classReference = factory, - message = "A factory must have exactly one abstract function returning the " + - "subcomponent $fqName.", - ) - } - } - - private fun ClassReference.checkUsesSameScope( - scope: ClassReference, - subcomponent: ClassReference, - ) { - annotations - .filter { it.fqName == contributesSubcomponentFqName } - .ifEmpty { - throw AnvilCompilationExceptionClassReference( - classReference = subcomponent, - message = "Couldn't find the annotation @ContributesSubcomponent for $fqName.", - ) - } - .forEach { annotation -> - val otherScope = annotation.scope() - if (otherScope != scope) { - throw AnvilCompilationExceptionClassReference( - classReference = subcomponent, - message = "${subcomponent.fqName} with scope ${scope.fqName} wants to replace " + - "$fqName with scope ${otherScope.fqName}. The replacement must use the same scope.", - ) - } - } - } -} 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..8987a318d 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 @@ -9,6 +9,7 @@ import com.google.devtools.ksp.symbol.KSType 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.internal.reference.argumentAt import com.squareup.anvil.compiler.isAnvilModule import com.squareup.anvil.compiler.qualifierFqName import com.squareup.kotlinpoet.ksp.toClassName @@ -93,6 +94,29 @@ internal fun KSAnnotation.scopeOrNull(): KSType? { internal fun KSAnnotation.boundTypeOrNull(): KSType? = argumentAt("boundType")?.value as? KSType? +@Suppress("UNCHECKED_CAST") +internal fun KSAnnotation.replaces(): List = + (argumentAt("replaces")?.value as? List).orEmpty().map { + it.resolveKSClassDeclaration() ?: throw KspAnvilException("Could not resolve replaces type $it}", this) + } + +@Suppress("UNCHECKED_CAST") +internal fun KSAnnotation.exclude(): List = + (argumentAt("exclude")?.value as? List).orEmpty().map { + it.resolveKSClassDeclaration() ?: throw KspAnvilException("Could not resolve exclude $it", this) + } + +internal fun KSAnnotation.parentScope(): KSClassDeclaration { + return ( + argumentAt("parentScope") + ?.value as? KSType + )?.resolveKSClassDeclaration() + ?: throw KspAnvilException( + message = "Couldn't find parentScope for $shortName.", + node = this, + ) +} + internal fun KSAnnotation.argumentAt( name: String, ): KSValueArgument? { diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesSubcomponentGeneratorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesSubcomponentGeneratorTest.kt index 387614d39..e4e4b7d02 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesSubcomponentGeneratorTest.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesSubcomponentGeneratorTest.kt @@ -5,13 +5,28 @@ import com.squareup.anvil.annotations.ContributesTo import com.squareup.anvil.compiler.compile import com.squareup.anvil.compiler.hintSubcomponent import com.squareup.anvil.compiler.hintSubcomponentParentScope +import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode import com.squareup.anvil.compiler.isError import com.squareup.anvil.compiler.subcomponentInterface +import com.squareup.anvil.compiler.walkGeneratedFiles import com.tschuchort.compiletesting.JvmCompilationResult import org.junit.Test -import java.io.File - -class ContributesSubcomponentGeneratorTest { +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +class ContributesSubcomponentGeneratorTest( + private val mode: AnvilCompilationMode, +) { + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data(): List = listOf( + AnvilCompilationMode.Ksp(), + AnvilCompilationMode.Embedded(), + ) + } @Test fun `there is a hint for contributed subcomponents`() { compile( @@ -23,13 +38,12 @@ class ContributesSubcomponentGeneratorTest { @ContributesSubcomponent(Any::class, Unit::class) interface SubcomponentInterface """, + mode = mode, ) { assertThat(subcomponentInterface.hintSubcomponent?.java).isEqualTo(subcomponentInterface) assertThat(subcomponentInterface.hintSubcomponentParentScope).isEqualTo(Unit::class) - val generatedFile = File(outputDirectory.parent, "build/anvil") - .walk() - .single { it.isFile && it.extension == "kt" } + val generatedFile = walkGeneratedFiles(mode).single() assertThat(generatedFile.name).isEqualTo("SubcomponentInterface.kt") } @@ -45,6 +59,7 @@ class ContributesSubcomponentGeneratorTest { @ContributesSubcomponent(Any::class, Unit::class) abstract class SubcomponentInterface """, + mode = mode, ) { assertThat(subcomponentInterface.hintSubcomponent?.java).isEqualTo(subcomponentInterface) assertThat(subcomponentInterface.hintSubcomponentParentScope).isEqualTo(Unit::class) @@ -61,6 +76,7 @@ class ContributesSubcomponentGeneratorTest { @ContributesSubcomponent(parentScope = Unit::class, scope = Any::class) interface SubcomponentInterface """, + mode = mode, ) { assertThat(subcomponentInterface.hintSubcomponent?.java).isEqualTo(subcomponentInterface) assertThat(subcomponentInterface.hintSubcomponentParentScope).isEqualTo(Unit::class) @@ -79,15 +95,14 @@ class ContributesSubcomponentGeneratorTest { interface SubcomponentInterface } """, + mode = mode, ) { val subcomponentInterface = classLoader .loadClass("com.squareup.test.Outer\$SubcomponentInterface") assertThat(subcomponentInterface.hintSubcomponent?.java).isEqualTo(subcomponentInterface) assertThat(subcomponentInterface.hintSubcomponentParentScope).isEqualTo(Unit::class) - val generatedFile = File(outputDirectory.parent, "build/anvil") - .walk() - .single { it.isFile && it.extension == "kt" } + val generatedFile = walkGeneratedFiles(mode).single() assertThat(generatedFile.name).isEqualTo("Outer_SubcomponentInterface.kt") } @@ -103,6 +118,7 @@ class ContributesSubcomponentGeneratorTest { @ContributesSubcomponent(Any::class, Unit::class) class SubcomponentInterface """, + mode = mode, ) { assertThat(exitCode).isError() // Position to the class. @@ -122,6 +138,7 @@ class ContributesSubcomponentGeneratorTest { @ContributesSubcomponent(Any::class, Unit::class) object SubcomponentInterface """, + mode = mode, ) { assertThat(exitCode).isError() // Position to the class. @@ -150,6 +167,7 @@ class ContributesSubcomponentGeneratorTest { @ContributesSubcomponent(Any::class, Unit::class) $visibility interface SubcomponentInterface """, + mode = mode, ) { assertThat(exitCode).isError() // Position to the class. @@ -179,6 +197,7 @@ class ContributesSubcomponentGeneratorTest { } } """, + mode = mode, ) { val parentComponent = subcomponentInterface.parentComponentInterface assertThat(parentComponent).isNotNull() @@ -209,9 +228,10 @@ class ContributesSubcomponentGeneratorTest { } } """, + mode = mode, ) { assertThat(exitCode).isError() - assertThat(messages).contains("Source0.kt:7:11") + assertThat(messages).contains("Source0.kt:7:") assertThat(messages).contains( "Expected zero or one parent component interface within " + "com.squareup.test.SubcomponentInterface being contributed to the parent scope.", @@ -237,9 +257,10 @@ class ContributesSubcomponentGeneratorTest { } } """, + mode = mode, ) { assertThat(exitCode).isError() - assertThat(messages).contains("Source0.kt:9:13") + assertThat(messages).contains("Source0.kt:9:") assertThat(messages).contains( "Expected zero or one function returning the subcomponent " + "com.squareup.test.SubcomponentInterface.", @@ -269,9 +290,10 @@ class ContributesSubcomponentGeneratorTest { } } """, + mode = mode, ) { assertThat(exitCode).isError() - assertThat(messages).contains("Source0.kt:8:11") + assertThat(messages).contains("Source0.kt:8:") assertThat(messages).contains( "Expected zero or one factory within com.squareup.test.SubcomponentInterface.", ) @@ -296,9 +318,10 @@ class ContributesSubcomponentGeneratorTest { } } """, + mode = mode, ) { assertThat(exitCode).isError() - assertThat(messages).contains("Source0.kt:10:3") + assertThat(messages).contains("Source0.kt:10:") assertThat(messages).contains("A factory must be an interface or an abstract class.") } @@ -318,9 +341,10 @@ class ContributesSubcomponentGeneratorTest { } } """, + mode = mode, ) { assertThat(exitCode).isError() - assertThat(messages).contains("Source0.kt:10:9") + assertThat(messages).contains("Source0.kt:10:") assertThat(messages).contains("A factory must be an interface or an abstract class.") } } @@ -341,9 +365,10 @@ class ContributesSubcomponentGeneratorTest { interface ComponentFactory } """, + mode = mode, ) { assertThat(exitCode).isError() - assertThat(messages).contains("Source0.kt:10:13") + assertThat(messages).contains("Source0.kt:10:") assertThat(messages).contains( "A factory must have exactly one abstract function returning the subcomponent " + "com.squareup.test.SubcomponentInterface.", @@ -370,9 +395,10 @@ class ContributesSubcomponentGeneratorTest { } } """, + mode = mode, ) { assertThat(exitCode).isError() - assertThat(messages).contains("Source0.kt:10:13") + assertThat(messages).contains("Source0.kt:10:") assertThat(messages).contains( "A factory must have exactly one abstract function returning the subcomponent " + "com.squareup.test.SubcomponentInterface.", @@ -398,9 +424,10 @@ class ContributesSubcomponentGeneratorTest { } } """, + mode = mode, ) { assertThat(exitCode).isError() - assertThat(messages).contains("Source0.kt:10:18") + assertThat(messages).contains("Source0.kt:10:") assertThat(messages).contains( "A factory must have exactly one abstract function returning the subcomponent " + "com.squareup.test.SubcomponentInterface.", @@ -425,9 +452,10 @@ class ContributesSubcomponentGeneratorTest { } } """, + mode = mode, ) { assertThat(exitCode).isError() - assertThat(messages).contains("Source0.kt:9:13") + assertThat(messages).contains("Source0.kt:9:") assertThat(messages).contains( "Within a class using @ContributesSubcomponent you must use " + "com.squareup.anvil.annotations.ContributesSubcomponent.Factory and not " + @@ -453,9 +481,10 @@ class ContributesSubcomponentGeneratorTest { } } """, + mode = mode, ) { assertThat(exitCode).isError() - assertThat(messages).contains("Source0.kt:9:13") + assertThat(messages).contains("Source0.kt:9:") assertThat(messages).contains( "Within a class using @ContributesSubcomponent you must use " + "com.squareup.anvil.annotations.ContributesSubcomponent.Factory and not " + @@ -484,6 +513,7 @@ class ContributesSubcomponentGeneratorTest { ) interface SubcomponentInterface2 """, + mode = mode, ) { assertThat(subcomponentInterface1.hintSubcomponent?.java).isEqualTo(subcomponentInterface1) assertThat(subcomponentInterface1.hintSubcomponentParentScope).isEqualTo(Unit::class) @@ -513,9 +543,10 @@ class ContributesSubcomponentGeneratorTest { ) interface SubcomponentInterface2 """, + mode = mode, ) { assertThat(exitCode).isError() - assertThat(messages).contains("Source0.kt:16:11") + assertThat(messages).contains("Source0.kt:16:") assertThat(messages).contains( "com.squareup.test.SubcomponentInterface2 with scope kotlin.Any wants to replace " + "com.squareup.test.SubcomponentInterface1 with scope kotlin.Long. The replacement " +