Skip to content

Commit

Permalink
Merge pull request #742 from square/joel.ksp-ContributesTo
Browse files Browse the repository at this point in the history
Implement KSP support for @ContributesTo
  • Loading branch information
JoelWilcox authored Aug 31, 2023
2 parents 0156121 + 27c147f commit a1be7c6
Show file tree
Hide file tree
Showing 6 changed files with 314 additions and 151 deletions.
7 changes: 4 additions & 3 deletions compiler/src/main/java/com/squareup/anvil/compiler/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,10 @@ internal const val SUBCOMPONENT_MODULE = "SubcomponentModule"
internal const val REFERENCE_SUFFIX = "_reference"
internal const val SCOPE_SUFFIX = "_scope"

internal fun FqName.isAnvilModule(): Boolean {
val name = asString()
return name.startsWith(MODULE_PACKAGE_PREFIX) && name.endsWith(ANVIL_MODULE_SUFFIX)
internal fun FqName.isAnvilModule(): Boolean = asString().isAnvilModule()

internal fun String.isAnvilModule(): Boolean {
return startsWith(MODULE_PACKAGE_PREFIX) && endsWith(ANVIL_MODULE_SUFFIX)
}

@Suppress("UNCHECKED_CAST")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
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.KSClassDeclaration
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.compiler.HINT_CONTRIBUTES_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.checkClassIsPublic
import com.squareup.anvil.compiler.codegen.ksp.checkNoDuplicateScope
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.scope
import com.squareup.anvil.compiler.contributesToFqName
import com.squareup.anvil.compiler.daggerModuleFqName
import com.squareup.anvil.compiler.internal.createAnvilSpec
import com.squareup.anvil.compiler.internal.reference.AnvilCompilationExceptionClassReference
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.anvil.compiler.mergeModulesFqName
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.Module
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 class in the `hint.anvil` packages. This allows the
* compiler plugin to find all contributed classes a lot faster when merging modules and component
* interfaces.
*/
internal object ContributesToCodeGen : AnvilApplicabilityChecker {

override fun isApplicable(context: AnvilContext) = !context.generateFactoriesOnly

fun generate(
className: ClassName,
scopes: List<ClassName>,
): FileSpec {
val fileName = className.generateClassName().simpleName
val generatedPackage = HINT_CONTRIBUTES_PACKAGE_PREFIX +
className.packageName.safePackageString(dotPrefix = true)
val classFqName = className.canonicalName
val propertyName = classFqName.replace('.', '_')

return FileSpec.createAnvilSpec(generatedPackage, fileName) {
addProperty(
PropertySpec
.builder(
name = propertyName + REFERENCE_SUFFIX,
type = KClass::class.asClassName().parameterizedBy(className)
)
.initializer("%T::class", className)
.addModifiers(PUBLIC)
.build()
)

scopes.forEachIndexed { index, scope ->
addProperty(
PropertySpec
.builder(
name = propertyName + SCOPE_SUFFIX + index,
type = KClass::class.asClassName().parameterizedBy(scope)
)
.initializer("%T::class", scope)
.addModifiers(PUBLIC)
.build()
)
}
}
}

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

override fun processChecked(resolver: Resolver): List<KSAnnotated> {
resolver.getSymbolsWithAnnotation(ContributesTo::class.qualifiedName!!)
.forEach { clazz ->
if (clazz !is KSClassDeclaration) {
env.logger.error(
"@${ContributesTo::class.simpleName} can only be applied to classes",
clazz,
)
return@forEach
}
if (!clazz.isInterface() &&
!clazz.isAnnotationPresent(daggerModuleFqName.toString()) &&
!clazz.isAnnotationPresent(mergeModulesFqName.toString())
) {
throw KspAnvilException(
message = "${clazz.qualifiedName!!.asString()} 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}?",
node = clazz,
)
}
clazz.checkClassIsPublic {
"${clazz.qualifiedName!!.asString()} is contributed to the Dagger graph, but the " +
"module is not public. Only public modules are supported."
}

val scopes = clazz.getKSAnnotationsByType(ContributesTo::class)
.toList()
.also { it.checkNoDuplicateScope(annotatedType = clazz, isContributeAnnotation = true) }
.map { it.scope().toClassName() }
.distinct()
// Give it a stable sort.
.sortedBy { it.canonicalName }

generate(clazz.toClassName(), scopes)
.writeTo(
codeGenerator = env.codeGenerator,
aggregating = false,
originatingKSFiles = listOf(clazz.containingFile!!),
)
}

return emptyList()
}

@AutoService(SymbolProcessorProvider::class)
class Provider : AnvilSymbolProcessorProvider(ContributesToCodeGen, ::KspGenerator)
}

@AutoService(CodeGenerator::class)
internal class EmbeddedGenerator : CodeGenerator {

override fun isApplicable(context: AnvilContext): Boolean {
return ContributesToCodeGen.isApplicable(context)
}

override fun generateCode(
codeGenDir: File,
module: ModuleDescriptor,
projectFiles: Collection<KtFile>
): Collection<GeneratedFile> {
return projectFiles
.classAndInnerClassReferences(module)
.filter { it.isAnnotatedWith(contributesToFqName) }
.onEach { clazz ->
if (!clazz.isInterface() &&
!clazz.isAnnotatedWith(daggerModuleFqName) &&
!clazz.isAnnotatedWith(mergeModulesFqName)
) {
throw AnvilCompilationExceptionClassReference(
classReference = clazz,
message = "${clazz.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}?",
)
}

if (clazz.visibility() != Visibility.PUBLIC) {
throw AnvilCompilationExceptionClassReference(
classReference = clazz,
message = "${clazz.fqName} is contributed to the Dagger graph, but the " +
"module is not public. Only public modules are supported.",
)
}
}
.map { clazz ->
val scopes = clazz.annotations
.find(contributesToFqName)
.also { it.checkNoDuplicateScope(contributeAnnotation = true) }
// Give it a stable sort.
.sortedBy { it.scope() }
.map { it.scope().asClassName() }

val spec = generate(clazz.asClassName(), scopes)

createGeneratedFile(
codeGenDir = codeGenDir,
packageName = spec.packageName,
fileName = spec.name,
content = spec.toString()
)
}
.toList()
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@file:Suppress("invisible_reference", "invisible_member")

package com.squareup.anvil.compiler.codegen.ksp

import com.google.devtools.ksp.isDefault
Expand All @@ -8,9 +9,45 @@ 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.isAnvilModule
import com.squareup.anvil.compiler.qualifierFqName
import com.squareup.kotlinpoet.ksp.toClassName
import org.jetbrains.kotlin.name.FqName

internal fun <T : KSAnnotation> List<T>.checkNoDuplicateScope(
annotatedType: KSClassDeclaration,
isContributeAnnotation: Boolean,
) {
// Exit early to avoid allocating additional collections.
if (size < 2) return
if (size == 2 && this[0].scope() != this[1].scope()) return

// Ignore generated Anvil modules. We'll throw a better error later for @Merge* annotations.
if (annotatedType.qualifiedName?.asString().orEmpty().isAnvilModule()) return

// Check for duplicate scopes. Multiple contributions to the same scope are forbidden.
val duplicates = groupBy { it.scope() }.filterValues { it.size > 1 }

if (duplicates.isNotEmpty()) {
val annotatedClass = annotatedType.qualifiedName!!.asString()
val duplicateScopesMessage =
duplicates.keys.joinToString(prefix = "[", postfix = "]") { it.toClassName().simpleName }

throw KspAnvilException(
message = if (isContributeAnnotation) {
"$annotatedClass contributes multiple times to the same scope: $duplicateScopesMessage. " +
"Contributing multiple times to the same scope is forbidden and all scopes must " +
"be distinct."
} else {
"$annotatedClass merges multiple times to the same scope: $duplicateScopesMessage. " +
"Merging multiple times to the same scope is forbidden and all scopes must " +
"be distinct."
},
node = annotatedType,
)
}
}

internal fun <T : KSAnnotation> List<T>.checkNoDuplicateScopeAndBoundType(
annotatedType: KSClassDeclaration
) {
Expand Down
Loading

0 comments on commit a1be7c6

Please sign in to comment.