-
Notifications
You must be signed in to change notification settings - Fork 6.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a basic KSP Symbol Processor for Glide
This library only implements support for basic configuration of Glide. Like the Java version it can detect and merge multiple LibraryGlideModules and a single AppGlideModule. The merged output (GeneratedAppGlideModule) will then be called via reflection to configure Glide when Glide is first used. Unlike the Java version this processor has no support for: 1. Extensions 2. Including or Excluding LibraryGlideModules that are added via AndroidManifest registration 3. Generated Glide, RequestOptions, RequestBuilder, and RequestManager overrides. 4. Excluding LibraryGlideModules that are added via annotations I suspect very few people use the first two missing features and so, barring major objections, those features will be only available via the Java processor and in the very long run, deprecated. Kotlin extension functions can provide the same value with less magic and complexity as Extensions. AndroidManifest registrtion has been deprecated for years. For #3 ideally we do not support these generated overrides either. Their only real purpose was to expose the functionality provided by Extensions. The one caveat is that our documentation has encouraged their use in the past. If we remove support instantly, it may complicate migration. I will support #4, but in a future change. This one is large enough already. PiperOrigin-RevId: 461943092
- Loading branch information
1 parent
6640376
commit c35ad13
Showing
15 changed files
with
889 additions
and
3 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
plugins { | ||
id 'org.jetbrains.kotlin.jvm' | ||
id 'com.google.devtools.ksp' | ||
} | ||
|
||
dependencies { | ||
implementation("com.squareup:kotlinpoet:1.12.0") | ||
implementation project(":annotation") | ||
implementation project(":glide") | ||
implementation 'com.google.devtools.ksp:symbol-processing-api:1.7.0-1.0.6' | ||
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.0.0") | ||
implementation("com.google.auto.service:auto-service-annotations:1.0.1") | ||
} | ||
|
||
apply from: "${rootProject.projectDir}/scripts/upload.gradle" |
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,6 @@ | ||
kotlin.code.style=official | ||
|
||
POM_NAME=Glide KSP Annotation Processor | ||
POM_ARTIFACT_ID=ksp | ||
POM_PACKAGING=jar | ||
POM_DESCRIPTION=Glide's KSP based annotation processor. Should be included in all Kotlin applications and libraries that use Glide's modules for configuration and do not require the more advanced features of the Java based compiler. |
213 changes: 213 additions & 0 deletions
213
annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/AppGlideModules.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,213 @@ | ||
package com.bumptech.glide.annotation.ksp | ||
|
||
import com.bumptech.glide.annotation.Excludes | ||
import com.google.devtools.ksp.getConstructors | ||
import com.google.devtools.ksp.processing.Resolver | ||
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment | ||
import com.google.devtools.ksp.symbol.KSAnnotation | ||
import com.google.devtools.ksp.symbol.KSClassDeclaration | ||
import com.google.devtools.ksp.symbol.KSDeclaration | ||
import com.google.devtools.ksp.symbol.KSFunctionDeclaration | ||
import com.squareup.kotlinpoet.AnnotationSpec | ||
import com.squareup.kotlinpoet.ClassName | ||
import com.squareup.kotlinpoet.FileSpec | ||
import com.squareup.kotlinpoet.FunSpec | ||
import com.squareup.kotlinpoet.KModifier | ||
import com.squareup.kotlinpoet.ParameterSpec | ||
import com.squareup.kotlinpoet.TypeSpec | ||
import kotlin.reflect.KClass | ||
|
||
// This class is visible only for testing | ||
// TODO(b/174783094): Add @VisibleForTesting when internal is supported. | ||
object AppGlideModuleConstants { | ||
// This variable is visible only for testing | ||
// TODO(b/174783094): Add @VisibleForTesting when internal is supported. | ||
const val INVALID_MODULE_MESSAGE = | ||
"Your AppGlideModule must have at least one constructor that has either no parameters or " + | ||
"accepts only a Context." | ||
|
||
private const val CONTEXT_NAME = "Context" | ||
internal const val CONTEXT_PACKAGE = "android.content" | ||
internal const val GLIDE_PACKAGE_NAME = "com.bumptech.glide" | ||
internal const val CONTEXT_QUALIFIED_NAME = "$CONTEXT_PACKAGE.$CONTEXT_NAME" | ||
internal const val GENERATED_ROOT_MODULE_PACKAGE_NAME = GLIDE_PACKAGE_NAME | ||
|
||
internal val CONTEXT_CLASS_NAME = ClassName(CONTEXT_PACKAGE, CONTEXT_NAME) | ||
} | ||
|
||
internal data class AppGlideModuleData( | ||
val name: ClassName, | ||
val constructor: Constructor, | ||
) { | ||
internal data class Constructor(val hasContext: Boolean) | ||
} | ||
|
||
/** | ||
* Given a [com.bumptech.glide.module.AppGlideModule] class declaration provided by the developer, | ||
* validate the class and produce a fully parsed [AppGlideModuleData] that allows us to generate a | ||
* valid [com.bumptech.glide.GeneratedAppGlideModule] implementation without further introspection. | ||
*/ | ||
internal class AppGlideModuleParser( | ||
private val environment: SymbolProcessorEnvironment, | ||
private val resolver: Resolver, | ||
private val appGlideModuleClass: KSClassDeclaration, | ||
) { | ||
|
||
fun parseAppGlideModule(): AppGlideModuleData { | ||
val constructor = parseAppGlideModuleConstructorOrThrow() | ||
val name = ClassName.bestGuess(appGlideModuleClass.qualifiedName!!.asString()) | ||
|
||
return AppGlideModuleData(name = name, constructor = constructor) | ||
} | ||
|
||
private fun parseAppGlideModuleConstructorOrThrow(): AppGlideModuleData.Constructor { | ||
val hasEmptyConstructors = appGlideModuleClass.getConstructors().any { it.parameters.isEmpty() } | ||
val hasContextParamOnlyConstructor = | ||
appGlideModuleClass.getConstructors().any { it.hasSingleContextParameter() } | ||
if (!hasEmptyConstructors && !hasContextParamOnlyConstructor) { | ||
throw InvalidGlideSourceException(AppGlideModuleConstants.INVALID_MODULE_MESSAGE) | ||
} | ||
return AppGlideModuleData.Constructor(hasContextParamOnlyConstructor) | ||
} | ||
|
||
private fun KSFunctionDeclaration.hasSingleContextParameter() = | ||
parameters.size == 1 && | ||
AppGlideModuleConstants.CONTEXT_QUALIFIED_NAME == | ||
parameters.single().type.resolve().declaration.qualifiedName?.asString() | ||
|
||
private data class IndexFilesAndLibraryModuleNames( | ||
val indexFiles: List<KSDeclaration>, | ||
val libraryModuleNames: List<String>, | ||
) | ||
|
||
private fun extractGlideModulesFromIndexAnnotation( | ||
index: KSDeclaration, | ||
): List<String> { | ||
val indexAnnotation: KSAnnotation = index.atMostOneIndexAnnotation() ?: return emptyList() | ||
environment.logger.info("Found index annotation: $indexAnnotation") | ||
return indexAnnotation.getModuleArgumentValues().toList() | ||
} | ||
|
||
private fun KSAnnotation.getModuleArgumentValues(): List<String> { | ||
val result = arguments.find { it.name?.getShortName().equals("modules") }?.value | ||
if (result is List<*> && result.all { it is String }) { | ||
@Suppress("UNCHECKED_CAST") return result as List<String> | ||
} | ||
throw InvalidGlideSourceException("Found an invalid internal Glide index: $this") | ||
} | ||
|
||
private fun KSDeclaration.atMostOneIndexAnnotation() = atMostOneAnnotation(Index::class) | ||
|
||
private fun KSDeclaration.atMostOneExcludesAnnotation() = atMostOneAnnotation(Excludes::class) | ||
|
||
private fun KSDeclaration.atMostOneAnnotation( | ||
annotation: KClass<out Annotation>, | ||
): KSAnnotation? { | ||
val matchingAnnotations: List<KSAnnotation> = | ||
annotations | ||
.filter { | ||
annotation.qualifiedName?.equals( | ||
it.annotationType.resolve().declaration.qualifiedName?.asString() | ||
) | ||
?: false | ||
} | ||
.toList() | ||
if (matchingAnnotations.size > 1) { | ||
throw InvalidGlideSourceException( | ||
"""Expected 0 or 1 $annotation annotations on ${this.qualifiedName}, but found: | ||
${matchingAnnotations.size}""" | ||
) | ||
} | ||
return matchingAnnotations.singleOrNull() | ||
} | ||
} | ||
|
||
/** | ||
* Given valid [AppGlideModuleData], writes a Kotlin implementation of | ||
* [com.bumptech.glide.GeneratedAppGlideModule]. | ||
* | ||
* This class should obtain all of its data from [AppGlideModuleData] and should not interact with | ||
* any ksp classes. In the long run, the restriction may allow us to share code between the Java and | ||
* Kotlin processors. | ||
*/ | ||
internal class AppGlideModuleGenerator(private val appGlideModuleData: AppGlideModuleData) { | ||
|
||
fun generateAppGlideModule(): FileSpec { | ||
val generatedAppGlideModuleClass = generateAppGlideModuleClass(appGlideModuleData) | ||
return FileSpec.builder( | ||
AppGlideModuleConstants.GLIDE_PACKAGE_NAME, | ||
"GeneratedAppGlideModuleImpl" | ||
) | ||
.addType(generatedAppGlideModuleClass) | ||
.build() | ||
} | ||
|
||
private fun generateAppGlideModuleClass( | ||
data: AppGlideModuleData, | ||
): TypeSpec { | ||
return TypeSpec.classBuilder("GeneratedAppGlideModuleImpl") | ||
.superclass( | ||
ClassName( | ||
AppGlideModuleConstants.GENERATED_ROOT_MODULE_PACKAGE_NAME, | ||
"GeneratedAppGlideModule" | ||
) | ||
) | ||
.addModifiers(KModifier.INTERNAL) | ||
.addProperty("appGlideModule", data.name, KModifier.PRIVATE) | ||
.primaryConstructor(generateConstructor(data)) | ||
.addFunction(generateRegisterComponents()) | ||
.addFunction(generateApplyOptions()) | ||
.addFunction(generateManifestParsingDisabled()) | ||
.build() | ||
} | ||
|
||
private fun generateConstructor(data: AppGlideModuleData): FunSpec { | ||
val contextParameterBuilder = | ||
ParameterSpec.builder("context", AppGlideModuleConstants.CONTEXT_CLASS_NAME) | ||
if (!data.constructor.hasContext) { | ||
contextParameterBuilder.addAnnotation( | ||
AnnotationSpec.builder(ClassName("kotlin", "Suppress")) | ||
.addMember("%S", "UNUSED_VARIABLE") | ||
.build() | ||
) | ||
} | ||
|
||
return FunSpec.constructorBuilder() | ||
.addParameter(contextParameterBuilder.build()) | ||
.addStatement( | ||
"appGlideModule = %T(${if (data.constructor.hasContext) "context" else ""})", | ||
data.name | ||
) | ||
.build() | ||
|
||
// TODO(judds): Log the discovered modules here. | ||
} | ||
|
||
// TODO(judds): call registerComponents on LibraryGlideModules here. | ||
private fun generateRegisterComponents() = | ||
FunSpec.builder("registerComponents") | ||
.addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) | ||
.addParameter("context", AppGlideModuleConstants.CONTEXT_CLASS_NAME) | ||
.addParameter("glide", ClassName(AppGlideModuleConstants.GLIDE_PACKAGE_NAME, "Glide")) | ||
.addParameter("registry", ClassName(AppGlideModuleConstants.GLIDE_PACKAGE_NAME, "Registry")) | ||
.addStatement("appGlideModule.registerComponents(context, glide, registry)") | ||
.build() | ||
|
||
private fun generateApplyOptions() = | ||
FunSpec.builder("applyOptions") | ||
.addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) | ||
.addParameter("context", AppGlideModuleConstants.CONTEXT_CLASS_NAME) | ||
.addParameter( | ||
"builder", | ||
ClassName(AppGlideModuleConstants.GLIDE_PACKAGE_NAME, "GlideBuilder") | ||
) | ||
.addStatement("appGlideModule.applyOptions(context, builder)") | ||
.build() | ||
|
||
private fun generateManifestParsingDisabled() = | ||
FunSpec.builder("isManifestParsingEnabled") | ||
.addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) | ||
.returns(Boolean::class) | ||
.addStatement("return false") | ||
.build() | ||
} |
150 changes: 150 additions & 0 deletions
150
annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/GlideSymbolProcessor.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,150 @@ | ||
package com.bumptech.glide.annotation.ksp | ||
|
||
import com.google.devtools.ksp.processing.Dependencies | ||
import com.google.devtools.ksp.processing.Resolver | ||
import com.google.devtools.ksp.processing.SymbolProcessor | ||
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment | ||
import com.google.devtools.ksp.symbol.KSAnnotated | ||
import com.google.devtools.ksp.symbol.KSClassDeclaration | ||
import com.google.devtools.ksp.symbol.KSFile | ||
import com.google.devtools.ksp.validate | ||
import com.squareup.kotlinpoet.FileSpec | ||
|
||
/** | ||
* Glide's KSP annotation processor. | ||
* | ||
* This class recognizes and parses [com.bumptech.glide.module.AppGlideModule]s and | ||
* [com.bumptech.glide.module.LibraryGlideModule]s that are annotated with | ||
* [com.bumptech.glide.annotation.GlideModule]. | ||
* | ||
* `LibraryGlideModule`s are merged into indexes, or classes generated in Glide's package. When a | ||
* `AppGlideModule` is found, we then generate Glide's configuration so that it calls the | ||
* `AppGlideModule` and anay included `LibraryGlideModules`. Using indexes allows us to process | ||
* `LibraryGlideModules` in multiple rounds and/or libraries. | ||
* | ||
* TODO(b/239086146): Finish implementing the behavior described here. | ||
*/ | ||
class GlideSymbolProcessor(private val environment: SymbolProcessorEnvironment) : SymbolProcessor { | ||
var isAppGlideModuleGenerated = false | ||
|
||
override fun process(resolver: Resolver): List<KSAnnotated> { | ||
val symbols = resolver.getSymbolsWithAnnotation("com.bumptech.glide.annotation.GlideModule") | ||
val (validSymbols, invalidSymbols) = symbols.partition { it.validate() }.toList() | ||
return try { | ||
processChecked(resolver, symbols, validSymbols, invalidSymbols) | ||
} catch (e: InvalidGlideSourceException) { | ||
environment.logger.error(e.userMessage) | ||
invalidSymbols | ||
} | ||
} | ||
|
||
private fun processChecked( | ||
resolver: Resolver, | ||
symbols: Sequence<KSAnnotated>, | ||
validSymbols: List<KSAnnotated>, | ||
invalidSymbols: List<KSAnnotated>, | ||
): List<KSAnnotated> { | ||
environment.logger.logging("Found symbols, valid: $validSymbols, invalid: $invalidSymbols") | ||
|
||
val (appGlideModules, libraryGlideModules) = extractGlideModules(validSymbols) | ||
|
||
if (libraryGlideModules.size + appGlideModules.size != validSymbols.count()) { | ||
val invalidModules = | ||
symbols | ||
.filter { !libraryGlideModules.contains(it) && !appGlideModules.contains(it) } | ||
.map { it.location.toString() } | ||
.toList() | ||
|
||
throw InvalidGlideSourceException( | ||
GlideSymbolProcessorConstants.INVALID_ANNOTATED_CLASS.format(invalidModules) | ||
) | ||
} | ||
|
||
if (appGlideModules.size > 1) { | ||
throw InvalidGlideSourceException( | ||
GlideSymbolProcessorConstants.SINGLE_APP_MODULE_ERROR.format(appGlideModules) | ||
) | ||
} | ||
|
||
environment.logger.logging( | ||
"Found AppGlideModules: $appGlideModules, LibraryGlideModules: $libraryGlideModules" | ||
) | ||
// TODO(judds): Add support for parsing LibraryGlideModules here. | ||
|
||
if (appGlideModules.isNotEmpty()) { | ||
parseAppGlideModuleAndWriteGeneratedAppGlideModule(resolver, appGlideModules.single()) | ||
} | ||
|
||
return invalidSymbols | ||
} | ||
|
||
private fun parseAppGlideModuleAndWriteGeneratedAppGlideModule( | ||
resolver: Resolver, | ||
appGlideModule: KSClassDeclaration, | ||
) { | ||
val appGlideModuleData = | ||
AppGlideModuleParser(environment, resolver, appGlideModule).parseAppGlideModule() | ||
val appGlideModuleGenerator = AppGlideModuleGenerator(appGlideModuleData) | ||
val appGlideModuleFileSpec: FileSpec = appGlideModuleGenerator.generateAppGlideModule() | ||
writeFile( | ||
appGlideModuleFileSpec, | ||
listOfNotNull(appGlideModule.containingFile), | ||
) | ||
} | ||
|
||
private fun writeFile(file: FileSpec, sources: List<KSFile>) { | ||
environment.codeGenerator | ||
.createNewFile( | ||
Dependencies( | ||
aggregating = false, | ||
sources = sources.toTypedArray(), | ||
), | ||
file.packageName, | ||
file.name | ||
) | ||
.writer() | ||
.use { file.writeTo(it) } | ||
|
||
environment.logger.logging("Wrote file: $file") | ||
} | ||
|
||
internal data class GlideModules( | ||
val appModules: List<KSClassDeclaration>, | ||
val libraryModules: List<KSClassDeclaration>, | ||
) | ||
|
||
private fun extractGlideModules(annotatedModules: List<KSAnnotated>): GlideModules { | ||
val appAndLibraryModuleNames = listOf(APP_MODULE_QUALIFIED_NAME, LIBRARY_MODULE_QUALIFIED_NAME) | ||
val modulesBySuperType: Map<String?, List<KSClassDeclaration>> = | ||
annotatedModules.filterIsInstance<KSClassDeclaration>().groupBy { classDeclaration -> | ||
appAndLibraryModuleNames.singleOrNull { classDeclaration.hasSuperType(it) } | ||
} | ||
|
||
val (appModules, libraryModules) = | ||
appAndLibraryModuleNames.map { modulesBySuperType[it] ?: emptyList() } | ||
return GlideModules(appModules, libraryModules) | ||
} | ||
|
||
private fun KSClassDeclaration.hasSuperType(superTypeQualifiedName: String) = | ||
superTypes | ||
.map { superType -> superType.resolve().declaration.qualifiedName!!.asString() } | ||
.contains(superTypeQualifiedName) | ||
} | ||
|
||
// This class is visible only for testing | ||
// TODO(b/174783094): Add @VisibleForTesting when internal is supported. | ||
object GlideSymbolProcessorConstants { | ||
// This variable is visible only for testing | ||
// TODO(b/174783094): Add @VisibleForTesting when internal is supported. | ||
val PACKAGE_NAME: String = GlideSymbolProcessor::class.java.`package`.name | ||
const val SINGLE_APP_MODULE_ERROR = "You can have at most one AppGlideModule, but found: %s" | ||
const val DUPLICATE_LIBRARY_MODULE_ERROR = | ||
"LibraryGlideModules %s are included more than once, keeping only one!" | ||
const val INVALID_ANNOTATED_CLASS = | ||
"@GlideModule annotated classes must implement AppGlideModule or LibraryGlideModule: %s" | ||
} | ||
|
||
internal class InvalidGlideSourceException(val userMessage: String) : Exception(userMessage) | ||
|
||
private const val APP_MODULE_QUALIFIED_NAME = "com.bumptech.glide.module.AppGlideModule" | ||
private const val LIBRARY_MODULE_QUALIFIED_NAME = "com.bumptech.glide.module.LibraryGlideModule" |
Oops, something went wrong.