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