Skip to content

Commit

Permalink
Implement KspMergeAnnotationsCheckSymbolProcessor (#10)
Browse files Browse the repository at this point in the history
Resolves #9
  • Loading branch information
ZacSweers authored Jul 6, 2024
1 parent ae23b09 commit 79ca744
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.squareup.anvil.annotations.MergeSubcomponent
import com.squareup.anvil.annotations.compat.MergeInterfaces
import com.squareup.anvil.annotations.compat.MergeModules
import com.squareup.anvil.compiler.api.ComponentMergingBackend
import com.squareup.anvil.compiler.codegen.KspMergeAnnotationsCheckSymbolProcessor
import com.squareup.anvil.compiler.codegen.generatedAnvilSubcomponentClassId
import com.squareup.anvil.compiler.codegen.ksp.AnvilSymbolProcessor
import com.squareup.anvil.compiler.codegen.ksp.AnvilSymbolProcessorProvider
Expand All @@ -42,6 +43,7 @@ import com.squareup.anvil.compiler.codegen.ksp.getSymbolsWithAnnotations
import com.squareup.anvil.compiler.codegen.ksp.includes
import com.squareup.anvil.compiler.codegen.ksp.isAnnotationPresent
import com.squareup.anvil.compiler.codegen.ksp.isInterface
import com.squareup.anvil.compiler.codegen.ksp.mergeAnnotations
import com.squareup.anvil.compiler.codegen.ksp.modules
import com.squareup.anvil.compiler.codegen.ksp.parentScope
import com.squareup.anvil.compiler.codegen.ksp.replaces
Expand Down Expand Up @@ -146,7 +148,8 @@ internal class KspContributionMerger(override val env: SymbolProcessorEnvironmen
mergeSubcomponentFqName,
mergeModulesFqName,
mergeInterfacesFqName,
).validate { deferred -> return deferred }
).filterIsInstance<KSClassDeclaration>()
.validate { deferred -> return deferred }
.also { mergeAnnotatedTypes ->
if (shouldDefer) {
return mergeAnnotatedTypes
Expand Down Expand Up @@ -787,18 +790,15 @@ internal class KspContributionMerger(override val env: SymbolProcessorEnvironmen
)
}

private inline fun Sequence<KSAnnotated>.validate(
escape: (List<KSAnnotated>) -> Nothing,
private inline fun Sequence<KSClassDeclaration>.validate(
escape: (List<KSClassDeclaration>) -> Nothing,
): List<KSClassDeclaration> {
val (valid, deferred) = filterIsInstance<KSClassDeclaration>().partition { annotated ->
val (valid, deferred) = partition { annotated ->
val superTypesHaveError = annotated.superTypes.any { it.resolve().isError }
if (superTypesHaveError) return@partition false
!annotated.findAll(
mergeComponentFqName.asString(),
mergeSubcomponentFqName.asString(),
mergeModulesFqName.asString(),
mergeInterfacesFqName.asString(),
).any { annotation ->

val mergeAnnotations = annotated.mergeAnnotations()
!mergeAnnotations.any { annotation ->
// If any of the parameters are unresolved, we need to defer this class
arrayOf("modules", "dependencies", "exclude", "includes").forEach { parameter ->
@Suppress("UNCHECKED_CAST")
Expand All @@ -808,6 +808,11 @@ internal class KspContributionMerger(override val env: SymbolProcessorEnvironmen
}
false
}

// Last step - run the validator manually here. It won't run on its own if component
// processing is enabled.
KspMergeAnnotationsCheckSymbolProcessor.validate(annotated, mergeAnnotations)
true
}
return if (deferred.isNotEmpty()) {
escape(deferred)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.squareup.anvil.compiler.codegen

import com.google.auto.service.AutoService
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.KSAnnotation
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.squareup.anvil.compiler.codegen.KspMergeAnnotationsCheckSymbolProcessor.Companion.checkNotAnnotatedWithDaggerAnnotation
import com.squareup.anvil.compiler.codegen.KspMergeAnnotationsCheckSymbolProcessor.Companion.checkSingleAnnotation
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.checkNoDuplicateScope
import com.squareup.anvil.compiler.codegen.ksp.declaringClass
import com.squareup.anvil.compiler.codegen.ksp.getSymbolsWithAnnotations
import com.squareup.anvil.compiler.codegen.ksp.isAnnotationPresent
import com.squareup.anvil.compiler.codegen.ksp.mergeAnnotations
import com.squareup.anvil.compiler.codegen.ksp.resolveKSClassDeclaration
import com.squareup.anvil.compiler.daggerComponentFqName
import com.squareup.anvil.compiler.daggerModuleFqName
import com.squareup.anvil.compiler.daggerSubcomponentFqName
import com.squareup.anvil.compiler.mergeComponentClassName
import com.squareup.anvil.compiler.mergeComponentFqName
import com.squareup.anvil.compiler.mergeInterfacesFqName
import com.squareup.anvil.compiler.mergeModulesClassName
import com.squareup.anvil.compiler.mergeModulesFqName
import com.squareup.anvil.compiler.mergeSubcomponentClassName
import com.squareup.anvil.compiler.mergeSubcomponentFqName
import com.squareup.kotlinpoet.ksp.toClassName
import org.jetbrains.kotlin.name.FqName

internal class KspMergeAnnotationsCheckSymbolProcessor(
override val env: SymbolProcessorEnvironment,
) : AnvilSymbolProcessor() {

@AutoService(SymbolProcessorProvider::class)
class Provider : AnvilSymbolProcessorProvider(
applicabilityChecker = { context -> !context.disableComponentMerging },
delegate = ::KspMergeAnnotationsCheckSymbolProcessor,
)

override fun processChecked(resolver: Resolver): List<KSAnnotated> {
resolver.getSymbolsWithAnnotations(
mergeComponentFqName,
mergeSubcomponentFqName,
mergeModulesFqName,
mergeInterfacesFqName,
)
.filterIsInstance<KSClassDeclaration>()
.forEach(::validate)

return emptyList()
}

companion object {
fun validate(
clazz: KSClassDeclaration,
mergeAnnotations: List<KSAnnotation> = clazz.mergeAnnotations(),
) {
mergeAnnotations.checkSingleAnnotation()
mergeAnnotations.checkNoDuplicateScope(
annotatedType = clazz,
isContributeAnnotation = false,
)

// Note that we only allow a single type of `@Merge*` annotation through the check above.
// The same class can't merge a component and subcomponent at the same time. Therefore,
// all annotations must have the same FqName and we can use the first annotation to check
// for the Dagger annotation.
mergeAnnotations
.firstOrNull {
it.annotationType.resolve().declaration.qualifiedName?.asString() != mergeInterfacesFqName.asString()
}
?.checkNotAnnotatedWithDaggerAnnotation()
}

private fun List<KSAnnotation>.checkSingleAnnotation() {
val distinctAnnotations = distinctBy { it.annotationType.resolve().declaration.qualifiedName }
if (distinctAnnotations.size > 1) {
throw KspAnvilException(
node = this[0].declaringClass,
message = "It's only allowed to have one single type of @Merge* annotation, " +
"however multiple instances of the same annotation are allowed. You mix " +
distinctAnnotations.joinToString(prefix = "[", postfix = "]") {
it.shortName.asString()
} +
" and this is forbidden.",
)
}
}

private fun KSAnnotation.checkNotAnnotatedWithDaggerAnnotation() {
if (declaringClass.isAnnotationPresent(daggerAnnotationFqName.asString())) {
throw KspAnvilException(
node = declaringClass,
message = "When using @$shortName it's not allowed to " +
"annotate the same class with @${daggerAnnotationFqName.shortName()}. " +
"The Dagger annotation will be generated.",
)
}
}

private val KSAnnotation.daggerAnnotationFqName: FqName
get() = when (annotationType.resolve().resolveKSClassDeclaration()?.toClassName()) {
mergeComponentClassName -> daggerComponentFqName
mergeSubcomponentClassName -> daggerSubcomponentFqName
mergeModulesClassName -> daggerModuleFqName
else -> throw NotImplementedError("Don't know how to handle $this.")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import com.google.devtools.ksp.symbol.KSTypeAlias
import com.google.devtools.ksp.symbol.KSValueParameter
import com.google.devtools.ksp.symbol.Modifier
import com.squareup.anvil.compiler.internal.reference.asClassId
import com.squareup.anvil.compiler.mergeComponentFqName
import com.squareup.anvil.compiler.mergeInterfacesFqName
import com.squareup.anvil.compiler.mergeModulesFqName
import com.squareup.anvil.compiler.mergeSubcomponentFqName
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.TypeName
Expand Down Expand Up @@ -193,9 +197,11 @@ internal fun KSFunctionDeclaration.returnTypeOrNull(): KSType? =

internal fun Resolver.getSymbolsWithAnnotations(
vararg annotations: FqName,
): Sequence<KSAnnotated> = annotations.asSequence().flatMap {
getSymbolsWithAnnotation(it.asString())
}
): Sequence<KSAnnotated> = annotations.asSequence()
.flatMap {
getSymbolsWithAnnotation(it.asString())
}
.distinct()

internal fun KSAnnotated.findAll(vararg annotations: String): List<KSAnnotation> {
return annotations.flatMap { annotation ->
Expand Down Expand Up @@ -250,3 +256,12 @@ internal fun KSValueParameter.toParameterSpec(): ParameterSpec {
.addAnnotations(annotations.map { it.toAnnotationSpec() }.asIterable())
.build()
}

internal fun KSAnnotated.mergeAnnotations(): List<KSAnnotation> {
return findAll(
mergeComponentFqName.asString(),
mergeSubcomponentFqName.asString(),
mergeModulesFqName.asString(),
mergeInterfacesFqName.asString(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.squareup.anvil.annotations.compat.MergeInterfaces
import com.squareup.anvil.compiler.api.ComponentMergingBackend
import com.squareup.anvil.compiler.internal.testing.extends
import com.tschuchort.compiletesting.KotlinCompilation.ExitCode
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
Expand All @@ -33,9 +32,6 @@ class InterfaceMergerRepeatableTest(
}

@Test fun `duplicate scopes are an error`() {
// TODO enable KSP for this once there's a KSP impl of MergeAnnotationsCheckGenerator
assumeTrue(backend == ComponentMergingBackend.IR)

compile(
"""
package com.squareup.test
Expand All @@ -57,8 +53,6 @@ class InterfaceMergerRepeatableTest(
}

@Test fun `different kind of merge annotations are forbidden`() {
// TODO enable KSP for this once there's a KSP impl of MergeAnnotationsCheckGenerator
assumeTrue(backend == ComponentMergingBackend.IR)
assumeMergeComponent(annotationClass)

compile(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import com.squareup.anvil.compiler.internal.testing.extends
import com.squareup.anvil.compiler.internal.testing.resolveIfMerged
import com.tschuchort.compiletesting.KotlinCompilation.ExitCode
import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
Expand Down Expand Up @@ -217,8 +216,6 @@ class InterfaceMergerTest(
}

@Test fun `replaced interfaces must use the same scope`() {
// TODO enable KSP for this once there's a KSP impl of MergeAnnotationsCheckGenerator
assumeTrue(backend == ComponentMergingBackend.IR)
compile(
"""
package com.squareup.test
Expand Down Expand Up @@ -731,8 +728,6 @@ class InterfaceMergerTest(

@Test
fun `replaced interfaces contributed to multiple scopes must use the same scope`() {
// TODO enable KSP for this once there's a KSP impl of MergeAnnotationsCheckGenerator
assumeTrue(backend == ComponentMergingBackend.IR)
compile(
"""
package com.squareup.test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import com.squareup.anvil.compiler.internal.testing.daggerModule
import com.squareup.anvil.compiler.internal.testing.resolveIfMerged
import com.squareup.anvil.compiler.internal.testing.withoutAnvilModules
import com.tschuchort.compiletesting.KotlinCompilation.ExitCode
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
Expand Down Expand Up @@ -97,9 +96,6 @@ class MergeModulesTest(
}

@Test fun `it's not allowed to have @Module and @MergeModules annotation at the same time`() {
// TODO enable KSP for this once there's a KSP impl of MergeAnnotationsCheckGenerator
assumeTrue(backend == ComponentMergingBackend.IR)

compile(
"""
package com.squareup.test
Expand Down Expand Up @@ -942,9 +938,6 @@ class MergeModulesTest(
}

@Test fun `a module is not allowed to be included and excluded`() {
// TODO enable KSP for this once there's a KSP impl of MergeAnnotationsCheckGenerator
assumeTrue(backend == ComponentMergingBackend.IR)

compile(
"""
package com.squareup.test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import com.squareup.anvil.compiler.internal.testing.anyDaggerComponent
import com.squareup.anvil.compiler.internal.testing.daggerComponent
import com.squareup.anvil.compiler.internal.testing.withoutAnvilModules
import com.tschuchort.compiletesting.KotlinCompilation.ExitCode
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
Expand All @@ -35,8 +34,6 @@ class ModuleMergerRepeatableTest(
}

@Test fun `duplicate scopes are an error`() {
// TODO enable KSP for this once there's a KSP impl of MergeAnnotationsCheckGenerator
assumeTrue(backend == ComponentMergingBackend.IR)
compile(
"""
package com.squareup.test
Expand All @@ -58,8 +55,6 @@ class ModuleMergerRepeatableTest(
}

@Test fun `different kind of merge annotations are forbidden`() {
// TODO enable KSP for this once there's a KSP impl of MergeAnnotationsCheckGenerator
assumeTrue(backend == ComponentMergingBackend.IR)
assumeMergeComponent(annotationClass)

compile(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import com.tschuchort.compiletesting.KotlinCompilation.ExitCode
import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK
import dagger.Component
import dagger.Subcomponent
import org.junit.Assume.assumeTrue
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
Expand Down Expand Up @@ -118,9 +117,6 @@ class ModuleMergerTest(

@Test
fun `it's not allowed to have @Component and @MergeComponent annotation at the same time`() {
// TODO enable KSP for this once there's a KSP impl of MergeAnnotationsCheckGenerator
assumeTrue(backend == ComponentMergingBackend.IR)

val daggerComponentClass = when (annotationClass) {
MergeComponent::class -> Component::class
MergeSubcomponent::class -> Subcomponent::class
Expand Down

0 comments on commit 79ca744

Please sign in to comment.