26
26
27
27
package io.spine.validation.java.ksp
28
28
29
+ import com.google.devtools.ksp.getClassDeclarationByName
30
+ import com.google.devtools.ksp.getConstructors
31
+ import com.google.devtools.ksp.isPublic
29
32
import com.google.devtools.ksp.processing.CodeGenerator
30
33
import com.google.devtools.ksp.processing.Dependencies
31
34
import com.google.devtools.ksp.processing.Resolver
32
35
import com.google.devtools.ksp.processing.SymbolProcessor
33
36
import com.google.devtools.ksp.symbol.KSAnnotated
34
- import com.google.devtools.ksp.symbol.KSAnnotation
35
37
import com.google.devtools.ksp.symbol.KSClassDeclaration
36
38
import com.google.devtools.ksp.symbol.KSType
37
39
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
38
44
import io.spine.validation.api.DiscoveredValidators
45
+ import io.spine.validation.api.MessageValidator
39
46
import io.spine.validation.api.Validator
40
47
41
48
/* *
@@ -46,65 +53,185 @@ import io.spine.validation.api.Validator
46
53
*/
47
54
internal class ValidatorProcessor (codeGenerator : CodeGenerator ) : SymbolProcessor {
48
55
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
+ */
50
74
private val output = codeGenerator.createNewFileByPath(
51
75
dependencies = Dependencies (aggregating = true ),
52
76
path = DiscoveredValidators .RESOURCES_LOCATION ,
53
77
extensionName = " "
54
78
).writer()
55
79
56
80
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
+ }
63
102
}
64
-
65
- if (validators.isEmpty()) {
66
- return emptyList()
67
- }
68
103
69
104
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 " )
78
109
}
79
110
}
80
111
81
- // Return an empty list: no deferred symbols
82
112
return emptyList()
83
113
}
114
+ }
84
115
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
+ }
88
146
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() }
92
153
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 {
99
158
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
103
163
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
+ )
105
176
}
106
177
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()
109
186
}
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
110
218
}
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