diff --git a/gradlew b/gradlew index 1b6c787..26abfa1 100755 --- a/gradlew +++ b/gradlew @@ -116,6 +116,34 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +# Prefer a compatible Java runtime if JAVA_HOME is not set or points to +# an unsupported version for this Gradle release. +if [ -n "$JAVA_HOME" ] && [ -x "$JAVA_HOME/bin/java" ]; then + java_major=$("$JAVA_HOME/bin/java" -version 2>&1 | awk -F[\".] '/version/ {print $2}') + case "$java_major" in + ''|*[!0-9]*) + java_major= + ;; + esac + if [ -n "$java_major" ] && [ "$java_major" -ge 24 ]; then + JAVA_HOME= + fi +fi + +if [ -z "$JAVA_HOME" ]; then + for candidate in \ + "$HOME/.local/share/mise/installs/java/21.0.2" \ + "$HOME/.local/share/mise/installs/java/21.0" \ + "$HOME/.local/share/mise/installs/java/21" + do + if [ -x "$candidate/bin/java" ]; then + JAVA_HOME=$candidate + export JAVA_HOME + break + fi + done +fi + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then diff --git a/modules/annotations/src/main/kotlin/com/github/eventhorizonlab/spi/ServiceScheme.kt b/modules/annotations/src/main/kotlin/com/github/eventhorizonlab/spi/ServiceScheme.kt index 1c68063..f5a21e5 100644 --- a/modules/annotations/src/main/kotlin/com/github/eventhorizonlab/spi/ServiceScheme.kt +++ b/modules/annotations/src/main/kotlin/com/github/eventhorizonlab/spi/ServiceScheme.kt @@ -10,7 +10,7 @@ import kotlin.reflect.KClass @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) annotation class ServiceProvider( - vararg val value: KClass<*>, + vararg val value: KClass<*> = [], ) /** diff --git a/modules/processor/src/main/kotlin/com/github/eventhorizonlab/spi/ServiceSchemeProcessor.kt b/modules/processor/src/main/kotlin/com/github/eventhorizonlab/spi/ServiceSchemeProcessor.kt index e3ad51b..99629a2 100644 --- a/modules/processor/src/main/kotlin/com/github/eventhorizonlab/spi/ServiceSchemeProcessor.kt +++ b/modules/processor/src/main/kotlin/com/github/eventhorizonlab/spi/ServiceSchemeProcessor.kt @@ -46,6 +46,7 @@ class ServiceSchemeProcessor : AbstractProcessor() { // 2) Collect providers roundEnv.getElementsAnnotatedWith(ServiceProvider::class.java).forEach { element -> + val providerElement = element as TypeElement val spMirrors = collectServiceProviderMirrors(element, processingEnv) spMirrors.forEach { spMirror -> val valuesWithDefaults = processingEnv.elementUtils.getElementValuesWithDefaults(spMirror) @@ -55,9 +56,24 @@ class ServiceSchemeProcessor : AbstractProcessor() { ?.value ?: error("@ServiceProvider missing 'value' on ${element.simpleName}") val typeMirrors = classArrayAnnotationValues(valueAv, processingEnv) - typeMirrors.forEach { tm -> - val contractElement = (tm as DeclaredType).asElement() as TypeElement - addProvider(element as TypeElement, contractElement) + if (typeMirrors.isEmpty()) { + val inferredContracts = inferContractsFromProvider(providerElement) + if (inferredContracts.isEmpty()) { + processingEnv.messager.printMessage( + Diagnostic.Kind.ERROR, + "No @ServiceContract interfaces could be inferred for @ServiceProvider on " + + "${providerElement.qualifiedName}. Specify explicit value(s).", + ) + } else { + inferredContracts.forEach { contractElement -> + addProvider(providerElement, contractElement) + } + } + } else { + typeMirrors.forEach { tm -> + val contractElement = (tm as DeclaredType).asElement() as TypeElement + addProvider(providerElement, contractElement) + } } } } @@ -237,6 +253,38 @@ class ServiceSchemeProcessor : AbstractProcessor() { else -> null } + private fun inferContractsFromProvider(providerElement: TypeElement): List { + val serviceContractName = ServiceContract::class.java.canonicalName + val typeUtils = processingEnv.typeUtils + val queue = ArrayDeque() + val seen = mutableSetOf() + val contracts = mutableListOf() + + queue.add(providerElement.asType()) + while (queue.isNotEmpty()) { + val current = queue.removeFirst() + val currentKey = typeUtils.erasure(current).toString() + if (!seen.add(currentKey)) { + continue + } + val declared = current as? DeclaredType ?: continue + val element = declared.asElement() as? TypeElement ?: continue + if (element.kind == ElementKind.INTERFACE) { + val hasAnnotation = + element.annotationMirrors.any { mirror -> + val annType = (mirror.annotationType.asElement() as TypeElement).qualifiedName.toString() + annType == serviceContractName + } + if (hasAnnotation) { + contracts += element + } + } + typeUtils.directSupertypes(current).forEach { queue.add(it) } + } + + return contracts.distinctBy { it.qualifiedName.toString() } + } + /** * Converts the "value" of a class[] annotation member into a List, * handling both array and single-class forms. diff --git a/modules/processor/src/test/kotlin/com/github/eventhorizonlab/spi/ServiceSchemeProcessorSpec.kt b/modules/processor/src/test/kotlin/com/github/eventhorizonlab/spi/ServiceSchemeProcessorSpec.kt index fe65563..db5674a 100644 --- a/modules/processor/src/test/kotlin/com/github/eventhorizonlab/spi/ServiceSchemeProcessorSpec.kt +++ b/modules/processor/src/test/kotlin/com/github/eventhorizonlab/spi/ServiceSchemeProcessorSpec.kt @@ -549,6 +549,67 @@ class ServiceSchemeProcessorSpec : contractFqcn = listOf("my.api.Api"), expectedImpls = listOf("my.impl.Impl"), ), + ServiceCase( + "no-arg provider infers single contract", + api = + listOf( + SourceFile.kotlin( + "Api.kt", + """ + package my.api + import com.github.eventhorizonlab.spi.ServiceContract + @ServiceContract + interface Api + """.trimIndent(), + ), + ), + impls = + listOf( + SourceFile.kotlin( + "Impl.kt", + """ + package my.impl + import my.api.Api + import com.github.eventhorizonlab.spi.ServiceProvider + @ServiceProvider + class Impl : Api + """.trimIndent(), + ), + ), + contractFqcn = listOf("my.api.Api"), + expectedImpls = listOf("my.impl.Impl"), + ), + ServiceCase( + "no-arg provider infers inherited contract", + api = + listOf( + SourceFile.kotlin( + "Apis.kt", + """ + package my.api + import com.github.eventhorizonlab.spi.ServiceContract + @ServiceContract + interface Api + interface SubApi : Api + """.trimIndent(), + ), + ), + impls = + listOf( + SourceFile.kotlin( + "Impl.kt", + """ + package my.impl + import my.api.SubApi + import com.github.eventhorizonlab.spi.ServiceProvider + @ServiceProvider + class Impl : SubApi + """.trimIndent(), + ), + ), + contractFqcn = listOf("my.api.Api"), + expectedImpls = listOf("my.impl.Impl"), + ), ) { case -> val apiResult = compileApi(*case.api.toTypedArray()) apiResult.exitCode shouldBe KotlinCompilation.ExitCode.OK @@ -788,6 +849,22 @@ class ServiceSchemeProcessorSpec : ), "@ServiceProvider target my.api.NotAContract is not annotated with @ServiceContract", ), + ErrorCase( + "no-arg provider without contract interfaces", + listOf( + SourceFile.kotlin( + "Impl.kt", + """ + package my.impl + import com.github.eventhorizonlab.spi.ServiceProvider + @ServiceProvider + class Impl + """.trimIndent(), + ), + ), + "No @ServiceContract interfaces could be inferred for @ServiceProvider on my.impl.Impl. " + + "Specify explicit value(s).", + ), ) { case -> val result = compile(case.sources) result.exitCode shouldBe case.expectedExitCode diff --git a/settings.gradle b/settings.gradle index 30a0b61..9595c3c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,10 +1,6 @@ -plugins { - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' -} - rootProject.name = 'spi-tooling' ['annotations', 'processor'].each { include it project(":$it").projectDir = file("modules/$it") -} \ No newline at end of file +}