@@ -7,17 +7,22 @@ import com.google.devtools.ksp.getVisibility
7
7
import com.google.devtools.ksp.processing.CodeGenerator
8
8
import com.google.devtools.ksp.processing.Dependencies
9
9
import com.google.devtools.ksp.processing.KSBuiltIns
10
+ import com.google.devtools.ksp.processing.KSPLogger
10
11
import com.google.devtools.ksp.processing.Resolver
11
12
import com.google.devtools.ksp.processing.SymbolProcessor
12
13
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
13
14
import com.google.devtools.ksp.processing.SymbolProcessorProvider
15
+ import com.google.devtools.ksp.symbol.ClassKind
14
16
import com.google.devtools.ksp.symbol.FunctionKind
15
17
import com.google.devtools.ksp.symbol.KSAnnotated
18
+ import com.google.devtools.ksp.symbol.KSClassDeclaration
19
+ import com.google.devtools.ksp.symbol.KSDeclaration
16
20
import com.google.devtools.ksp.symbol.KSFile
17
21
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
18
22
import com.google.devtools.ksp.symbol.KSType
19
23
import com.google.devtools.ksp.symbol.KSValueParameter
20
24
import com.google.devtools.ksp.symbol.Visibility
25
+ import com.squareup.kotlinpoet.ClassName
21
26
import com.squareup.kotlinpoet.CodeBlock
22
27
import com.squareup.kotlinpoet.FileSpec
23
28
import com.squareup.kotlinpoet.FunSpec
@@ -35,22 +40,21 @@ public class RoutingProcessorProvider : SymbolProcessorProvider {
35
40
override fun create (environment : SymbolProcessorEnvironment ): SymbolProcessor {
36
41
return RoutingProcessor (
37
42
codeGenerator = environment.codeGenerator,
38
- options = environment.options
43
+ options = environment.options,
44
+ logger = environment.logger,
39
45
)
40
46
}
41
47
}
42
48
43
49
private class RoutingProcessor (
44
50
private val codeGenerator : CodeGenerator ,
45
51
private val options : Map <String , String >,
52
+ private val logger : KSPLogger ,
46
53
) : SymbolProcessor {
47
54
private var invoked = false
48
55
49
56
private val fileName: String
50
- get() = options[" Routing_Module_Name" ] ? : " Module"
51
-
52
- private val composableEnabled: Boolean
53
- get() = options[" Routing_Compose_Enable" ]?.toBooleanStrictOrNull() ? : false
57
+ get() = (options[FLAG_ROUTING_MODULE_NAME ] ? : " Module" ) + " Routes"
54
58
55
59
override fun process (resolver : Resolver ): List <KSAnnotated > {
56
60
if (invoked) {
@@ -66,17 +70,9 @@ private class RoutingProcessor(
66
70
67
71
resolver
68
72
.getSymbolsWithAnnotation(Route ::class .java.name)
69
- .filterIsInstance<KSFunctionDeclaration >()
70
- .forEach { func ->
71
- val qualifiedName = func.qualifiedName?.asString()
72
- check(func.functionKind == FunctionKind .TOP_LEVEL ) {
73
- " $qualifiedName fun must be a top level fun"
74
- }
75
- check(func.getVisibility() != Visibility .PRIVATE ) {
76
- " $qualifiedName fun must not be private"
77
- }
78
- func.containingFile?.let (ksFiles::add)
79
- func.wrapFunctionWithHandle(qualifiedName, configureSpec, resolver)
73
+ .filterIsInstance<KSDeclaration >()
74
+ .forEach { symbol ->
75
+ symbol.transform(ksFiles, configureSpec, resolver)
80
76
}
81
77
82
78
configureSpec
@@ -87,28 +83,80 @@ private class RoutingProcessor(
87
83
}
88
84
89
85
@OptIn(KspExperimental ::class )
90
- private fun KSFunctionDeclaration. wrapFunctionWithHandle (
91
- qualifiedName : String? ,
86
+ private fun KSDeclaration. transform (
87
+ ksFiles : MutableSet < KSFile > ,
92
88
configureSpec : FunSpec .Builder ,
93
89
resolver : Resolver
94
90
) {
91
+ val qualifiedName = qualifiedName?.asString() ? : return
92
+ logger.info(" >>>> transforming: $qualifiedName " )
93
+ check(getVisibility() != Visibility .PRIVATE ) {
94
+ " $qualifiedName must not be private"
95
+ }
96
+ containingFile?.let (ksFiles::add)
95
97
val routeAnnotation = checkNotNull(getAnnotationsByType(Route ::class ).firstOrNull()) {
96
- " Invalid state because a @Route was not found to '$qualifiedName '"
98
+ " Invalid state because is missing @Route to '$qualifiedName '"
99
+ }
100
+ when (this ) {
101
+ is KSFunctionDeclaration -> {
102
+ check(functionKind == FunctionKind .TOP_LEVEL ) {
103
+ " $qualifiedName must be a top level fun"
104
+ }
105
+ logger.info(" >>>> transforming fun: $qualifiedName " )
106
+ wrapFunctionWithHandle(routeAnnotation, qualifiedName, configureSpec, resolver, null )
107
+ }
108
+
109
+ is KSClassDeclaration -> {
110
+ check(classKind == ClassKind .OBJECT || classKind == ClassKind .CLASS ) {
111
+ " $qualifiedName must be a class or object. ${classKind.type} is not supported"
112
+ }
113
+ check(superTypes.any { type ->
114
+ type.resolve().declaration.qualifiedName?.asString() == VOYAGER_SCREEN_QUALIFIED_NAME
115
+ }) {
116
+ " @Route can be applied to object or class that inherit from '$VOYAGER_SCREEN_QUALIFIED_NAME ' only"
117
+ }
118
+ logger.info(" >>>> transforming class: $qualifiedName " )
119
+ declarations
120
+ .filterIsInstance<KSFunctionDeclaration >()
121
+ .filter { func -> func.simpleName.asString() == CONSTRUCTOR_NAME }
122
+ .forEach { constructor ->
123
+ val annotation = constructor .getAnnotationsByType(Route ::class ).firstOrNull() ? : routeAnnotation
124
+ constructor .wrapFunctionWithHandle(
125
+ annotation,
126
+ qualifiedName,
127
+ configureSpec,
128
+ resolver,
129
+ classKind
130
+ )
131
+ }
132
+ }
133
+
134
+ else -> error(" $qualifiedName is not supported. Class and top level fun are supported only" )
97
135
}
136
+ }
137
+
138
+ private fun KSFunctionDeclaration.wrapFunctionWithHandle (
139
+ routeAnnotation : Route ,
140
+ qualifiedName : String ,
141
+ configureSpec : FunSpec .Builder ,
142
+ resolver : Resolver ,
143
+ classKind : ClassKind ? ,
144
+ ) {
98
145
val isRegexRoute = routeAnnotation.regex.isNotBlank()
99
146
check(isRegexRoute || routeAnnotation.path.isNotBlank()) {
100
- " Using @Route a path or a regex is required "
147
+ " @Route requires a path or a regex"
101
148
}
102
149
check(! isRegexRoute || routeAnnotation.name.isBlank()) {
103
- " @Route with regex can't be named"
150
+ " @Route having regex can't be named"
104
151
}
105
152
106
- val isComposable = annotations.any { it.shortName.asString() == " Composable" }
153
+ val isScreen = classKind != null
154
+ val isComposable = ! isScreen && annotations.any { it.shortName.asString() == " Composable" }
107
155
108
156
if (isRegexRoute) {
109
- check(! isComposable) {
157
+ check(! isComposable && ! isScreen ) {
110
158
// TODO: Add regex support to composable handle
111
- " Combining @Route(regex = ...) and @Composable are not supported for $qualifiedName "
159
+ " $qualifiedName has @Route(regex = ...) that cannot be applied to @Composable or Voyager Screen "
112
160
}
113
161
if (routeAnnotation.method.isBlank()) {
114
162
configureSpec
@@ -135,10 +183,12 @@ private class RoutingProcessor(
135
183
routeAnnotation.name.isBlank() -> " name = null"
136
184
else -> """ name = "${routeAnnotation.name} """"
137
185
}
138
- var memberName = handle
139
- if (composableEnabled && isComposable) {
140
- memberName = composable
186
+ val memberName = when {
187
+ isComposable -> composable
188
+ isScreen -> screen
189
+ else -> handle
141
190
}
191
+ logger.info(" >>>> transforming -> name: $named and member: $memberName " )
142
192
if (routeAnnotation.method.isBlank()) {
143
193
configureSpec
144
194
.beginControlFlow(" %M(path = %S, $named )" , memberName, routeAnnotation.path)
@@ -150,7 +200,7 @@ private class RoutingProcessor(
150
200
}
151
201
}
152
202
153
- val codeBlock = generateHandleBody(isRegexRoute, routeAnnotation, resolver, qualifiedName)
203
+ val codeBlock = generateHandleBody(isRegexRoute, routeAnnotation, resolver, qualifiedName, classKind )
154
204
155
205
configureSpec
156
206
.addCode(codeBlock)
@@ -161,16 +211,27 @@ private class RoutingProcessor(
161
211
isRegexRoute : Boolean ,
162
212
routeAnnotation : Route ,
163
213
resolver : Resolver ,
164
- qualifiedName : String?
214
+ qualifiedName : String ,
215
+ classKind : ClassKind ? ,
165
216
): CodeBlock {
166
- val funcMember = MemberName (packageName.asString(), simpleName.asString())
167
217
val funcBuilder = CodeBlock .builder()
168
218
val hasZeroOrOneParameter = parameters.size < 2
169
- if (hasZeroOrOneParameter) {
170
- funcBuilder.add(FUN_INVOKE_START , funcMember)
171
- } else {
172
- funcBuilder
173
- .addStatement(FUN_INVOKE_START , funcMember)
219
+ val funName = simpleName.asString()
220
+ val member: Any = when {
221
+ classKind != null -> ClassName (packageName.asString(), qualifiedName.split(" ." ).last())
222
+ else -> MemberName (packageName.asString(), funName)
223
+ }
224
+ val template = when (classKind) {
225
+ ClassKind .OBJECT -> FUN_TYPE_INVOKE
226
+ ClassKind .CLASS -> FUN_TYPE_INVOKE_START
227
+ else -> FUN_MEMBER_INVOKE_START
228
+ }
229
+ logger.info(" >>>> fun name: $funName -> template: $template -> member: $member " )
230
+ when {
231
+ classKind == ClassKind .OBJECT -> funcBuilder.addStatement(template, member)
232
+ hasZeroOrOneParameter -> funcBuilder.add(template, member)
233
+ else -> funcBuilder
234
+ .addStatement(template, member)
174
235
.indent()
175
236
}
176
237
@@ -202,15 +263,17 @@ private class RoutingProcessor(
202
263
}
203
264
}
204
265
205
- if (hasZeroOrOneParameter ) {
206
- funcBuilder.addStatement( FUN_INVOKE_END )
207
- } else {
208
- funcBuilder
209
- .unindent()
210
- .addStatement( FUN_INVOKE_END )
266
+ if (classKind == ClassKind . OBJECT ) {
267
+ return funcBuilder.build( )
268
+ }
269
+
270
+ if (hasZeroOrOneParameter. not ()) {
271
+ funcBuilder.unindent( )
211
272
}
212
273
213
- return funcBuilder.build()
274
+ return funcBuilder
275
+ .addStatement(FUN_INVOKE_END )
276
+ .build()
214
277
}
215
278
216
279
@OptIn(KspExperimental ::class )
@@ -336,7 +399,7 @@ private class RoutingProcessor(
336
399
FileSpec
337
400
.builder(
338
401
packageName = " dev.programadorthi.routing.generated" ,
339
- fileName = " ${ fileName} Routes " ,
402
+ fileName = fileName,
340
403
)
341
404
.addFileComment(" Generated by Kotlin Routing" )
342
405
.addFunction(this )
@@ -374,6 +437,7 @@ private class RoutingProcessor(
374
437
}
375
438
376
439
private companion object {
440
+ private val screen = MemberName (" dev.programadorthi.routing.voyager" , " screen" )
377
441
private val composable = MemberName (" dev.programadorthi.routing.compose" , " composable" )
378
442
private val handle = MemberName (" dev.programadorthi.routing.core" , " handle" )
379
443
private val routeMethod = MemberName (" dev.programadorthi.routing.core" , " RouteMethod" )
@@ -385,9 +449,17 @@ private class RoutingProcessor(
385
449
private const val CALL_PROPERTY_TEMPLATE = """ %L = %M.%L%L"""
386
450
private const val BODY_TEMPLATE = " %L = %M.%M()%L"
387
451
private const val FUN_INVOKE_END = " )"
388
- private const val FUN_INVOKE_START = " %M("
452
+ private const val FUN_MEMBER_INVOKE_START = " %M("
453
+ private const val FUN_TYPE_INVOKE = " %T"
454
+ private const val FUN_TYPE_INVOKE_START = " $FUN_TYPE_INVOKE ("
389
455
private const val PATH_TEMPLATE = """ %L = %M.parameters["%L"]%L"""
390
456
private const val TAILCARD_TEMPLATE = """ %L = %M.parameters.getAll("%L")%L"""
457
+
458
+ private const val FLAG_ROUTING_MODULE_NAME = " Routing_Module_Name"
459
+
460
+ private const val VOYAGER_SCREEN_QUALIFIED_NAME = " cafe.adriel.voyager.core.screen.Screen"
461
+
462
+ private const val CONSTRUCTOR_NAME = " <init>"
391
463
}
392
464
393
465
}
0 commit comments