forked from bumptech/glide
-
Notifications
You must be signed in to change notification settings - Fork 6
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.
- Loading branch information
Showing
15 changed files
with
1,444 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,13 @@ | ||
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") | ||
} |
213 changes: 213 additions & 0 deletions
213
annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/AppGlideModuleGenerator.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.KspExperimental | ||
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.ClassName | ||
import com.squareup.kotlinpoet.FileSpec | ||
import com.squareup.kotlinpoet.FunSpec | ||
import com.squareup.kotlinpoet.KModifier | ||
import com.squareup.kotlinpoet.TypeSpec | ||
import kotlin.reflect.KClass | ||
|
||
object AppGlideModuleConstants { | ||
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 GLIDE_PACKAGE_NAME = "com.bumptech.glide" | ||
private const val CONTEXT_PACKAGE = "android.content" | ||
private const val CONTEXT_NAME = "Context" | ||
private const val CONTEXT_QUALIFIED_NAME = "$CONTEXT_PACKAGE.$CONTEXT_NAME" | ||
private const val GENERATED_ROOT_MODULE_PACKAGE_NAME = GLIDE_PACKAGE_NAME | ||
private val CONTEXT_CLASS_NAME = ClassName(CONTEXT_PACKAGE, CONTEXT_NAME) | ||
|
||
internal data class AppGlideModuleData( | ||
val name: ClassName, | ||
val constructor: Constructor, | ||
val allowedLibraryGlideModuleNames: List<String>, | ||
val sources: List<KSDeclaration>, | ||
) { | ||
internal data class Constructor(val hasContext: Boolean) | ||
} | ||
|
||
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()) | ||
|
||
val (indexFiles, allLibraryModuleNames) = getIndexesAndLibraryGlideModuleNames() | ||
val excludedGlideModuleClassNames = getExcludedGlideModuleClassNames() | ||
val filteredGlideModuleClassNames = | ||
allLibraryModuleNames.filterNot { excludedGlideModuleClassNames.contains(it) } | ||
|
||
return AppGlideModuleData( | ||
name = name, | ||
constructor = constructor, | ||
allowedLibraryGlideModuleNames = filteredGlideModuleClassNames, | ||
sources = indexFiles) | ||
} | ||
|
||
private fun getExcludedGlideModuleClassNames(): List<String> { | ||
val excludesAnnotation = appGlideModuleClass.atMostOneExcludesAnnotation() | ||
// TODO(judds): Do something with this. | ||
environment.logger.info("Found excludes annotation arguments: ${excludesAnnotation?.arguments}") | ||
return listOf() | ||
} | ||
|
||
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 && | ||
CONTEXT_QUALIFIED_NAME == | ||
parameters.single().type.resolve().declaration.qualifiedName?.asString() | ||
|
||
private data class IndexFilesAndLibraryModuleNames( | ||
val indexFiles: List<KSDeclaration>, val libraryModuleNames: List<String> | ||
) | ||
|
||
@OptIn(KspExperimental::class) | ||
private fun getIndexesAndLibraryGlideModuleNames(): IndexFilesAndLibraryModuleNames { | ||
val (indexFiles: MutableList<KSDeclaration>, libraryGlideModuleNames: MutableList<String>) = | ||
resolver.getDeclarationsFromPackage(GlideSymbolProcessorConstants.PACKAGE_NAME) | ||
.fold( | ||
Pair(mutableListOf<KSDeclaration>(), mutableListOf<String>()) | ||
) { pair, current -> | ||
val libraryGlideModuleNames = extractGlideModulesFromIndexAnnotation(current) | ||
if (libraryGlideModuleNames.isNotEmpty()) { | ||
pair.first.add(current) | ||
pair.second.addAll(libraryGlideModuleNames) | ||
} | ||
pair | ||
} | ||
|
||
return IndexFilesAndLibraryModuleNames(indexFiles, libraryGlideModuleNames) | ||
} | ||
|
||
private fun extractGlideModulesFromIndexAnnotation( | ||
index: KSDeclaration, | ||
): List<String> { | ||
val indexAnnotation: KSAnnotation = index.atMostOneIndexAnnotation() ?: return listOf() | ||
environment.logger.info("Found index annotation: $indexAnnotation") | ||
return indexAnnotation.getModuleArgumentValues().toList() | ||
} | ||
|
||
private fun KSAnnotation.getModuleArgumentValues(): List<String> { | ||
return arguments.find { it.name?.getShortName().equals("modules") }?.value as List<String> | ||
} | ||
|
||
|
||
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 the Index class, but found: | ||
${matchingAnnotations.size}""" | ||
) | ||
} | ||
return matchingAnnotations.singleOrNull() | ||
} | ||
} | ||
|
||
internal class AppGlideModuleGenerator(private val appGlideModuleData: AppGlideModuleData) { | ||
|
||
fun generateAppGlideModule(): FileSpec { | ||
val generatedAppGlideModuleClass = | ||
generateAppGlideModuleClass(appGlideModuleData) | ||
return FileSpec.builder(GLIDE_PACKAGE_NAME, "GeneratedAppGlideModuleImpl") | ||
.addType(generatedAppGlideModuleClass) | ||
.build() | ||
} | ||
|
||
private fun generateAppGlideModuleClass( | ||
data: AppGlideModuleData | ||
): TypeSpec { | ||
return TypeSpec.classBuilder("GeneratedAppGlideModuleImpl") | ||
.superclass(ClassName(GENERATED_ROOT_MODULE_PACKAGE_NAME, "GeneratedAppGlideModule")) | ||
.addModifiers(KModifier.INTERNAL) | ||
.addProperty("appGlideModule", data.name, KModifier.PRIVATE) | ||
.primaryConstructor(generateConstructor(data)) | ||
.addFunction(generateRegisterComponents(data.allowedLibraryGlideModuleNames)) | ||
.addFunction(generateApplyOptions()) | ||
.addFunction(generateManifestParsingDisabled()) | ||
.build() | ||
} | ||
|
||
private fun generateConstructor(data: AppGlideModuleData): FunSpec { | ||
return FunSpec.constructorBuilder() | ||
.addParameter("context", CONTEXT_CLASS_NAME) | ||
.addStatement( | ||
"appGlideModule = %T(${if (data.constructor.hasContext) "context" else ""})", data.name | ||
) | ||
.build() | ||
|
||
// TODO(judds): Log the discovered modules here. | ||
} | ||
|
||
private fun generateRegisterComponents(allowedGlideModuleNames: List<String>) = | ||
FunSpec.builder("registerComponents") | ||
.addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) | ||
.addParameter("context", CONTEXT_CLASS_NAME) | ||
.addParameter("glide", ClassName(GLIDE_PACKAGE_NAME, "Glide")) | ||
.addParameter("registry", ClassName(GLIDE_PACKAGE_NAME, "Registry")) | ||
.apply { | ||
allowedGlideModuleNames.forEach { | ||
addStatement( | ||
"%T().registerComponents(context, glide, registry)", ClassName.bestGuess(it) | ||
) | ||
} | ||
} | ||
.addStatement("appGlideModule.registerComponents(context, glide, registry)") | ||
.build() | ||
|
||
private fun generateApplyOptions() = | ||
FunSpec.builder("applyOptions") | ||
.addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) | ||
.addParameter("context", CONTEXT_CLASS_NAME) | ||
.addParameter("builder", ClassName(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() | ||
} | ||
|
148 changes: 148 additions & 0 deletions
148
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,148 @@ | ||
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 | ||
|
||
object GlideSymbolProcessorConstants { | ||
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" | ||
} | ||
|
||
private const val APP_MODULE_QUALIFIED_NAME = "com.bumptech.glide.module.AppGlideModule" | ||
private const val LIBRARY_MODULE_QUALIFIED_NAME = | ||
"com.bumptech.glide.module.LibraryGlideModule" | ||
|
||
class GlideSymbolProcessor(private val environment: SymbolProcessorEnvironment) : SymbolProcessor { | ||
|
||
override fun process(resolver: Resolver): List<KSAnnotated> { | ||
val symbols = resolver.getSymbolsWithAnnotation("com.bumptech.glide.annotation.GlideModule") | ||
val invalidSymbols = symbols.filter { !it.validate() }.toList() | ||
val validSymbols = symbols.filter { 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" | ||
) | ||
|
||
if (libraryGlideModules.isNotEmpty()) { | ||
parseLibraryModulesAndWriteIndex(libraryGlideModules) | ||
return invalidSymbols + appGlideModules | ||
} | ||
|
||
if (appGlideModules.isNotEmpty()) { | ||
parseAppGlideModuleAndIndexesAndWriteGeneratedAppGlideModule( | ||
resolver, appGlideModules.single() | ||
) | ||
} | ||
|
||
return invalidSymbols | ||
} | ||
|
||
private fun parseAppGlideModuleAndIndexesAndWriteGeneratedAppGlideModule( | ||
resolver: Resolver, appGlideModule: KSClassDeclaration | ||
) { | ||
val appGlideModuleData = | ||
AppGlideModuleParser(environment, resolver, appGlideModule) | ||
.parseAppGlideModule() | ||
val appGlideModuleGenerator = AppGlideModuleGenerator(appGlideModuleData) | ||
val appGlideModuleFileSpec: FileSpec = appGlideModuleGenerator.generateAppGlideModule() | ||
writeFile( | ||
appGlideModuleFileSpec, | ||
appGlideModuleData.sources.mapNotNull { it.containingFile }, | ||
) | ||
} | ||
|
||
private fun parseLibraryModulesAndWriteIndex( | ||
libraryGlideModuleClassDeclarations: List<KSClassDeclaration> | ||
) { | ||
val libraryGlideModulesParser = | ||
LibraryGlideModulesParser(environment, libraryGlideModuleClassDeclarations) | ||
val uniqueLibraryGlideModules = libraryGlideModulesParser.parseUnique() | ||
val index: FileSpec = IndexGenerator.generate(uniqueLibraryGlideModules.map { it.name }) | ||
writeFile(index, uniqueLibraryGlideModules.mapNotNull { it.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] ?: listOf() } | ||
return GlideModules(appModules, libraryModules) | ||
} | ||
|
||
private fun KSClassDeclaration.hasSuperType(superTypeQualifiedName: String) = | ||
superTypes | ||
.map { superType -> superType.resolve().declaration.qualifiedName!!.asString() } | ||
.contains(superTypeQualifiedName) | ||
} | ||
|
||
class InvalidGlideSourceException(val userMessage: String) : Exception(userMessage) |
13 changes: 13 additions & 0 deletions
13
...ion/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/GlideSymbolProcessorProvider.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,13 @@ | ||
package com.bumptech.glide.annotation.ksp | ||
|
||
import com.google.auto.service.AutoService | ||
import com.google.devtools.ksp.processing.SymbolProcessor | ||
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment | ||
import com.google.devtools.ksp.processing.SymbolProcessorProvider | ||
|
||
@AutoService(SymbolProcessorProvider::class) | ||
class GlideSymbolProcessorProvider : SymbolProcessorProvider { | ||
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { | ||
return GlideSymbolProcessor(environment) | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/Index.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,5 @@ | ||
package com.bumptech.glide.annotation.ksp | ||
|
||
@Target(AnnotationTarget.CLASS) | ||
@Retention(AnnotationRetention.BINARY) | ||
annotation class Index(val modules: Array<String>) |
Oops, something went wrong.