Skip to content

Commit 83c932e

Browse files
authored
feat: Contracts with no providers no longer fail (#11)
Co-authored-by: LMLiam <46268350+TheRealEmissions@users.noreply.github.com>
1 parent 38c3c47 commit 83c932e

6 files changed

Lines changed: 1046 additions & 753 deletions

File tree

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
kotlin.code.style=official
22
group=io.github.eventhorizonlab
3-
baseVersion=0.1.20
3+
baseVersion=0.1.21
44
org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8

modules/annotations/src/main/kotlin/com/github/eventhorizonlab/spi/ServiceScheme.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import kotlin.reflect.KClass
99
@MustBeDocumented
1010
@Target(AnnotationTarget.CLASS)
1111
@Retention(AnnotationRetention.RUNTIME)
12-
annotation class ServiceProvider(vararg val value: KClass<*>)
12+
annotation class ServiceProvider(
13+
vararg val value: KClass<*>,
14+
)
1315

1416
/**
1517
* Marks an interface as a ServiceLoader contract.
1618
*/
1719
@MustBeDocumented
1820
@Target(AnnotationTarget.CLASS)
1921
@Retention(AnnotationRetention.RUNTIME)
20-
annotation class ServiceContract
22+
annotation class ServiceContract

modules/processor/src/main/kotlin/com/github/eventhorizonlab/spi/ServiceSchemeProcessor.kt

Lines changed: 143 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -15,41 +15,45 @@ import javax.tools.Diagnostic
1515
import javax.tools.StandardLocation
1616

1717
private data class ProviderInfo(
18-
val contractCanonical: String, val contractBinary: String, val providerBinary: String
18+
val contractCanonical: String,
19+
val contractBinary: String,
20+
val providerBinary: String,
1921
)
2022

2123
@SupportedOptions("org.gradle.annotation.processing.aggregating")
2224
@AutoService(Processor::class)
2325
@SupportedSourceVersion(SourceVersion.RELEASE_24)
2426
class ServiceSchemeProcessor : AbstractProcessor() {
25-
26-
override fun getSupportedAnnotationTypes() = setOf(
27-
ServiceProvider::class.java.canonicalName, ServiceContract::class.java.canonicalName
28-
)
27+
override fun getSupportedAnnotationTypes() =
28+
setOf(
29+
ServiceProvider::class.java.canonicalName,
30+
ServiceContract::class.java.canonicalName,
31+
)
2932

3033
private val contracts = mutableSetOf<String>() // canonical names
3134
private val providers = mutableListOf<ProviderInfo>()
3235

3336
override fun process(
34-
annotations: MutableSet<out TypeElement>, roundEnv: RoundEnvironment
37+
annotations: MutableSet<out TypeElement>,
38+
roundEnv: RoundEnvironment,
3539
): Boolean {
36-
// 1) Collect contracts defined in this compilation
37-
roundEnv.getElementsAnnotatedWith(ServiceContract::class.java).filter { it.kind == ElementKind.INTERFACE }
38-
.map { (it as TypeElement).qualifiedName.toString() }.forEach { contracts += it }
40+
// 1) Collect contracts
41+
roundEnv
42+
.getElementsAnnotatedWith(ServiceContract::class.java)
43+
.filter { it.kind == ElementKind.INTERFACE }
44+
.map { (it as TypeElement).qualifiedName.toString() }
45+
.forEach { contracts += it }
3946

4047
// 2) Collect providers
4148
roundEnv.getElementsAnnotatedWith(ServiceProvider::class.java).forEach { element ->
42-
// 1) Collect all ServiceProvider mirrors (direct + from container if repeatable)
4349
val spMirrors = collectServiceProviderMirrors(element, processingEnv)
44-
45-
// 2) For each ServiceProvider mirror, read its "value" (array of class literals)
4650
spMirrors.forEach { spMirror ->
4751
val valuesWithDefaults = processingEnv.elementUtils.getElementValuesWithDefaults(spMirror)
4852
val valueAv =
49-
valuesWithDefaults.entries.firstOrNull { it.key.simpleName.contentEquals("value") }?.value ?: error(
50-
"@ServiceProvider missing 'value' on ${element.simpleName}"
51-
)
52-
53+
valuesWithDefaults.entries
54+
.firstOrNull { it.key.simpleName.contentEquals("value") }
55+
?.value
56+
?: error("@ServiceProvider missing 'value' on ${element.simpleName}")
5357
val typeMirrors = classArrayAnnotationValues(valueAv, processingEnv)
5458
typeMirrors.forEach { tm ->
5559
val contractElement = (tm as DeclaredType).asElement() as TypeElement
@@ -58,10 +62,13 @@ class ServiceSchemeProcessor : AbstractProcessor() {
5862
}
5963
}
6064

61-
// 3) On final round, generate files + validate
65+
// 3) Validate implementations every round (so we catch missing @ServiceProvider)
66+
validateImplementations(roundEnv)
67+
68+
// 4) On final round, generate files + validate provider targets
6269
if (roundEnv.processingOver()) {
6370
generateServiceFiles()
64-
validateProviders()
71+
validateProviderTargets()
6572
}
6673

6774
return true
@@ -73,120 +80,175 @@ class ServiceSchemeProcessor : AbstractProcessor() {
7380
}
7481

7582
// Flag contracts declared here with no providers
76-
contracts.filter { canonical ->
77-
providers.none { it.contractCanonical == canonical }
78-
}.forEach { contract ->
79-
processingEnv.messager.printMessage(
80-
Diagnostic.Kind.ERROR, missingServiceProviderErrorMessage(contract)
81-
)
82-
}
83+
contracts
84+
.filter { canonical ->
85+
providers.none { it.contractCanonical == canonical }
86+
}.forEach { contract ->
87+
processingEnv.messager.printMessage(
88+
Diagnostic.Kind.WARNING,
89+
"No @ServiceProvider found for contract $contract in this compilation unit. " +
90+
"This may be intentional if this module only defines contracts.",
91+
)
92+
}
93+
}
94+
95+
private fun validateImplementations(roundEnv: RoundEnvironment) {
96+
val contractTypes = contracts.mapNotNull { processingEnv.elementUtils.getTypeElement(it) }
97+
val contractTypeMirrors = contractTypes.map { it.asType() }.toSet()
98+
99+
roundEnv.rootElements
100+
.filterIsInstance<TypeElement>()
101+
.filter { it.kind == ElementKind.CLASS }
102+
.forEach { clazz ->
103+
val implementsContract =
104+
contractTypeMirrors.any { ct ->
105+
processingEnv.typeUtils.isAssignable(clazz.asType(), ct)
106+
}
107+
val hasServiceProvider =
108+
clazz.annotationMirrors.any {
109+
(it.annotationType.asElement() as TypeElement).qualifiedName.toString() ==
110+
ServiceProvider::class.java.canonicalName
111+
}
112+
if (implementsContract && !hasServiceProvider) {
113+
val contractName =
114+
contractTypes
115+
.first {
116+
processingEnv.typeUtils.isAssignable(clazz.asType(), it.asType())
117+
}.qualifiedName
118+
.toString()
119+
processingEnv.messager.printMessage(
120+
Diagnostic.Kind.ERROR,
121+
missingServiceProviderErrorMessage(contractName),
122+
)
123+
}
124+
}
83125
}
84126

85-
private fun validateProviders() {
127+
private fun validateProviderTargets() {
86128
providers.map { it.contractCanonical }.distinct().forEach { canonicalName ->
87129
val contractElement = processingEnv.elementUtils.getTypeElement(canonicalName)
88-
val hasAnnotation = contractElement?.annotationMirrors?.any {
89-
val annType = (it.annotationType.asElement() as TypeElement).qualifiedName.toString()
90-
annType == ServiceContract::class.java.canonicalName
91-
} ?: false
92-
130+
val hasAnnotation =
131+
contractElement?.annotationMirrors?.any {
132+
val annType = (it.annotationType.asElement() as TypeElement).qualifiedName.toString()
133+
annType == ServiceContract::class.java.canonicalName
134+
} ?: false
93135
if (!hasAnnotation) {
94-
val hint = if (contractElement == null) {
95-
"Contract type not found — is the API module on the processor's compile classpath?"
96-
} else {
97-
"Type is present but not annotated with @ServiceContract."
98-
}
136+
val hint =
137+
if (contractElement == null) {
138+
"Contract type not found— is the API module on the processor's compile classpath?"
139+
} else {
140+
"Type is present but not annotated with @ServiceContract."
141+
}
99142
processingEnv.messager.printMessage(
100143
Diagnostic.Kind.ERROR,
101-
"@ServiceProvider target $canonicalName is not annotated with @ServiceContract. $hint"
144+
"@ServiceProvider target $canonicalName is not annotated with @ServiceContract. $hint",
102145
)
103146
}
104147
}
105148
}
106149

107-
private fun writeServiceFile(contractBinary: String, impls: List<String>) {
150+
private fun writeServiceFile(
151+
contractBinary: String,
152+
impls: List<String>,
153+
) {
108154
processingEnv.messager.printMessage(
109155
Diagnostic.Kind.NOTE,
110-
"Writing META-INF/services/$contractBinary with ${impls.size} implementation(s): ${impls.joinToString()}"
156+
"Writing META-INF/services/$contractBinary with ${impls.size} implementation(s): ${impls.joinToString()}",
111157
)
112-
processingEnv.filer.createResource(StandardLocation.CLASS_OUTPUT, "", "META-INF/services/$contractBinary")
113-
.openWriter().use { writer ->
158+
processingEnv.filer
159+
.createResource(StandardLocation.CLASS_OUTPUT, "", "META-INF/services/$contractBinary")
160+
.openWriter()
161+
.use { writer ->
114162
impls.forEach { writer.write("$it\n") }
115163
}
116164
}
117165

118-
private fun addProvider(providerElement: TypeElement, contractElement: TypeElement) {
166+
private fun addProvider(
167+
providerElement: TypeElement,
168+
contractElement: TypeElement,
169+
) {
119170
val contractCanonical = contractElement.qualifiedName.toString()
120171
val contractBinary = processingEnv.getStringifiedBinaryName(contractElement)
121172
val providerBinary = processingEnv.getStringifiedBinaryName(providerElement)
122173

123174
providers += ProviderInfo(contractCanonical, contractBinary, providerBinary)
124175
}
125176

126-
127177
/**
128178
* Returns all @ServiceProvider annotation mirrors on the element, expanding a repeatable
129179
* container if present, using only javax.lang.model APIs.
130180
*/
131181
private fun collectServiceProviderMirrors(
132-
element: javax.lang.model.element.Element, processingEnv: ProcessingEnvironment
182+
element: javax.lang.model.element.Element,
183+
processingEnv: ProcessingEnvironment,
133184
): List<AnnotationMirror> {
134185
val spName = ServiceProvider::class.java.canonicalName
135186
val spType = processingEnv.elementUtils.getTypeElement(spName) ?: return emptyList()
136187

137188
// Find container type from @Repeatable, if any
138-
val containerName = spType.annotationMirrors.firstNotNullOfOrNull { am ->
139-
val annType = (am.annotationType.asElement() as TypeElement).qualifiedName.toString()
140-
if (annType == Repeatable::class.java.canonicalName) {
141-
// @Repeatable(value = Container.class)
142-
val values = processingEnv.elementUtils.getElementValuesWithDefaults(am)
143-
val repeatableValueAv = values.entries.first { it.key.simpleName.contentEquals("value") }.value
144-
val containerTm = repeatableValueAv.value as TypeMirror
145-
((containerTm as DeclaredType).asElement() as TypeElement).qualifiedName.toString()
146-
} else null
147-
}
189+
val containerName =
190+
spType.annotationMirrors.firstNotNullOfOrNull { am ->
191+
val annType = (am.annotationType.asElement() as TypeElement).qualifiedName.toString()
192+
if (annType == Repeatable::class.java.canonicalName) {
193+
// @Repeatable(value = Container.class)
194+
val values = processingEnv.elementUtils.getElementValuesWithDefaults(am)
195+
val repeatableValueAv = values.entries.first { it.key.simpleName.contentEquals("value") }.value
196+
val containerTm = repeatableValueAv.value as TypeMirror
197+
((containerTm as DeclaredType).asElement() as TypeElement).qualifiedName.toString()
198+
} else {
199+
null
200+
}
201+
}
148202

149-
val direct = element.annotationMirrors.filter { m ->
150-
((m.annotationType.asElement() as TypeElement).qualifiedName.toString() == spName)
151-
}
203+
val direct =
204+
element.annotationMirrors.filter { m ->
205+
((m.annotationType.asElement() as TypeElement).qualifiedName.toString() == spName)
206+
}
152207

153-
val expandedFromContainer = if (containerName != null) {
154-
element.annotationMirrors.filter { (it.annotationType.asElement() as TypeElement).qualifiedName.toString() == containerName }
155-
.flatMap { containerMirror ->
156-
// Container has a "value" which is an array of nested @ServiceProvider annotations
157-
val values = processingEnv.elementUtils.getElementValuesWithDefaults(containerMirror)
158-
val valueAv = values.entries.firstOrNull { it.key.simpleName.contentEquals("value") }?.value
159-
?: return@flatMap emptyList()
160-
val arr = valueAv.value as List<*>
161-
arr.filterIsInstance<AnnotationValue>().mapNotNull { it.value as? AnnotationMirror }
162-
}
163-
} else emptyList()
208+
val expandedFromContainer =
209+
if (containerName != null) {
210+
element.annotationMirrors
211+
.filter { (it.annotationType.asElement() as TypeElement).qualifiedName.toString() == containerName }
212+
.flatMap { containerMirror ->
213+
// Container has a "value" which is an array of nested @ServiceProvider annotations
214+
val values = processingEnv.elementUtils.getElementValuesWithDefaults(containerMirror)
215+
val valueAv =
216+
values.entries.firstOrNull { it.key.simpleName.contentEquals("value") }?.value
217+
?: return@flatMap emptyList()
218+
val arr = valueAv.value as List<*>
219+
arr.filterIsInstance<AnnotationValue>().mapNotNull { it.value as? AnnotationMirror }
220+
}
221+
} else {
222+
emptyList()
223+
}
164224

165225
return direct + expandedFromContainer
166226
}
167227

168-
private fun toTypeMirror(raw: Any?, processingEnv: ProcessingEnvironment): TypeMirror? = when (raw) {
169-
null -> null
170-
is TypeMirror -> raw
171-
is AnnotationValue -> toTypeMirror(raw.value, processingEnv)
172-
is String -> processingEnv.elementUtils.getTypeElement(raw)?.asType()
173-
else -> null
174-
}
228+
private fun toTypeMirror(
229+
raw: Any?,
230+
processingEnv: ProcessingEnvironment,
231+
): TypeMirror? =
232+
when (raw) {
233+
null -> null
234+
is TypeMirror -> raw
235+
is AnnotationValue -> toTypeMirror(raw.value, processingEnv)
236+
is String -> processingEnv.elementUtils.getTypeElement(raw)?.asType()
237+
else -> null
238+
}
175239

176240
/**
177241
* Converts the "value" of a class[] annotation member into a List<TypeMirror>,
178242
* handling both array and single-class forms.
179243
*/
180244
private fun classArrayAnnotationValues(
181245
valueAv: AnnotationValue,
182-
processingEnv: ProcessingEnvironment
183-
): List<TypeMirror> {
184-
return when (val raw = valueAv.value) {
246+
processingEnv: ProcessingEnvironment,
247+
): List<TypeMirror> =
248+
when (val raw = valueAv.value) {
185249
is List<*> -> raw.mapNotNull { toTypeMirror(it, processingEnv) }
186250
else -> listOfNotNull(toTypeMirror(raw, processingEnv))
187251
}
188-
}
189-
190252
}
191253

192-
internal fun missingServiceProviderErrorMessage(contract: String) = "No @ServiceProvider found for contract $contract"
254+
internal fun missingServiceProviderErrorMessage(contract: String) = "No @ServiceProvider found for contract $contract"

modules/processor/src/main/kotlin/com/github/eventhorizonlab/spi/extensions/ProcessingEnvironmentExtensions.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,4 @@ package com.github.eventhorizonlab.spi.extensions
33
import javax.annotation.processing.ProcessingEnvironment
44
import javax.lang.model.element.TypeElement
55

6-
internal fun ProcessingEnvironment.getStringifiedBinaryName(type: TypeElement) =
7-
elementUtils.getBinaryName(type).toString()
6+
internal fun ProcessingEnvironment.getStringifiedBinaryName(type: TypeElement) = elementUtils.getBinaryName(type).toString()

0 commit comments

Comments
 (0)