diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5994952..81d69ca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ kct = "0.5.0-alpha07" kotlin = "2.0.0" jdk = "21" jvmTarget = "11" +kotlinpoet = "1.16.0" ksp = "2.0.0-1.0.21" ktfmt = "0.49" @@ -24,7 +25,8 @@ guava = "com.google.guava:guava:33.2.0-jre" kotlin-compilerEmbeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } -kotlinpoet = "com.squareup:kotlinpoet:1.16.0" +kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } +kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet" } ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } @@ -35,4 +37,4 @@ ktfmt = { module = "com.facebook:ktfmt", version.ref = "ktfmt" } junit = { module = "junit:junit", version = "4.13.2" } truth = { module = "com.google.truth:truth", version = "1.4.2" } kct-core = { module = "dev.zacsweers.kctfork:core", version.ref = "kct" } -kct-ksp = { module = "dev.zacsweers.kctfork:ksp", version.ref ="kct" } +kct-ksp = { module = "dev.zacsweers.kctfork:ksp", version.ref = "kct" } diff --git a/processor/build.gradle.kts b/processor/build.gradle.kts index 193f2d6..c073397 100644 --- a/processor/build.gradle.kts +++ b/processor/build.gradle.kts @@ -40,4 +40,5 @@ dependencies { testImplementation(libs.kct.ksp) testImplementation(libs.ksp.api) testImplementation(libs.truth) + testImplementation(libs.kotlinpoet.ksp) } diff --git a/processor/src/main/kotlin/dev/zacsweers/autoservice/ksp/AutoServiceSymbolProcessor.kt b/processor/src/main/kotlin/dev/zacsweers/autoservice/ksp/AutoServiceSymbolProcessor.kt index 47e8d82..7261fab 100644 --- a/processor/src/main/kotlin/dev/zacsweers/autoservice/ksp/AutoServiceSymbolProcessor.kt +++ b/processor/src/main/kotlin/dev/zacsweers/autoservice/ksp/AutoServiceSymbolProcessor.kt @@ -82,6 +82,8 @@ public class AutoServiceSymbolProcessor(environment: SymbolProcessorEnvironment) return emptyList() } + val deferred = mutableListOf() + resolver .getSymbolsWithAnnotation(AUTO_SERVICE_NAME) .filterIsInstance() @@ -119,34 +121,51 @@ public class AutoServiceSymbolProcessor(environment: SymbolProcessorEnvironment) } for (providerType in providerInterfaces) { - val providerDecl = providerType.declaration.closestClassDeclaration()!! - if (checkImplementer(providerImplementer, providerType)) { - providers.put( - providerDecl.toBinaryName(), - providerImplementer.toBinaryName() to providerImplementer.containingFile!!) - } else { - val message = - "ServiceProviders must implement their service provider interface. " + - providerImplementer.qualifiedName + - " does not implement " + - providerDecl.qualifiedName - logger.error(message, providerImplementer) + if (providerType.isError) { + deferred += providerImplementer return@forEach } + val providerDecl = providerType.declaration.closestClassDeclaration()!! + when (checkImplementer(providerImplementer, providerType)) { + ValidationResult.VALID -> { + providers.put( + providerDecl.toBinaryName(), + providerImplementer.toBinaryName() to providerImplementer.containingFile!!, + ) + } + ValidationResult.INVALID -> { + val message = + "ServiceProviders must implement their service provider interface. " + + providerImplementer.qualifiedName + + " does not implement " + + providerDecl.qualifiedName + logger.error(message, providerImplementer) + } + ValidationResult.DEFERRED -> { + deferred += providerImplementer + } + } } } generateAndClearConfigFiles() - return emptyList() + return deferred } private fun checkImplementer( providerImplementer: KSClassDeclaration, - providerType: KSType - ): Boolean { + providerType: KSType, + ): ValidationResult { if (!verify) { - return true + return ValidationResult.VALID + } + for (superType in providerImplementer.getAllSuperTypes()) { + if (superType.isAssignableFrom(providerType)) { + return ValidationResult.VALID + } else if (superType.isError) { + return ValidationResult.DEFERRED + } } - return providerImplementer.getAllSuperTypes().any { it.isAssignableFrom(providerType) } + return ValidationResult.INVALID } private fun generateAndClearConfigFiles() { @@ -200,6 +219,12 @@ public class AutoServiceSymbolProcessor(environment: SymbolProcessorEnvironment) return ClassName(pkgName, simpleNames) } + private enum class ValidationResult { + VALID, + INVALID, + DEFERRED, + } + @AutoService(SymbolProcessorProvider::class) public class Provider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = diff --git a/processor/src/test/kotlin/dev/zacsweers/autoservice/ksp/AutoServiceSymbolProcessorTest.kt b/processor/src/test/kotlin/dev/zacsweers/autoservice/ksp/AutoServiceSymbolProcessorTest.kt index 2571e13..a2eb87e 100644 --- a/processor/src/test/kotlin/dev/zacsweers/autoservice/ksp/AutoServiceSymbolProcessorTest.kt +++ b/processor/src/test/kotlin/dev/zacsweers/autoservice/ksp/AutoServiceSymbolProcessorTest.kt @@ -18,15 +18,29 @@ package dev.zacsweers.autoservice.ksp import com.google.common.truth.Truth.assertThat +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +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.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.ksp.addOriginatingKSFile +import com.squareup.kotlinpoet.ksp.writeTo import com.tschuchort.compiletesting.KotlinCompilation import com.tschuchort.compiletesting.KotlinCompilation.ExitCode import com.tschuchort.compiletesting.SourceFile import com.tschuchort.compiletesting.configureKsp +import com.tschuchort.compiletesting.SourceFile.Companion.kotlin +import com.tschuchort.compiletesting.kspIncremental import com.tschuchort.compiletesting.kspSourcesDir import com.tschuchort.compiletesting.symbolProcessorProviders import com.tschuchort.compiletesting.useKsp2 import java.io.File import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -65,7 +79,7 @@ class AutoServiceSymbolProcessorTest( @Test fun smokeTest() { val source = - SourceFile.kotlin( + kotlin( "CustomCallable.kt", """ package test @@ -119,7 +133,7 @@ class AutoServiceSymbolProcessorTest( @Test fun errorOnNoServiceInterfacesProvided() { val source = - SourceFile.kotlin( + kotlin( "CustomCallable.kt", """ package test @@ -145,4 +159,77 @@ class AutoServiceSymbolProcessorTest( """ .trimIndent()) } + + class TacoGenerator(private val codeGenerator: CodeGenerator) : SymbolProcessor { + class Provider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment) = + TacoGenerator(environment.codeGenerator) + } + + override fun process(resolver: Resolver): List { + resolver + .getSymbolsWithAnnotation("test.GenerateInterface") + .filterIsInstance() + .forEach { annotated -> + FileSpec.get( + annotated.packageName.asString(), + TypeSpec.interfaceBuilder("I${annotated.simpleName.asString()}") + .addOriginatingKSFile(annotated.containingFile!!) + .build(), + ) + .writeTo(codeGenerator, aggregating = false) + } + return emptyList() + } + } + + @Ignore("https://github.com/google/ksp/issues/1916") + @Test + fun deferredTypes() { + // This tests handling error types in multiple rounds to ensure we defer correctly when + // encountering error types. This works by having a simple processor that generates an + // interface with a given name from an annotated class, then an AutoService-annotated + // class that implements that interface. + val generateAnnotation = + kotlin( + "GenerateInterface.kt", + """ + package test + annotation class GenerateInterface + """, + ) + val source = + kotlin( + "CustomCallable.kt", + """ + package test + import com.google.auto.service.AutoService + import java.util.concurrent.Callable + import test.IExample + + @GenerateInterface + class Example + + @AutoService(IExample::class, Callable::class) + class ExampleImpl : IExample, Callable { + override fun call(): String = "Hello world!" + } + """, + ) + + val compilation = + KotlinCompilation().apply { + sources = listOf(generateAnnotation, source) + inheritClassPath = true + symbolProcessorProviders = + listOf(AutoServiceSymbolProcessor.Provider(), TacoGenerator.Provider()) + kspIncremental = incremental + } + val result = compilation.compile() + assertThat(result.exitCode).isEqualTo(ExitCode.OK) + val generatedSourcesDir = compilation.kspSourcesDir + val generatedFile = File(generatedSourcesDir, "resources/META-INF/services/test.IExample") + assertThat(generatedFile.exists()).isTrue() + assertThat(generatedFile.readText()).isEqualTo("test.ExampleImpl\n") + } }