-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Support a new @DesignModuleClass annotation to declare a class that holds customizations. These classes can be used to consolidate customizations into a single parameter in a @DesignComponent function, as well as nested in another @DesignModuleClass. When declaring a @DesignModuleClass, use the @DesignProperty and @DesignVariantProperty annotations instead of the normal @Design and @DesignVariant properties. This is needed because of a bug in KSP that prevents annotations from being used with multiple targets. Once fixed, we can remove @DesignProperty and @DesignVariantProperty. A @DesignModuleClass parameter can be specified in a @DesignComponent function with the @DesignModule annotation. A @DesignModuleClass class property can be specified in a @DesignModuleClass class with the @DesignModuleProperty annotation. Once the KSP bug is fixed, we can remove the @DesignModuleProperty annotation. This change creates a new source file BuildModuleClass.kt to process @DesignModuleClass classes. Each @DesignModuleClass will generate a new file associated with that class that has an extension function for the class. The extension function creates a CustomizationContext object, populates customizations for it, and then returns it. Node queries and ignored images are also tracked so that any @DesignComponent function that uses modules will generate the appropriate queries() and ignoredImages() functions. This change also cleans up some of the code in BuildProcessor.kt and moves common functionality into a separate file.
- Loading branch information
Showing
8 changed files
with
767 additions
and
198 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
251 changes: 251 additions & 0 deletions
251
codegen/src/main/kotlin/com/android/designcompose/codegen/BuildModuleClass.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,251 @@ | ||
/* | ||
* Copyright 2023 Google LLC | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.android.designcompose.codegen | ||
|
||
import com.google.devtools.ksp.processing.CodeGenerator | ||
import com.google.devtools.ksp.processing.KSPLogger | ||
import com.google.devtools.ksp.processing.Resolver | ||
import com.google.devtools.ksp.symbol.ClassKind | ||
import com.google.devtools.ksp.symbol.KSClassDeclaration | ||
import com.google.devtools.ksp.symbol.KSPropertyDeclaration | ||
import com.google.devtools.ksp.symbol.KSVisitorVoid | ||
import java.io.OutputStream | ||
|
||
// Helper class that keeps track of query node names and ignored image node names from properties of | ||
// @DesignModuleClass classes. This class stores hashes of these node names indexed by class name | ||
// so that @DesignComponent functions that use @DesignModule objects can generate the proper | ||
// queries() and ignoredImages() functions by querying this class. | ||
class ModuleNodeNameTable { | ||
private val queries: HashMap<String, HashSet<String>> = HashMap() | ||
private val ignoredImages: HashMap<String, HashSet<String>> = HashMap() | ||
// Maps a module class name to a set of module class names that it references. | ||
private val nestedModules: HashMap<String, HashSet<String>> = HashMap() | ||
|
||
// Return a set of node queries for the module named className. This includes any queries from | ||
// nested modules as well. | ||
fun getNodeQueries(className: String): HashSet<String> { | ||
val allQueries = HashSet<String>() | ||
// Call function to recursively add queries | ||
addModuleNodeQueries(className, allQueries) | ||
return allQueries | ||
} | ||
|
||
// Add node queries associated with className, then recurse on any classes that className | ||
// references in its properties. | ||
private fun addModuleNodeQueries(className: String, allQueries: HashSet<String>) { | ||
// Add queries for the specified class | ||
val classQueries = queries[className] | ||
classQueries?.let { allQueries.addAll(it) } | ||
// Recurse on any modules that className references | ||
nestedModules[className]?.forEach { addModuleNodeQueries(it, allQueries) } | ||
} | ||
|
||
// Return a set of ignored image node names for the module named className. This includes any | ||
// node names from nested modules as well. | ||
fun getIgnoredImages(className: String): HashSet<String>? { | ||
val allIgnored = HashSet<String>() | ||
// Call function to recursively add ignored image nodes | ||
addModuleIgnoredImages(className, allIgnored) | ||
return allIgnored | ||
} | ||
|
||
// Add ignored node names associated with className, then recurse on any classes that className | ||
// references in its properties. | ||
private fun addModuleIgnoredImages(className: String, allIgnored: HashSet<String>) { | ||
// Add ignored images for the specified class | ||
val classIgnored = ignoredImages[className] | ||
classIgnored?.let { allIgnored.addAll(it) } | ||
// Recurse on any modules that className references | ||
nestedModules[className]?.forEach { addModuleIgnoredImages(it, allIgnored) } | ||
} | ||
|
||
internal fun addNodeToQueries(className: String, nodeName: String) { | ||
if (!queries.containsKey(className)) queries[className] = HashSet() | ||
queries[className]?.add(nodeName) | ||
} | ||
|
||
internal fun addIgnoredImage(className: String, nodeName: String) { | ||
if (!ignoredImages.containsKey(className)) ignoredImages[className] = HashSet() | ||
ignoredImages[className]?.add(nodeName) | ||
} | ||
|
||
internal fun addNestedModule(className: String, moduleName: String, logger: KSPLogger) { | ||
if (!nestedModules.containsKey(className)) nestedModules[className] = HashSet() | ||
|
||
// Check that there is no recursive nesting of module classes | ||
if (hasRecursiveNesting(className, moduleName)) { | ||
logger.error("Recursive nesting detected between $className, $moduleName") | ||
return | ||
} | ||
|
||
nestedModules[className]?.add(moduleName) | ||
} | ||
|
||
private fun hasRecursiveNesting(className: String, moduleName: String): Boolean { | ||
if (className == moduleName) return true | ||
val nested = nestedModules[moduleName] | ||
nested?.forEach { if (hasRecursiveNesting(className, it)) return true } | ||
return false | ||
} | ||
} | ||
|
||
// For each @DesignModuleClass annotation, create a DesignModuleVisitor class to process the | ||
// annotation and associated class. | ||
internal fun processDesignModulesClasses( | ||
resolver: Resolver, | ||
codeGenerator: CodeGenerator, | ||
logger: KSPLogger | ||
): ModuleNodeNameTable { | ||
val nodeNameTable = ModuleNodeNameTable() | ||
|
||
val moduleClasses = | ||
resolver | ||
.getSymbolsWithAnnotation("com.android.designcompose.annotation.DesignModuleClass") | ||
.filterIsInstance<KSClassDeclaration>() // Making sure we take only class declarations. | ||
moduleClasses.forEach { | ||
it.accept( | ||
DesignModuleVisitor( | ||
codeGenerator, | ||
logger, | ||
nodeNameTable, | ||
), | ||
Unit | ||
) | ||
} | ||
return nodeNameTable | ||
} | ||
|
||
// This class generates a file for a @DesignModuleClass class and creates an extension function | ||
// for the class that sets up all the customizations used in the class. | ||
private class DesignModuleVisitor( | ||
private val codeGenerator: CodeGenerator, | ||
private val logger: KSPLogger, | ||
private val nodeNameTable: ModuleNodeNameTable, | ||
) : KSVisitorVoid() { | ||
private lateinit var out: OutputStream | ||
private lateinit var className: String | ||
private lateinit var qualifiedclassName: String | ||
|
||
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) { | ||
className = classDeclaration.simpleName.asString() | ||
qualifiedclassName = classDeclaration.qualifiedName?.asString() ?: return | ||
val packageName = classDeclaration.packageName.asString() | ||
|
||
if (classDeclaration.classKind != ClassKind.CLASS) { | ||
logger.error( | ||
"@DesignModuleClass must be used with a class. ${classDeclaration.classKind.type} not supported" | ||
) | ||
return | ||
} | ||
|
||
// Create a new file for each @DesignDoc annotation | ||
out = | ||
createNewFile( | ||
codeGenerator, | ||
className, | ||
packageName, | ||
setOf(classDeclaration.containingFile!!), | ||
) | ||
|
||
// Create the customizations() extension function and boilerplate code | ||
out += "fun $className.customizations(): CustomizationContext {\n" | ||
out += " val customizations = CustomizationContext()\n" | ||
out += " val variantProperties = HashMap<String, String>()\n" | ||
|
||
// Iterate through the properties and generate code to set a customization for each | ||
val properties = classDeclaration.declarations | ||
properties.forEach { property -> property.accept(this, data) } | ||
|
||
out += " customizations.setVariantProperties(variantProperties)\n" | ||
out += " return customizations\n" | ||
out += "}\n" | ||
|
||
out.close() | ||
} | ||
|
||
override fun visitPropertyDeclaration(property: KSPropertyDeclaration, data: Unit) { | ||
// Skip non-annotated properties | ||
if (property.annotations.asIterable().toList().isEmpty()) return | ||
|
||
val propertyName = property.simpleName.asString() | ||
val variantProperty = property.getVariantProperty() | ||
variantProperty?.let { | ||
out += " variantProperties[\"$variantProperty\"] = $propertyName.name\n" | ||
// Add any variants encountered to the set of node queries | ||
nodeNameTable.addNodeToQueries(qualifiedclassName, it) | ||
return | ||
} | ||
|
||
val customizationType = property.customizationType() | ||
|
||
// For modules, add a nested module reference from this module to the property. | ||
// Output a line to merge the customizations into ours | ||
if (customizationType == CustomizationType.Module) { | ||
property.type.resolve().declaration.qualifiedName?.let { | ||
nodeNameTable.addNestedModule(qualifiedclassName, it.asString(), logger) | ||
} | ||
out += " customizations.mergeFrom($propertyName.customizations())\n" | ||
return | ||
} | ||
|
||
// Get the node name in the Design annotation. If it can't be found, an annotation that is | ||
// not part of DesignCompose was used, so ignore and return. | ||
val nodeName = property.getAnnotatedNodeName() ?: return | ||
|
||
// Add to set of ignored images | ||
if (property.customizationType().shouldIgnoreImage()) | ||
nodeNameTable.addIgnoredImage(qualifiedclassName, nodeName) | ||
|
||
// Create a line of code to set the customization based on the customization type | ||
when (customizationType) { | ||
CustomizationType.Text -> | ||
out += " customizations.setText(\"$nodeName\", $propertyName)\n" | ||
CustomizationType.TextFunction -> | ||
out += " customizations.setTextFunction(\"$nodeName\", $propertyName)\n" | ||
CustomizationType.Image -> | ||
out += " customizations.setImage(\"$nodeName\", $propertyName)\n" | ||
CustomizationType.Brush -> | ||
out += " customizations.setBrush(\"$nodeName\", $propertyName)\n" | ||
CustomizationType.BrushFunction -> | ||
out += " customizations.setBrushFunction(\"$nodeName\", $propertyName)\n" | ||
CustomizationType.Modifier -> | ||
out += " customizations.setModifier(\"$nodeName\", $propertyName)\n" | ||
CustomizationType.TapCallback -> | ||
out += " customizations.setTapCallback(\"$nodeName\", $propertyName)\n" | ||
CustomizationType.ContentReplacement -> | ||
out += " customizations.setContent(\"$nodeName\", $propertyName)\n" | ||
CustomizationType.ComponentReplacement -> | ||
out += " customizations.setComponent(\"$nodeName\", $propertyName)\n" | ||
CustomizationType.ListContent -> | ||
out += " customizations.setListContent(\"$nodeName\", $propertyName)\n" | ||
CustomizationType.ImageWithContext -> | ||
out += " customizations.setImageWithContext(\"$nodeName\", $propertyName)\n" | ||
CustomizationType.Visibility -> | ||
out += " customizations.setVisible(\"$nodeName\", $propertyName)\n" | ||
CustomizationType.TextStyle -> | ||
out += " customizations.setTextStyle(\"$nodeName\", $propertyName)\n" | ||
CustomizationType.Meter -> | ||
out += " customizations.setMeterValue(\"$nodeName\", $propertyName)\n" | ||
CustomizationType.MeterFunction -> | ||
out += " customizations.setMeterFunction(\"$nodeName\", $propertyName)\n" | ||
else -> | ||
logger.error( | ||
"Invalid parameter type ${property.type.typeString()} property \"$propertyName\"" | ||
) | ||
} | ||
} | ||
} |
Oops, something went wrong.