Skip to content

Commit ef1a3f7

Browse files
author
yevhenii-nadtochii
committed
Implement tests for KSP processor
1 parent a513a2a commit ef1a3f7

File tree

4 files changed

+278
-72
lines changed

4 files changed

+278
-72
lines changed

java-api/src/main/kotlin/io/spine/validation/api/Validator.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import kotlin.reflect.KClass
3838
*
3939
* 1. The class must implement the [MessageValidator] interface.
4040
* 2. The class must have a public, no-args constructor.
41+
* 3. The class cannot be `inner`.
4142
*
4243
* @see MessageValidator
4344
*/

java-ksp/src/main/kotlin/io/spine/validation/java/ksp/ValidatorProcessor.kt

Lines changed: 166 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,23 @@
2626

2727
package io.spine.validation.java.ksp
2828

29+
import com.google.devtools.ksp.getClassDeclarationByName
30+
import com.google.devtools.ksp.getConstructors
31+
import com.google.devtools.ksp.isPublic
2932
import com.google.devtools.ksp.processing.CodeGenerator
3033
import com.google.devtools.ksp.processing.Dependencies
3134
import com.google.devtools.ksp.processing.Resolver
3235
import com.google.devtools.ksp.processing.SymbolProcessor
3336
import com.google.devtools.ksp.symbol.KSAnnotated
34-
import com.google.devtools.ksp.symbol.KSAnnotation
3537
import com.google.devtools.ksp.symbol.KSClassDeclaration
3638
import com.google.devtools.ksp.symbol.KSType
3739
import com.google.devtools.ksp.symbol.KSTypeReference
40+
import com.google.devtools.ksp.symbol.Modifier
41+
import io.spine.string.qualified
42+
import io.spine.string.qualifiedClassName
43+
import io.spine.string.simply
3844
import io.spine.validation.api.DiscoveredValidators
45+
import io.spine.validation.api.MessageValidator
3946
import io.spine.validation.api.Validator
4047

4148
/**
@@ -46,65 +53,185 @@ import io.spine.validation.api.Validator
4653
*/
4754
internal class ValidatorProcessor(codeGenerator: CodeGenerator) : SymbolProcessor {
4855

49-
private val discoveredValidators = mutableSetOf<String>()
56+
/**
57+
* Already discovered validators.
58+
*
59+
* The map contains a mapping of the message class to the validator.
60+
*
61+
* The same validator can be discovered several times because
62+
* KSP may have several rounds of the code analysis.
63+
*
64+
* This map is used to prevent outputting the same validator twice
65+
* and to check is the same message has more than one validator.
66+
*/
67+
private val alreadyDiscovered = mutableMapOf<KSClassDeclaration, KSClassDeclaration>()
68+
69+
/**
70+
* The output file with the discovered validators.
71+
*
72+
* Each line represents a single mapping: `${MESSAGE_CLASS}:${VALIDATOR_CLASS}`.
73+
*/
5074
private val output = codeGenerator.createNewFileByPath(
5175
dependencies = Dependencies(aggregating = true),
5276
path = DiscoveredValidators.RESOURCES_LOCATION,
5377
extensionName = ""
5478
).writer()
5579

5680
override fun process(resolver: Resolver): List<KSAnnotated> {
57-
val validators = resolver.getSymbolsWithAnnotation(validator.qualifiedName!!)
58-
.filterIsInstance<KSClassDeclaration>().associateBy { kclass ->
59-
val annotation = kclass.annotations.find {
60-
it.shortName.getShortName() == validator.simpleName
61-
}!!
62-
annotation.argumentValue()
81+
val messageValidatorInterface = resolver
82+
.getClassDeclarationByName<MessageValidator<*>>()!!
83+
.asStarProjectedType()
84+
val annotatedValidators = resolver
85+
.getSymbolsWithAnnotation(ValidatorAnnotation.qualifiedName!!)
86+
.filterIsInstance<KSClassDeclaration>() // Matches the declared annotation target.
87+
.onEach { it.checkApplicability(messageValidatorInterface) }
88+
val newlyDiscovered = annotatedValidators
89+
.map { validator ->
90+
val message = validator.validatedMessage(messageValidatorInterface)
91+
message to validator
92+
}
93+
.filterNot { (message, validator) ->
94+
when (val previous = alreadyDiscovered[message]) {
95+
validator -> true // Prevents the same validator being discovered twice.
96+
null -> {
97+
alreadyDiscovered[message] = validator
98+
false
99+
}
100+
else -> message.reportDuplicateValidator(validator, previous)
101+
}
63102
}
64-
65-
if (validators.isEmpty()) {
66-
return emptyList()
67-
}
68103

69104
output.use { writer ->
70-
validators.forEach { (message, validator) ->
71-
val validatorFQN = validator.qualifiedName?.asString()
72-
?: noQualifiedName(validator)
73-
val messageFQN = message.qualifiedName?.asString()
74-
?: noQualifiedName(message) // May indicate a local message.
75-
if (discoveredValidators.add(validatorFQN)) {
76-
writer.appendLine("$messageFQN:$validatorFQN")
77-
}
105+
newlyDiscovered.forEach { (message, validator) ->
106+
val validatorFQN = validator.qualifiedName?.asString()!!
107+
val messageFQN = message.qualifiedName?.asString()!!
108+
writer.appendLine("$messageFQN:$validatorFQN")
78109
}
79110
}
80111

81-
// Return an empty list: no deferred symbols
82112
return emptyList()
83113
}
114+
}
84115

85-
private fun noQualifiedName(ksclass: KSClassDeclaration): Nothing = error(
86-
"The class `$ksclass` has no qualified name."
87-
)
116+
/**
117+
* Checks if the [Validator] annotation can be used with this [KSClassDeclaration].
118+
*
119+
* The method ensures the following:
120+
*
121+
* 1. This class is not `inner`.
122+
* 2. It implements [MessageValidator] interface.
123+
* 3. It has a public, no-args constructor.
124+
*/
125+
private fun KSClassDeclaration.checkApplicability(messageValidator: KSType) {
126+
check(!modifiers.contains(Modifier.INNER)) {
127+
"""
128+
The `${qualifiedName?.asString()}` class cannot be marked with the `@${simply<Validator>()}` annotation.
129+
This annotation is not applicable to the `inner` classes.
130+
Please consider making the class nested or top-level.
131+
""".trimIndent()
132+
}
133+
check(messageValidator.isAssignableFrom(asStarProjectedType())) {
134+
"""
135+
The `${qualifiedName?.asString()}` class cannot be marked with the `@${simply<Validator>()}` annotation.
136+
This annotation requires the target class to implement the `${qualified<MessageValidator<*>>()}` interface.
137+
""".trimIndent()
138+
}
139+
check(hasPublicNoArgConstructor()) {
140+
"""
141+
The `${qualifiedName?.asString()}` class cannot be marked with the `@${simply<Validator>()}` annotation.
142+
This annotation requires the target class to have a public, no-args constructor.
143+
""".trimIndent()
144+
}
145+
}
88146

89-
private fun KSAnnotation.argumentValue(argumentName: String = "value"): KSClassDeclaration {
90-
val valueArg = arguments.firstOrNull { it.name?.asString() == argumentName }
91-
?: error("Annotation `@$shortName` has no argument named `$argumentName`.")
147+
/**
148+
* Returns `true` if this class has a public, no-args constructor.
149+
*/
150+
private fun KSClassDeclaration.hasPublicNoArgConstructor(): Boolean =
151+
getConstructors()
152+
.any { it.isPublic() && it.parameters.isEmpty() }
92153

93-
// the raw .value can be a KSType or a KSTypeReference
94-
val kType: KSType = when (val raw = valueArg.value) {
95-
is KSType -> raw
96-
is KSTypeReference -> raw.resolve()
97-
else -> error("Unsupported annotation parameter type: `${raw?.javaClass}`.")
98-
}
154+
/**
155+
* Returns a class of the validated message of this validator [KSClassDeclaration].
156+
*/
157+
private fun KSClassDeclaration.validatedMessage(messageValidator: KSType): KSClassDeclaration {
99158

100-
// its declaration is a KSClassDeclaration
101-
val declaration = kType.declaration as? KSClassDeclaration
102-
?: error("Expected a class declaration, but got `$kType`.")
159+
val annotation = annotations.first { it.shortName.asString() == ValidatorAnnotation.simpleName }
160+
val annotationArg = annotation.arguments
161+
.first { it.name?.asString() == VALIDATOR_ARGUMENT_NAME }
162+
.value
103163

104-
return declaration
164+
// The argument value can be a `KSType` or a `KSTypeReference`.
165+
// The latter must be resolved.
166+
val annotationMessage = when (annotationArg) {
167+
is KSType -> annotationArg
168+
is KSTypeReference -> annotationArg.resolve()
169+
else -> error(
170+
"""
171+
`${simply<ValidatorProcessor>()}` cannot parse the argument parameter of the `@${annotation.shortName.asString()}` annotation.
172+
Unexpected KSP type of the argument value: `${annotationArg?.qualifiedClassName}`.
173+
The argument value: `$annotationArg`.
174+
""".trimIndent()
175+
)
105176
}
106177

107-
private companion object {
108-
val validator = Validator::class
178+
val interfaceMessage = interfaceMessage(messageValidator.declaration as KSClassDeclaration)!!
179+
check(annotationMessage == interfaceMessage) {
180+
"""
181+
The `@${annotation.shortName.asString()}` annotation is applied to incompatible `${qualifiedName?.asString()}` validator.
182+
The validated message type of the annotation and the validator must match.
183+
The message type specified for the annotation: `${annotationMessage.declaration.qualifiedName?.asString()}`.
184+
The message type specified for the validator: `${interfaceMessage.declaration.qualifiedName?.asString()}`.
185+
""".trimIndent()
109186
}
187+
188+
return annotationMessage.declaration as KSClassDeclaration
189+
}
190+
191+
/**
192+
* Walks the inheritance tree of this [KSClassDeclaration] and, if it implements
193+
* the generic interface [messageValidator], returns its single type‐argument.
194+
*/
195+
private fun KSClassDeclaration.interfaceMessage(
196+
messageValidator: KSClassDeclaration,
197+
visited: MutableSet<KSClassDeclaration> = mutableSetOf()
198+
): KSType? {
199+
200+
// Prevents cycles.
201+
if (!visited.add(this)) {
202+
return null
203+
}
204+
205+
for (superRef: KSTypeReference in superTypes) {
206+
val superType = superRef.resolve()
207+
val superDecl = superType.declaration as? KSClassDeclaration ?: continue
208+
209+
if (superDecl.qualifiedName?.asString() == messageValidator.qualifiedName?.asString()) {
210+
return superType.arguments.first().type?.resolve()
211+
}
212+
213+
superDecl.interfaceMessage(messageValidator, visited)
214+
?.let { return it }
215+
}
216+
217+
return null
110218
}
219+
220+
private fun KSClassDeclaration.reportDuplicateValidator(
221+
newValidator: KSClassDeclaration,
222+
oldValidator: KSClassDeclaration
223+
): Nothing = error("""
224+
Cannot register the `${newValidator.qualifiedName?.asString()}` validator.
225+
The message type `${qualifiedName?.asString()}` is already validated by the `${oldValidator.qualifiedName?.asString()}` validator.
226+
Only one validator is allowed per message type.
227+
""".trimIndent())
228+
229+
/**
230+
* The name of the [Validator.value] property.
231+
*/
232+
private const val VALIDATOR_ARGUMENT_NAME = "value"
233+
234+
/**
235+
* The class of the [Validator] annotation.
236+
*/
237+
private val ValidatorAnnotation = Validator::class

0 commit comments

Comments
 (0)