Skip to content

Commit

Permalink
Implement KSP support for ContributesMultibinding (#740)
Browse files Browse the repository at this point in the history
* Also add simple symbol processor support

* Conditionally run KSP tests to work-around Github-Windows-CI specific errors

---------

Co-authored-by: Joel Wilcox <jwilcox@squareup.com>
  • Loading branch information
ZacSweers and JoelWilcox committed Nov 14, 2023
1 parent 1947fe1 commit 0cea83e
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 203 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs :
# Expressions in Github actions are limited. If there would be an if expression, then we
# wouldn't need to duplicate the next step and depending on the OS enable / disable them.
- name : Test on Windows
run : ./gradlew.bat assemble test --no-build-cache --no-daemon --stacktrace -Doverride_config-fullTestRun=false
run : ./gradlew.bat assemble test --no-build-cache --no-daemon --stacktrace -Doverride_config-fullTestRun=false -Doverride_config-includeKspTests=false

- name : Upload Test Results
uses : actions/upload-artifact@v3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,13 @@ public interface AnvilApplicabilityChecker {
* will only be called _once_.
*/
public fun isApplicable(context: AnvilContext): Boolean

public companion object {
/** Returns an instance that always returns true. */
public fun always(): AnvilApplicabilityChecker = Always
}

private object Always : AnvilApplicabilityChecker {
override fun isApplicable(context: AnvilContext): Boolean = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,32 +28,7 @@ fun simpleCodeGenerator(
mapper.invoke(this, it)
}
.map { content ->
val packageName = content.lines()
.map { it.trim() }
.firstNotNullOfOrNull { line ->
line
.takeIf { it.startsWith("package ") }
?.substringAfter("package ")
}
?: "tempPackage"

val fileName = content.lines()
.map { it.trim() }
.firstNotNullOfOrNull { line ->
// Try finding the class name.
line
.takeIf { it.startsWith("class ") || it.contains(" class ") }
?.substringAfter("class ")
?.trim()
?.substringBefore(" ")
?: line
// Check for interfaces, too.
.takeIf { it.startsWith("interface ") || it.contains(" interface ") }
?.substringAfter("interface ")
?.trim()
?.substringBefore(" ")
}
?: "NewFile${counter++}"
val (packageName, fileName) = parseSimpleFileContents(content)

createGeneratedFile(
codeGenDir = codeGenDir,
Expand All @@ -65,3 +40,48 @@ fun simpleCodeGenerator(
.toList()
}
}

/**
* Represents the metadata of a file that is generated by [simpleCodeGenerator].
*/
data class SimpleFileContents(
val packageName: String,
val fileName: String,
)

/**
* Parses [SimpleFileContents] metadata from a given source [content].
*/
fun parseSimpleFileContents(content: String): SimpleFileContents {
val packageName = content.lines()
.map { it.trim() }
.firstNotNullOfOrNull { line ->
line
.takeIf { it.startsWith("package ") }
?.substringAfter("package ")
}
?: "tempPackage"

val fileName = content.lines()
.map { it.trim() }
.firstNotNullOfOrNull { line ->
// Try finding the class name.
line
.takeIf { it.startsWith("class ") || it.contains(" class ") }
?.substringAfter("class ")
?.trim()
?.substringBefore(" ")
?: line
// Check for interfaces, too.
.takeIf { it.startsWith("interface ") || it.contains(" interface ") }
?.substringAfter("interface ")
?.trim()
?.substringBefore(" ")
}
?: "NewFile${counter++}"

return SimpleFileContents(
packageName = packageName,
fileName = fileName,
)
}
1 change: 1 addition & 0 deletions compiler/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ buildConfig {

buildConfigField('boolean', 'WARNINGS_AS_ERRORS',"${rootProject.ext.warningsAsErrors}")
buildConfigField('boolean', 'FULL_TEST_RUN', "${libs.versions.config.fullTestRun.get()}")
buildConfigField('boolean', 'INCLUDE_KSP_TESTS', "${libs.versions.config.includeKspTests.get()}")
}

publish {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
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.ContributesMultibinding
import com.squareup.anvil.compiler.HINT_MULTIBINDING_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.checkClassExtendsBoundType
import com.squareup.anvil.compiler.codegen.ksp.checkClassIsPublic
import com.squareup.anvil.compiler.codegen.ksp.checkNoDuplicateScopeAndBoundType
import com.squareup.anvil.compiler.codegen.ksp.checkNotMoreThanOneMapKey
import com.squareup.anvil.compiler.codegen.ksp.checkNotMoreThanOneQualifier
import com.squareup.anvil.compiler.codegen.ksp.checkSingleSuperType
import com.squareup.anvil.compiler.codegen.ksp.getKSAnnotationsByType
import com.squareup.anvil.compiler.codegen.ksp.scope
import com.squareup.anvil.compiler.contributesMultibindingFqName
import com.squareup.anvil.compiler.internal.createAnvilSpec
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 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 `anvil.hint.multibinding` package. This
* allows the compiler plugin to find all contributed multibindings a lot faster when merging
* modules and component interfaces.
*/
internal object ContributesMultibindingCodeGen : AnvilApplicabilityChecker {

fun generate(
className: ClassName,
scopes: List<ClassName>
): FileSpec {
val fileName = className.generateClassName().simpleName
val generatedPackage = HINT_MULTIBINDING_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()
)
}
}
}

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

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

override fun processChecked(resolver: Resolver): List<KSAnnotated> {
resolver.getSymbolsWithAnnotation(ContributesMultibinding::class.java.canonicalName)
.forEach { clazz ->
if (clazz !is KSClassDeclaration) {
env.logger.error(
"@${ContributesMultibinding::class.simpleName} can only be applied to classes",
clazz
)
return@forEach
}
clazz.checkClassIsPublic {
"${clazz.qualifiedName!!.asString()} is binding a type, but the class is not public. " +
"Only public types are supported."
}
clazz.checkNotMoreThanOneQualifier(contributesMultibindingFqName)
clazz.checkNotMoreThanOneMapKey()
clazz.checkSingleSuperType(contributesMultibindingFqName, resolver)
clazz.checkClassExtendsBoundType(contributesMultibindingFqName, resolver)

// All good, generate away
val className = clazz.toClassName()
val scopes = clazz.getKSAnnotationsByType(ContributesMultibinding::class)
.toList()
.also { it.checkNoDuplicateScopeAndBoundType(clazz) }
.map { it.scope().toClassName() }
.distinct()
// Give it a stable sort.
.sortedBy { it.canonicalName }

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

return emptyList()
}

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

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

override fun isApplicable(context: AnvilContext): Boolean =
ContributesMultibindingCodeGen.isApplicable(context)

override fun generateCode(
codeGenDir: File,
module: ModuleDescriptor,
projectFiles: Collection<KtFile>
): Collection<GeneratedFile> {
return projectFiles
.classAndInnerClassReferences(module)
.filter { it.isAnnotatedWith(contributesMultibindingFqName) }
.onEach { clazz ->
clazz.checkClassIsPublic {
"${clazz.fqName} is binding a type, but the class is not public. " +
"Only public types are supported."
}
clazz.checkNotMoreThanOneQualifier(contributesMultibindingFqName)
clazz.checkNotMoreThanOneMapKey()
clazz.checkSingleSuperType(contributesMultibindingFqName)
clazz.checkClassExtendsBoundType(contributesMultibindingFqName)
}
.map { clazz ->
val className = clazz.asClassName()
val scopes = clazz.annotations
.find(contributesMultibindingFqName)
.also { it.checkNoDuplicateScopeAndBoundType() }
.distinctBy { it.scope() }
// Give it a stable sort.
.sortedBy { it.scope() }
.map { it.scope().asClassName() }

val spec = generate(className, scopes)

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

0 comments on commit 0cea83e

Please sign in to comment.