Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions gradlew

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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<*> = [],
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
}
}
}
Expand Down Expand Up @@ -237,6 +253,38 @@ class ServiceSchemeProcessor : AbstractProcessor() {
else -> null
}

private fun inferContractsFromProvider(providerElement: TypeElement): List<TypeElement> {
val serviceContractName = ServiceContract::class.java.canonicalName
val typeUtils = processingEnv.typeUtils
val queue = ArrayDeque<TypeMirror>()
val seen = mutableSetOf<String>()
val contracts = mutableListOf<TypeElement>()

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<TypeMirror>,
* handling both array and single-class forms.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 1 addition & 5 deletions settings.gradle
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}

rootProject.name = 'spi-tooling'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep a Java 24 toolchain source configured

Removing the Foojay toolchains resolver here means Gradle no longer has any toolchain download repository configured. The modules still request Java 24 toolchains (see modules/annotations/build.gradle and modules/processor/build.gradle), so any environment without a locally installed JDK 24 will now fail toolchain resolution with “no compatible toolchain” instead of provisioning one. If the intent is to avoid network access during settings evaluation, consider an alternative local toolchain repo configuration or lowering the toolchain version to what’s already installed rather than dropping resolver support entirely.

Useful? React with 👍 / 👎.


['annotations', 'processor'].each {
include it
project(":$it").projectDir = file("modules/$it")
}
}
Loading