Skip to content

Commit

Permalink
Add support for @excludes in Glide's KSP processor.
Browse files Browse the repository at this point in the history
  • Loading branch information
sjudd committed Aug 1, 2022
1 parent 6f681a2 commit 2a787b9
Show file tree
Hide file tree
Showing 5 changed files with 421 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ 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.google.devtools.ksp.symbol.KSNode
import com.google.devtools.ksp.symbol.KSType
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
Expand All @@ -26,9 +28,17 @@ 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."
// This variable is visible only for testing
// TODO(b/174783094): Add @VisibleForTesting when internal is supported.
const val INVALID_EXCLUDES_ANNOTATION_MESSAGE = """
@Excludes on %s is invalid. The value argument of your @Excludes annotation must be set to
either a single LibraryGlideModule class or a non-empty list of LibraryGlideModule classes.
Remove the annotation if you do not wish to exclude any LibraryGlideModules. Include each
LibraryGlideModule you do wish to exclude exactly once. Do not put types other than
LibraryGlideModules in the argument list"""

private const val CONTEXT_NAME = "Context"
internal const val CONTEXT_PACKAGE = "android.content"
private 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
Expand Down Expand Up @@ -74,14 +84,84 @@ internal class AppGlideModuleParser(
}

private fun getExcludedGlideModuleClassNames(): Set<String> {
val excludesAnnotation = appGlideModuleClass.atMostOneExcludesAnnotation()
// TODO(judds): Implement support for the excludes annotation.
val excludesAnnotation = appGlideModuleClass.atMostOneExcludesAnnotation() ?: return emptySet()
environment.logger.logging(
"Found excludes annotation arguments: ${excludesAnnotation?.arguments}"
"Found excludes annotation arguments: ${excludesAnnotation.arguments}"
)
return emptySet()
return parseExcludesAnnotationArgumentsOrNull(excludesAnnotation)
?: throw InvalidGlideSourceException(
AppGlideModuleConstants.INVALID_EXCLUDES_ANNOTATION_MESSAGE.format(
appGlideModuleClass.qualifiedName?.asString()))
}

/**
* Given a list of arguments from an [com.bumptech.glide.annotation.Excludes] annotation, parses
* and returns a list of qualified names of the excluded
* [com.bumptech.glide.module.LibraryGlideModule] implementations, or returns null if the
* arguments are invalid.
*
* Ideally we'd throw more specific exceptions based on the type of failure. However, there are
* a bunch of individual failure types and they differ depending on whether the source was written
* in Java or Kotlin. Rather than trying to describe every failure in detail, we'll just return
* null and allow callers to describe the correct behavior.
*/
private fun parseExcludesAnnotationArgumentsOrNull(
excludesAnnotation: KSAnnotation
): Set<String>? {
val valueArguments: List<KSType>? = excludesAnnotation.valueArgumentList()
// From the 'value' argument list, keep only those types that:
return valueArguments
// 1. extend LibraryGlideModules
?.filter { it.extendsLibraryGlideModule() }
// 2. have valid qualified names
?.mapNotNull { it.declaration.qualifiedName?.asString() }
// 3. are unique
?.toSet()
// And then return the validated set only if the excludes annotation was non-empty every
// excluded module referenced by the annotation was valid.
?.takeIf { it.isNotEmpty() && it.size == valueArguments.size }
}

private fun KSType.extendsLibraryGlideModule(): Boolean =
ModuleParser.extractGlideModules(listOf<KSNode>(declaration)).libraryModules.size == 1

/**
* Parses the `value` argument as a list of the given type, or returns `null` if the annotation
* has multiple arguments or `value` has any entries that are not of the expected type `T`.
*
* `value` is the name of the default annotation parameter allowed by syntax like
* `@Excludes(argument)` or `@Excludes(argument1, argument2)` or
* `@Excludes({argument1, argument2})`, depending on the source type (Kotlin or Java). This method
* requires that the annotation has exactly one `value` argument of a given type and standardizes
* the differences KSP produces between Kotlin and Java source.
*
* To make this function more general purpose, we should assert that the values are of type T
* rather just returning null. For our current single use case, returning null matches the use
* case for the caller better than throwing.
*/
private inline fun <reified T> KSAnnotation.valueArgumentList(): List<T>? {
// Require that the annotation has a single value argument that points either to a single thing
// or a list of things (A or [A, B, C]). First validate that there's exactly one parameter and
// that it has the expected name.
// e.g. @Excludes(value = (A or [A, B, C])) -> (A or [A, B, C])
val valueParameterValue: Any? =
arguments.singleOrNull()
.takeIf{ it?.name?.asString() == "value" }
?.value

// Next unify the types by verifying that it either has a single value of T, or a List of
// T and converting both to List<T>
// (A or [A, B, C]) -> ([A] or [A, B, C]) with the correct types
return when(valueParameterValue) {
is T -> listOf(valueParameterValue)
is List<*> -> valueParameterValue.asListGivenTypeOfOrNull()
else -> null
}
}

private inline fun <reified T> List<*>.asListGivenTypeOfOrNull(): List<T>? =
filterIsInstance(T::class.java).takeIf { it.size == size }

private fun parseAppGlideModuleConstructorOrThrow(): AppGlideModuleData.Constructor {
val hasEmptyConstructors = appGlideModuleClass.getConstructors().any { it.parameters.isEmpty() }
val hasContextParamOnlyConstructor =
Expand Down Expand Up @@ -152,7 +232,7 @@ internal class AppGlideModuleParser(
.toList()
if (matchingAnnotations.size > 1) {
throw InvalidGlideSourceException(
"""Expected 0 or 1 $annotation annotations on ${this.qualifiedName}, but found:
"""Expected 0 or 1 $annotation annotations on $qualifiedName, but found:
${matchingAnnotations.size}"""
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class GlideSymbolProcessor(private val environment: SymbolProcessorEnvironment)
): List<KSAnnotated> {
environment.logger.logging("Found symbols, valid: $validSymbols, invalid: $invalidSymbols")

val (appGlideModules, libraryGlideModules) = extractGlideModules(validSymbols)
val (appGlideModules, libraryGlideModules) = ModuleParser.extractGlideModules(validSymbols)

if (libraryGlideModules.size + appGlideModules.size != validSymbols.count()) {
val invalidModules =
Expand Down Expand Up @@ -136,28 +136,6 @@ class GlideSymbolProcessor(private val environment: SymbolProcessorEnvironment)

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
Expand All @@ -175,5 +153,3 @@ object GlideSymbolProcessorConstants {

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"
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.bumptech.glide.annotation.ksp

import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSNode

object ModuleParser {

internal data class GlideModules(
val appModules: List<KSClassDeclaration>,
val libraryModules: List<KSClassDeclaration>,
)

internal fun extractGlideModules(annotatedModules: List<KSNode>): 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)

private const val APP_MODULE_QUALIFIED_NAME = "com.bumptech.glide.module.AppGlideModule"
private const val LIBRARY_MODULE_QUALIFIED_NAME = "com.bumptech.glide.module.LibraryGlideModule"
}
Loading

0 comments on commit 2a787b9

Please sign in to comment.