Skip to content

Commit

Permalink
Add a basic KSP Symbol Processor for Glide
Browse files Browse the repository at this point in the history
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
sjudd committed Jul 6, 2022
1 parent dae0f9a commit 9b7cf7f
Show file tree
Hide file tree
Showing 16 changed files with 1,451 additions and 3 deletions.
13 changes: 13 additions & 0 deletions annotation/ksp/build.gradle
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")
}
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()
}

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

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"
)

// TODO(judds): Consider what happens if a LibraryGlideModule is discovered after we've already
// written the GeneratedAppGldieModule below.
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)
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)
}
}
Loading

0 comments on commit 9b7cf7f

Please sign in to comment.