1 /*
<lambda>null2  * Copyright 2025 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.appfunctions.compiler.processors
18 
19 import androidx.appfunctions.compiler.AppFunctionCompiler
20 import androidx.appfunctions.compiler.core.AnnotatedAppFunctions
21 import androidx.appfunctions.compiler.core.AppFunctionComponentRegistryGenerator
22 import androidx.appfunctions.compiler.core.AppFunctionComponentRegistryGenerator.AppFunctionComponent
23 import androidx.appfunctions.compiler.core.AppFunctionSymbolResolver
24 import androidx.appfunctions.compiler.core.IntrospectionHelper.APP_FUNCTION_FUNCTION_NOT_FOUND_EXCEPTION_CLASS
25 import androidx.appfunctions.compiler.core.IntrospectionHelper.AppFunctionComponentRegistryAnnotation
26 import androidx.appfunctions.compiler.core.IntrospectionHelper.AppFunctionContextClass
27 import androidx.appfunctions.compiler.core.IntrospectionHelper.AppFunctionInvokerClass
28 import androidx.appfunctions.compiler.core.IntrospectionHelper.ConfigurableAppFunctionFactoryClass
29 import androidx.appfunctions.compiler.core.toTypeName
30 import com.google.devtools.ksp.KspExperimental
31 import com.google.devtools.ksp.processing.CodeGenerator
32 import com.google.devtools.ksp.processing.Dependencies
33 import com.google.devtools.ksp.processing.Resolver
34 import com.google.devtools.ksp.processing.SymbolProcessor
35 import com.google.devtools.ksp.symbol.KSAnnotated
36 import com.google.devtools.ksp.symbol.KSFunctionDeclaration
37 import com.squareup.kotlinpoet.CodeBlock
38 import com.squareup.kotlinpoet.FileSpec
39 import com.squareup.kotlinpoet.FunSpec
40 import com.squareup.kotlinpoet.KModifier
41 import com.squareup.kotlinpoet.ParameterSpec
42 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
43 import com.squareup.kotlinpoet.PropertySpec
44 import com.squareup.kotlinpoet.TypeSpec
45 import com.squareup.kotlinpoet.asClassName
46 import com.squareup.kotlinpoet.asTypeName
47 import com.squareup.kotlinpoet.buildCodeBlock
48 
49 /**
50  * The processor to generate AppFunctionInvoker classes for AppFunction.
51  *
52  * For each AppFunction class, a corresponding AppFunctionInvoker implementation would be generated
53  * under the same package. For example,
54  * ```
55  * class NoteFunction: CreateNote {
56  *   @AppFunction
57  *   override suspend fun createNote(): Note { ... }
58  * }
59  * ```
60  *
61  * A corresponding `$NoteFunction_AppFunctionInvoker` class will be generated:
62  * ```
63  * class $$NoteFunction_AppFunctionInvoker: AppFunctionInvoker {
64  *   override val supportedFunctionIds: Set<String> = setOf(
65  *     "com.example.NoteFunction#createNote",
66  *   )
67  *
68  *   suspend fun unsafeInvoke(
69  *     appFunctionContext: AppFunctionContext,
70  *     functionIdentifier: String,
71  *     parameters: Map<String, Any?>,
72  *   ): Any? {
73  *     return when(functionIdentifier) {
74  *       "com.example.NoteFunction#createNote" -> {
75  *         ConfigurableAppFunctionFactory<NoteFunction>(
76  *           appFunctionContext.context
77  *         )
78  *         .createEnclosingClass(NoteFunction::class.java)
79  *         .createNote(
80  *           appFunctionContext,
81  *           parameters["createNoteParams"] as MyCreateNoteParams
82  *         )
83  *       }
84  *       else -> {
85  *         throw AppFunctionFunctionNotFoundException(...)
86  *       }
87  *     }
88  *   }
89  * }
90  * ```
91  * * **Important:** [androidx.appfunctions.compiler.processors.AppFunctionInvokerProcessor] will
92  * * process exactly once for each compilation unit to generate a single registry for looking up
93  * * all generated invokers within the compilation unit.
94  */
95 class AppFunctionInvokerProcessor(private val codeGenerator: CodeGenerator) : SymbolProcessor {
96 
97     private var hasProcessed = false
98 
99     @OptIn(KspExperimental::class)
100     override fun process(resolver: Resolver): List<KSAnnotated> {
101         if (hasProcessed) return emptyList()
102         hasProcessed = true
103 
104         val appFunctionSymbolResolver = AppFunctionSymbolResolver(resolver)
105         val appFunctionClasses = appFunctionSymbolResolver.resolveAnnotatedAppFunctions()
106         val generatedInvokerComponents =
107             buildList<AppFunctionComponent> {
108                 for (appFunctionClass in appFunctionClasses) {
109                     val invokerQualifiedName = generateAppFunctionInvokerClass(appFunctionClass)
110                     add(
111                         AppFunctionComponent(
112                             qualifiedName = invokerQualifiedName,
113                             sourceFiles = appFunctionClass.getSourceFiles(),
114                         )
115                     )
116                 }
117             }
118 
119         AppFunctionComponentRegistryGenerator(codeGenerator)
120             .generateRegistry(
121                 resolver.getModuleName().asString(),
122                 AppFunctionComponentRegistryAnnotation.Category.INVOKER,
123                 generatedInvokerComponents,
124             )
125         return emptyList()
126     }
127 
128     /**
129      * Generates an implementation of AppFunctionInvoker for [appFunctionClass].
130      *
131      * @return fully qualified name of the generated invoker implementation class.
132      */
133     private fun generateAppFunctionInvokerClass(appFunctionClass: AnnotatedAppFunctions): String {
134         val originalPackageName = appFunctionClass.classDeclaration.packageName.asString()
135         val originalClassName = appFunctionClass.classDeclaration.simpleName.asString()
136 
137         val invokerClassName = getAppFunctionInvokerClassName(originalClassName)
138         val invokerClassBuilder = TypeSpec.classBuilder(invokerClassName)
139         invokerClassBuilder.addSuperinterface(AppFunctionInvokerClass.CLASS_NAME)
140         invokerClassBuilder.addAnnotation(AppFunctionCompiler.GENERATED_ANNOTATION)
141         invokerClassBuilder.addProperty(buildSupportedFunctionIdsProperty(appFunctionClass))
142         invokerClassBuilder.addFunction(buildUnsafeInvokeFunction(appFunctionClass))
143 
144         val fileSpec =
145             FileSpec.builder(originalPackageName, invokerClassName)
146                 .addType(invokerClassBuilder.build())
147                 .build()
148         codeGenerator
149             .createNewFile(
150                 Dependencies(
151                     aggregating = true,
152                     sources = appFunctionClass.getSourceFiles().toTypedArray(),
153                 ),
154                 originalPackageName,
155                 invokerClassName
156             )
157             .bufferedWriter()
158             .use { fileSpec.writeTo(it) }
159 
160         return "${originalPackageName}.$invokerClassName"
161     }
162 
163     private fun buildSupportedFunctionIdsProperty(
164         annotatedAppFunctions: AnnotatedAppFunctions
165     ): PropertySpec {
166         val functionIds =
167             annotatedAppFunctions.appFunctionDeclarations.map { function ->
168                 annotatedAppFunctions.getAppFunctionIdentifier(function)
169             }
170         return PropertySpec.builder(
171                 AppFunctionInvokerClass.SUPPORTED_FUNCTION_IDS_PROPERTY_NAME,
172                 Set::class.asClassName().parameterizedBy(String::class.asTypeName()),
173             )
174             .addModifiers(KModifier.OVERRIDE)
175             .initializer(
176                 buildCodeBlock {
177                     addStatement("setOf(")
178                     indent()
179                     for (functionId in functionIds) {
180                         addStatement("%S,", functionId)
181                     }
182                     unindent()
183                     add(")")
184                 }
185             )
186             .build()
187     }
188 
189     private fun buildUnsafeInvokeFunction(annotatedAppFunctions: AnnotatedAppFunctions): FunSpec {
190         val contextSpec =
191             ParameterSpec.builder(
192                     AppFunctionInvokerClass.UnsafeInvokeMethod.APPLICATION_CONTEXT_PARAM_NAME,
193                     AppFunctionContextClass.CLASS_NAME
194                 )
195                 .build()
196         val functionIdentifierSpec =
197             ParameterSpec.builder(
198                     AppFunctionInvokerClass.UnsafeInvokeMethod.FUNCTION_ID_PARAM_NAME,
199                     String::class
200                 )
201                 .build()
202         val functionParametersSpec =
203             ParameterSpec.builder(
204                     AppFunctionInvokerClass.UnsafeInvokeMethod.PARAMETERS_PARAM_NAME,
205                     Map::class.asClassName()
206                         .parameterizedBy(
207                             String::class.asTypeName(),
208                             Any::class.asTypeName().copy(nullable = true),
209                         ),
210                 )
211                 .build()
212         return FunSpec.builder(AppFunctionInvokerClass.UnsafeInvokeMethod.METHOD_NAME)
213             .addModifiers(KModifier.SUSPEND)
214             .addModifiers(KModifier.OVERRIDE)
215             .addParameter(contextSpec)
216             .addParameter(functionIdentifierSpec)
217             .addParameter(functionParametersSpec)
218             .returns(Any::class.asTypeName().copy(nullable = true))
219             .addCode(
220                 buildCodeBlock {
221                     addStatement("val result: Any? = when (${functionIdentifierSpec.name}) {")
222                     indent()
223                     for (appFunction in annotatedAppFunctions.appFunctionDeclarations) {
224                         appendInvocationBranchStatement(
225                             annotatedAppFunctions,
226                             appFunction,
227                             contextSpec,
228                             functionParametersSpec,
229                         )
230                     }
231                     unindent()
232                     add(
233                         """
234                 else -> {
235                   throw %T("Unable to find ${'$'}${functionIdentifierSpec.name}")
236                 }
237               }
238               return result
239               """
240                             .trimIndent(),
241                         APP_FUNCTION_FUNCTION_NOT_FOUND_EXCEPTION_CLASS,
242                     )
243                 }
244             )
245             .build()
246     }
247 
248     /**
249      * Appends a branch statement for [appFunction] within [annotatedAppFunctions].
250      *
251      * This append the code block like
252      *
253      * ```
254      * "com.example.TestFunction#test" -> {
255      *   ConfigurableAppFunctionFactory<TestFunction>(applicationContext.context)
256      *     .createEnclosingClass(TestFunction::class.java)
257      *     .test(appFunctionContext, parameters["param1"] as Int)
258      * }
259      * ```
260      */
261     private fun CodeBlock.Builder.appendInvocationBranchStatement(
262         annotatedAppFunctions: AnnotatedAppFunctions,
263         appFunction: KSFunctionDeclaration,
264         contextSpec: ParameterSpec,
265         functionParametersSpec: ParameterSpec,
266     ) {
267         val functionParameterStatement =
268             appFunction.getAppFunctionParametersStatement(contextSpec, functionParametersSpec)
269         val formatStringMap =
270             mapOf<String, Any>(
271                 "function_id" to annotatedAppFunctions.getAppFunctionIdentifier(appFunction),
272                 "factory_class" to ConfigurableAppFunctionFactoryClass.CLASS_NAME,
273                 "enclosing_class" to annotatedAppFunctions.getEnclosingClassName(),
274                 "context_param" to contextSpec.name,
275                 "context_property" to AppFunctionContextClass.CONTEXT_PROPERTY_NAME,
276                 "create_method" to
277                     ConfigurableAppFunctionFactoryClass.CreateEnclosingClassMethod.METHOD_NAME,
278                 "function_name" to appFunction.simpleName.asString(),
279                 "parameters" to functionParameterStatement
280             )
281         addNamed("\"%function_id:L\" -> {\n", formatStringMap)
282         indent()
283         addNamed("%factory_class:T<%enclosing_class:T>(\n", formatStringMap)
284         indent()
285         addNamed("%context_param:L.%context_property:L\n", formatStringMap)
286         unindent()
287         add(")\n")
288         addNamed(".%create_method:L(%enclosing_class:T::class.java)\n", formatStringMap)
289         addNamed(".%function_name:L(%parameters:L)\n", formatStringMap)
290         unindent()
291         add("}\n")
292     }
293 
294     private fun KSFunctionDeclaration.getAppFunctionParametersStatement(
295         contextSpec: ParameterSpec,
296         functionParametersSpec: ParameterSpec,
297     ): String {
298         val args =
299             buildList<String> {
300                 for ((index, value) in parameters.withIndex()) {
301                     if (index == 0) {
302                         // The first parameter is always AppFunctionContext.
303                         add(contextSpec.name)
304                     } else {
305                         val parameterName = checkNotNull(value.name).asString()
306                         val parameterType = value.type.toTypeName()
307                         add(
308                             "${functionParametersSpec.name}[\"${parameterName}\"] as $parameterType"
309                         )
310                     }
311                 }
312             }
313         return args.joinToString(separator = ", ")
314     }
315 
316     private fun getAppFunctionInvokerClassName(functionClassName: String): String {
317         return "$%s_AppFunctionInvoker".format(functionClassName)
318     }
319 }
320