From 0cea83e52ed74213df78c7fe41d973ef95397ca3 Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Tue, 14 Nov 2023 17:39:38 -0500 Subject: [PATCH] Implement KSP support for ContributesMultibinding (#740) * Also add simple symbol processor support * Conditionally run KSP tests to work-around Github-Windows-CI specific errors --------- Co-authored-by: Joel Wilcox --- .github/workflows/ci.yml | 2 +- .../compiler/api/AnvilApplicabilityChecker.kt | 9 + .../internal/testing/SimpleCodeGenerator.kt | 72 ++++--- compiler/build.gradle | 1 + .../codegen/ContributesMultibindingCodeGen.kt | 187 ++++++++++++++++ .../ContributesMultibindingGenerator.kt | 107 ---------- .../codegen/ksp/AnvilSymbolProcessing.kt | 2 +- .../com/squareup/anvil/compiler/TestUtils.kt | 6 +- .../ContributesMultibindingGeneratorTest.kt | 199 ++++++++++++------ .../codegen/ksp/SimpleSymbolProcessor.kt | 40 ++++ gradle/libs.versions.toml | 4 + 11 files changed, 426 insertions(+), 203 deletions(-) create mode 100644 compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesMultibindingCodeGen.kt delete mode 100644 compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesMultibindingGenerator.kt create mode 100644 compiler/src/test/java/com/squareup/anvil/compiler/codegen/ksp/SimpleSymbolProcessor.kt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba4fd9b2b..b6e23a93b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/compiler-api/src/main/java/com/squareup/anvil/compiler/api/AnvilApplicabilityChecker.kt b/compiler-api/src/main/java/com/squareup/anvil/compiler/api/AnvilApplicabilityChecker.kt index 924aa9301..9ce579086 100644 --- a/compiler-api/src/main/java/com/squareup/anvil/compiler/api/AnvilApplicabilityChecker.kt +++ b/compiler-api/src/main/java/com/squareup/anvil/compiler/api/AnvilApplicabilityChecker.kt @@ -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 + } } diff --git a/compiler-utils/src/testFixtures/java/com/squareup/anvil/compiler/internal/testing/SimpleCodeGenerator.kt b/compiler-utils/src/testFixtures/java/com/squareup/anvil/compiler/internal/testing/SimpleCodeGenerator.kt index c719c0427..6473710e7 100644 --- a/compiler-utils/src/testFixtures/java/com/squareup/anvil/compiler/internal/testing/SimpleCodeGenerator.kt +++ b/compiler-utils/src/testFixtures/java/com/squareup/anvil/compiler/internal/testing/SimpleCodeGenerator.kt @@ -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, @@ -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, + ) +} diff --git a/compiler/build.gradle b/compiler/build.gradle index f61455962..77dea7886 100644 --- a/compiler/build.gradle +++ b/compiler/build.gradle @@ -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 { diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesMultibindingCodeGen.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesMultibindingCodeGen.kt new file mode 100644 index 000000000..df90a7dc1 --- /dev/null +++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesMultibindingCodeGen.kt @@ -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 + ): 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 { + 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 + ): Collection { + 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() + } + } +} diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesMultibindingGenerator.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesMultibindingGenerator.kt deleted file mode 100644 index 1496b3ea8..000000000 --- a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributesMultibindingGenerator.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.squareup.anvil.compiler.codegen - -import com.google.auto.service.AutoService -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.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.contributesMultibindingFqName -import com.squareup.anvil.compiler.internal.buildFile -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 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. - */ -@AutoService(CodeGenerator::class) -internal class ContributesMultibindingGenerator : 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(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 fileName = clazz.generateClassName().relativeClassName.asString() - val generatedPackage = HINT_MULTIBINDING_PACKAGE_PREFIX + - clazz.packageFqName.safePackageString(dotPrefix = true) - val className = clazz.asClassName() - val classFqName = clazz.fqName.toString() - val propertyName = classFqName.replace('.', '_') - - 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 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() - ) - - 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() - ) - } - } - - createGeneratedFile( - codeGenDir = codeGenDir, - packageName = generatedPackage, - fileName = fileName, - content = content - ) - } - .toList() - } -} diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ksp/AnvilSymbolProcessing.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ksp/AnvilSymbolProcessing.kt index fb2c07dd2..1201681fe 100644 --- a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ksp/AnvilSymbolProcessing.kt +++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ksp/AnvilSymbolProcessing.kt @@ -12,7 +12,7 @@ private object NoOpProcessor : SymbolProcessor { override fun process(resolver: Resolver): List = emptyList() } -internal abstract class AnvilSymbolProcessorProvider( +internal open class AnvilSymbolProcessorProvider( private val applicabilityChecker: AnvilApplicabilityChecker, private val delegate: (SymbolProcessorEnvironment) -> AnvilSymbolProcessor, ) : SymbolProcessorProvider { 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 2292f9971..36aebeedc 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/TestUtils.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/TestUtils.kt @@ -177,11 +177,13 @@ internal fun ComparableSubject.isError() { internal fun isFullTestRun(): Boolean = FULL_TEST_RUN internal fun checkFullTestRun() = assumeTrue(isFullTestRun()) +internal fun includeKspTests(): Boolean = INCLUDE_KSP_TESTS internal fun JvmCompilationResult.walkGeneratedFiles(mode: AnvilCompilationMode): Sequence { val dirToSearch = when (mode) { - is AnvilCompilationMode.Embedded -> outputDirectory.parentFile.resolve("build/anvil") - is AnvilCompilationMode.Ksp -> outputDirectory.parentFile.resolve("ksp/sources") + is AnvilCompilationMode.Embedded -> + outputDirectory.parentFile.resolve("build${File.separator}anvil") + is AnvilCompilationMode.Ksp -> outputDirectory.parentFile.resolve("ksp${File.separator}sources") } return dirToSearch.walkTopDown() .filter { it.isFile && it.extension == "kt" } diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesMultibindingGeneratorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesMultibindingGeneratorTest.kt index 97b6e75d1..6a3969047 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesMultibindingGeneratorTest.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ContributesMultibindingGeneratorTest.kt @@ -1,21 +1,42 @@ package com.squareup.anvil.compiler.codegen import com.google.common.truth.Truth.assertThat +import com.squareup.anvil.annotations.MergeComponent +import com.squareup.anvil.compiler.codegen.ksp.simpleSymbolProcessor import com.squareup.anvil.compiler.compile import com.squareup.anvil.compiler.contributingInterface import com.squareup.anvil.compiler.hintMultibinding import com.squareup.anvil.compiler.hintMultibindingScope import com.squareup.anvil.compiler.hintMultibindingScopes +import com.squareup.anvil.compiler.includeKspTests +import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode import com.squareup.anvil.compiler.internal.testing.simpleCodeGenerator import com.squareup.anvil.compiler.isError import com.squareup.anvil.compiler.mergeComponentFqName import com.squareup.anvil.compiler.secondContributingInterface +import com.squareup.anvil.compiler.walkGeneratedFiles import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK import org.junit.Test -import java.io.File +import org.junit.runner.RunWith +import org.junit.runners.Parameterized @Suppress("RemoveRedundantQualifierName") -class ContributesMultibindingGeneratorTest { +@RunWith(Parameterized::class) +class ContributesMultibindingGeneratorTest( + private val mode: AnvilCompilationMode +) { + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic fun modes(): Collection { + return buildList { + add(AnvilCompilationMode.Embedded()) + if (includeKspTests()) { + add(AnvilCompilationMode.Ksp()) + } + } + } + } @Test fun `there is a hint for a contributed multibinding for interfaces`() { compile( @@ -28,14 +49,14 @@ class ContributesMultibindingGeneratorTest { @ContributesMultibinding(Any::class, ParentInterface::class) interface ContributingInterface : ParentInterface - """ + """, + mode = mode, ) { assertThat(contributingInterface.hintMultibinding?.java).isEqualTo(contributingInterface) assertThat(contributingInterface.hintMultibindingScope).isEqualTo(Any::class) - val generatedFile = File(outputDirectory.parent, "build/anvil") - .walk() - .single { it.isFile && it.extension == "kt" } + val generatedFile = walkGeneratedFiles(mode) + .single() assertThat(generatedFile.name).isEqualTo("ContributingInterface.kt") } @@ -52,7 +73,8 @@ class ContributesMultibindingGeneratorTest { @ContributesMultibinding(Any::class, ParentInterface::class) class ContributingInterface : ParentInterface - """ + """, + mode = mode, ) { assertThat(contributingInterface.hintMultibinding?.java).isEqualTo(contributingInterface) assertThat(contributingInterface.hintMultibindingScope).isEqualTo(Any::class) @@ -70,7 +92,8 @@ class ContributesMultibindingGeneratorTest { @ContributesMultibinding(Any::class, ParentInterface::class) object ContributingInterface : ParentInterface - """ + """, + mode = mode, ) { assertThat(contributingInterface.hintMultibinding?.java).isEqualTo(contributingInterface) assertThat(contributingInterface.hintMultibindingScope).isEqualTo(Any::class) @@ -88,7 +111,8 @@ class ContributesMultibindingGeneratorTest { @ContributesMultibinding(boundType = ParentInterface::class, scope = Int::class) class ContributingInterface : ParentInterface - """ + """, + mode = mode, ) { assertThat(contributingInterface.hintMultibindingScope).isEqualTo(Int::class) } @@ -107,7 +131,8 @@ class ContributesMultibindingGeneratorTest { @ContributesMultibinding(Any::class, ParentInterface::class) interface ContributingInterface : ParentInterface } - """ + """, + mode = mode, ) { val contributingInterface = classLoader.loadClass("com.squareup.test.Abc\$ContributingInterface") @@ -129,16 +154,16 @@ class ContributesMultibindingGeneratorTest { @ContributesMultibinding(Any::class, ParentInterface::class) class ContributingClass : ParentInterface } - """ + """, + mode = mode, ) { val contributingClass = classLoader.loadClass("com.squareup.test.Abc\$ContributingClass") assertThat(contributingClass.hintMultibinding?.java).isEqualTo(contributingClass) assertThat(contributingClass.hintMultibindingScope).isEqualTo(Any::class) - val generatedFile = File(outputDirectory.parent, "build/anvil") - .walk() - .single { it.isFile && it.extension == "kt" } + val generatedFile = walkGeneratedFiles(mode) + .single() assertThat(generatedFile.name).isEqualTo("Abc_ContributingClass.kt") } @@ -195,7 +220,8 @@ class ContributesMultibindingGeneratorTest { @AnyQualifier1 @AnyQualifier2 interface ContributingInterface : ParentInterface - """ + """, + mode = mode, ) { assertThat(exitCode).isError() assertThat(messages).contains( @@ -215,7 +241,8 @@ class ContributesMultibindingGeneratorTest { @ContributesMultibinding(Any::class) interface ContributingInterface : ParentInterface, CharSequence - """ + """, + mode = mode, ) { assertThat(exitCode).isError() assertThat(messages).contains( @@ -240,7 +267,8 @@ class ContributesMultibindingGeneratorTest { @ContributesMultibinding(Any::class) interface ContributingInterface : Abc(), ParentInterface - """ + """, + mode = mode, ) { assertThat(exitCode).isError() assertThat(messages).contains( @@ -261,7 +289,8 @@ class ContributesMultibindingGeneratorTest { @ContributesMultibinding(Any::class) object ContributingInterface - """ + """, + mode = mode, ) { assertThat(exitCode).isError() assertThat(messages).contains( @@ -288,7 +317,8 @@ class ContributesMultibindingGeneratorTest { boundType = ParentInterface::class ) interface ContributingInterface : ParentInterface, CharSequence - """ + """, + mode = mode, ) { assertThat(contributingInterface.hintMultibinding?.java).isEqualTo(contributingInterface) assertThat(contributingInterface.hintMultibindingScope).isEqualTo(Int::class) @@ -306,7 +336,8 @@ class ContributesMultibindingGeneratorTest { @ContributesMultibinding(Any::class, ParentInterface::class) interface ContributingInterface : CharSequence - """ + """, + mode = mode, ) { assertThat(exitCode).isError() assertThat(messages).contains( @@ -325,7 +356,8 @@ class ContributesMultibindingGeneratorTest { @ContributesMultibinding(Int::class, boundType = Any::class) interface ContributingInterface - """ + """, + mode = mode, ) { assertThat(contributingInterface.hintMultibinding?.java).isEqualTo(contributingInterface) assertThat(contributingInterface.hintMultibindingScope).isEqualTo(Int::class) @@ -333,24 +365,38 @@ class ContributesMultibindingGeneratorTest { } @Test fun `a contributed multibinding can be generated`() { - val codeGenerator = simpleCodeGenerator { clazz -> - clazz - .takeIf { it.isAnnotatedWith(mergeComponentFqName) } - ?.let { - //language=kotlin - """ - package com.squareup.test - - import com.squareup.anvil.annotations.ContributesMultibinding - import dagger.MapKey - import javax.inject.Singleton - - @ContributesMultibinding(Any::class) - @BindingKey("abc") - @Singleton - interface ContributingInterface : ParentInterface - """.trimIndent() + val stubContentToGenerate = + //language=kotlin + """ + package com.squareup.test + + import com.squareup.anvil.annotations.ContributesMultibinding + import dagger.MapKey + import javax.inject.Singleton + + @ContributesMultibinding(Any::class) + @BindingKey("abc") + @Singleton + interface ContributingInterface : ParentInterface + """.trimIndent() + + val localMode = when (mode) { + is AnvilCompilationMode.Embedded -> { + val codeGenerator = simpleCodeGenerator { clazz -> + clazz + .takeIf { it.isAnnotatedWith(mergeComponentFqName) } + ?.let { stubContentToGenerate } + } + AnvilCompilationMode.Embedded(listOf(codeGenerator)) + } + is AnvilCompilationMode.Ksp -> { + val processor = simpleSymbolProcessor { resolver -> + resolver.getSymbolsWithAnnotation(MergeComponent::class.qualifiedName!!) + .map { stubContentToGenerate } + .toList() } + AnvilCompilationMode.Ksp(listOf(processor)) + } } compile( @@ -368,7 +414,7 @@ class ContributesMultibindingGeneratorTest { @MergeComponent(Any::class) interface ComponentInterface """, - codeGenerators = listOf(codeGenerator) + mode = localMode ) { assertThat(exitCode).isEqualTo(OK) assertThat(contributingInterface.hintMultibindingScope).isEqualTo(Any::class) @@ -376,29 +422,45 @@ class ContributesMultibindingGeneratorTest { } @Test fun `a contributed multibinding can be generated with map keys being generated`() { - val codeGenerator = simpleCodeGenerator { clazz -> - clazz - .takeIf { it.isAnnotatedWith(mergeComponentFqName) } - ?.let { - //language=kotlin - """ - package com.squareup.test - - import com.squareup.anvil.annotations.ContributesMultibinding - import dagger.MapKey - import javax.inject.Singleton - - interface ParentInterface - - @MapKey - annotation class BindingKey1(val value: String) + val stubContentToGenerate = + //language=kotlin + """ + package com.squareup.test + + import com.squareup.anvil.annotations.ContributesMultibinding + import dagger.MapKey + import javax.inject.Singleton + + interface ParentInterface + + @MapKey + annotation class BindingKey1(val value: String) - @ContributesMultibinding(Any::class) - @BindingKey1("abc") - @Singleton - interface ContributingInterface : ParentInterface - """.trimIndent() + @ContributesMultibinding(Any::class) + @BindingKey1("abc") + @Singleton + interface ContributingInterface : ParentInterface + """.trimIndent() + + val localMode = when (mode) { + is AnvilCompilationMode.Embedded -> { + val codeGenerator = simpleCodeGenerator { clazz -> + clazz + .takeIf { it.isAnnotatedWith(mergeComponentFqName) } + ?.let { + stubContentToGenerate + } + } + AnvilCompilationMode.Embedded(listOf(codeGenerator)) + } + is AnvilCompilationMode.Ksp -> { + val processor = simpleSymbolProcessor { resolver -> + resolver.getSymbolsWithAnnotation(MergeComponent::class.qualifiedName!!) + .map { stubContentToGenerate } + .toList() } + AnvilCompilationMode.Ksp(listOf(processor)) + } } compile( @@ -410,7 +472,7 @@ class ContributesMultibindingGeneratorTest { @MergeComponent(Any::class) interface ComponentInterface """, - codeGenerators = listOf(codeGenerator) + mode = localMode ) { assertThat(exitCode).isEqualTo(OK) assertThat(contributingInterface.hintMultibindingScope).isEqualTo(Any::class) @@ -429,7 +491,8 @@ class ContributesMultibindingGeneratorTest { @ContributesMultibinding(Any::class) @ContributesMultibinding(Unit::class) class ContributingInterface : ParentInterface - """ + """, + mode = mode, ) { assertThat(contributingInterface.hintMultibinding?.java).isEqualTo(contributingInterface) assertThat(contributingInterface.hintMultibindingScopes) @@ -453,7 +516,8 @@ class ContributesMultibindingGeneratorTest { @ContributesMultibinding(Unit::class) @ContributesMultibinding(Any::class) class SecondContributingInterface : ParentInterface - """ + """, + mode = mode, ) { assertThat(contributingInterface.hintMultibindingScopes) .containsExactly(Any::class, Unit::class) @@ -477,7 +541,8 @@ class ContributesMultibindingGeneratorTest { @ContributesMultibinding(Any::class) @com.squareup.anvil.annotations.ContributesMultibinding(Unit::class) class ContributingInterface : ParentInterface - """ + """, + mode = mode, ) { assertThat(contributingInterface.hintMultibinding?.java).isEqualTo(contributingInterface) assertThat(contributingInterface.hintMultibindingScopes) @@ -499,7 +564,8 @@ class ContributesMultibindingGeneratorTest { @ContributesMultibinding(Unit::class) @ContributesMultibinding(Unit::class, replaces = [Int::class]) class ContributingInterface : ParentInterface - """ + """, + mode = mode, ) { assertThat(exitCode).isError() assertThat(messages).contains( @@ -526,7 +592,8 @@ class ContributesMultibindingGeneratorTest { @ContributesMultibinding(Unit::class, boundType = ParentInterface1::class) @ContributesMultibinding(Unit::class, boundType = ParentInterface2::class) class ContributingInterface : ParentInterface1, ParentInterface2 - """ + """, + mode = mode, ) { assertThat(contributingInterface.hintMultibinding?.java).isEqualTo(contributingInterface) assertThat(contributingInterface.hintMultibindingScopes) diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ksp/SimpleSymbolProcessor.kt b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ksp/SimpleSymbolProcessor.kt new file mode 100644 index 000000000..4d1fa94fe --- /dev/null +++ b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/ksp/SimpleSymbolProcessor.kt @@ -0,0 +1,40 @@ +package com.squareup.anvil.compiler.codegen.ksp + +import com.google.devtools.ksp.processing.Dependencies +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.squareup.anvil.compiler.api.AnvilApplicabilityChecker +import com.squareup.anvil.compiler.internal.testing.parseSimpleFileContents +import java.io.OutputStreamWriter +import java.nio.charset.StandardCharsets + +internal fun simpleSymbolProcessor( + mapper: AnvilSymbolProcessor.(resolver: Resolver) -> List +): SymbolProcessorProvider = AnvilSymbolProcessorProvider( + AnvilApplicabilityChecker.always() +) { env -> SimpleSymbolProcessor(env, mapper) } + +private class SimpleSymbolProcessor( + override val env: SymbolProcessorEnvironment, + private val mapper: AnvilSymbolProcessor.(resolver: Resolver) -> List +) : AnvilSymbolProcessor() { + override fun processChecked(resolver: Resolver): List { + this.mapper(resolver) + .map { content -> + val (packageName, fileName) = parseSimpleFileContents(content) + + val dependencies = Dependencies(aggregating = false, sources = emptyArray()) + val file = env.codeGenerator.createNewFile(dependencies, packageName, "$fileName.kt") + // Don't use writeTo(file) because that tries to handle directories under the hood + OutputStreamWriter(file, StandardCharsets.UTF_8) + .buffered() + .use { writer -> + writer.write(content) + } + } + .toList() + return emptyList() + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c8a6ce0bb..b21b66d04 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -38,6 +38,10 @@ mavenPublish = "0.25.3" # with one annotation instead of all options. We also skip tests that run the Dagger annotation # processor (KAPT is slow). config-fullTestRun = "true" +# This is exclusively used to work-around an issue with certain KSP tests on Windows on CI. +# We've run the same tests without issue on local Windows machines, so this just allows us to +# unblock KSP work for the moment until the source of the Git-Windows-CI jank can be identified. +config-includeKspTests = "true" config-generateDaggerFactoriesWithAnvil = "true" [plugins]