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'

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