From 9b7cf7fcb0fd64198b7fb6f64b9d7a0486f05e0e Mon Sep 17 00:00:00 2001 From: Sam Judd Date: Fri, 1 Jul 2022 13:10:00 -0700 Subject: [PATCH] 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. --- annotation/ksp/build.gradle | 13 + .../annotation/ksp/AppGlideModuleGenerator.kt | 213 +++++++++ .../annotation/ksp/GlideSymbolProcessor.kt | 150 ++++++ .../ksp/GlideSymbolProcessorProvider.kt | 13 + .../bumptech/glide/annotation/ksp/Index.kt | 5 + .../annotation/ksp/LibraryGlideModules.kt | 132 ++++++ annotation/ksp/test/build.gradle | 24 + .../ksp/test/src/main/AndroidManifest.xml | 5 + .../ksp/test/AppGlideModuleOnlyTests.kt | 346 ++++++++++++++ .../ksp/test/LibraryGlideModuleTests.kt | 443 ++++++++++++++++++ .../annotation/ksp/test/SourceTestHelpers.kt | 83 ++++ build.gradle | 15 +- gradle.properties | 2 + .../glide/GeneratedAppGlideModule.java | 5 +- scripts/ci_unit.sh | 3 + settings.gradle | 2 + 16 files changed, 1451 insertions(+), 3 deletions(-) create mode 100644 annotation/ksp/build.gradle create mode 100644 annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/AppGlideModuleGenerator.kt create mode 100644 annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/GlideSymbolProcessor.kt create mode 100644 annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/GlideSymbolProcessorProvider.kt create mode 100644 annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/Index.kt create mode 100644 annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/LibraryGlideModules.kt create mode 100644 annotation/ksp/test/build.gradle create mode 100644 annotation/ksp/test/src/main/AndroidManifest.xml create mode 100644 annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/test/AppGlideModuleOnlyTests.kt create mode 100644 annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/test/LibraryGlideModuleTests.kt create mode 100644 annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/test/SourceTestHelpers.kt diff --git a/annotation/ksp/build.gradle b/annotation/ksp/build.gradle new file mode 100644 index 0000000000..32ba3e2875 --- /dev/null +++ b/annotation/ksp/build.gradle @@ -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") +} \ No newline at end of file diff --git a/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/AppGlideModuleGenerator.kt b/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/AppGlideModuleGenerator.kt new file mode 100644 index 0000000000..fb78479884 --- /dev/null +++ b/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/AppGlideModuleGenerator.kt @@ -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, + val sources: List, +) { + 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 { + 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, val libraryModuleNames: List + ) + + @OptIn(KspExperimental::class) + private fun getIndexesAndLibraryGlideModuleNames(): IndexFilesAndLibraryModuleNames { + val (indexFiles: MutableList, libraryGlideModuleNames: MutableList) = + resolver.getDeclarationsFromPackage(GlideSymbolProcessorConstants.PACKAGE_NAME) + .fold( + Pair(mutableListOf(), mutableListOf()) + ) { 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 { + val indexAnnotation: KSAnnotation = index.atMostOneIndexAnnotation() ?: return listOf() + environment.logger.info("Found index annotation: $indexAnnotation") + return indexAnnotation.getModuleArgumentValues().toList() + } + + private fun KSAnnotation.getModuleArgumentValues(): List { + return arguments.find { it.name?.getShortName().equals("modules") }?.value as List + } + + + private fun KSDeclaration.atMostOneIndexAnnotation() = + atMostOneAnnotation(Index::class) + + private fun KSDeclaration.atMostOneExcludesAnnotation() = + atMostOneAnnotation(Excludes::class) + + private fun KSDeclaration.atMostOneAnnotation( + annotation: KClass + ): KSAnnotation? { + val matchingAnnotations: List = + 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) = + 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() +} + diff --git a/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/GlideSymbolProcessor.kt b/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/GlideSymbolProcessor.kt new file mode 100644 index 0000000000..51e0406070 --- /dev/null +++ b/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/GlideSymbolProcessor.kt @@ -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 { + 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, + validSymbols: List, + invalidSymbols: List, + ): List { + 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 + ) { + 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) { + 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, val libraryModules: List + ) + + private fun extractGlideModules(annotatedModules: List): GlideModules { + val appAndLibraryModuleNames = + listOf(APP_MODULE_QUALIFIED_NAME, LIBRARY_MODULE_QUALIFIED_NAME) + val modulesBySuperType: Map> = + annotatedModules + .filterIsInstance() + .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) \ No newline at end of file diff --git a/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/GlideSymbolProcessorProvider.kt b/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/GlideSymbolProcessorProvider.kt new file mode 100644 index 0000000000..f5b918c6d0 --- /dev/null +++ b/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/GlideSymbolProcessorProvider.kt @@ -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) + } +} \ No newline at end of file diff --git a/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/Index.kt b/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/Index.kt new file mode 100644 index 0000000000..59f6a5f7b3 --- /dev/null +++ b/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/Index.kt @@ -0,0 +1,5 @@ +package com.bumptech.glide.annotation.ksp + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.BINARY) +annotation class Index(val modules: Array) diff --git a/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/LibraryGlideModules.kt b/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/LibraryGlideModules.kt new file mode 100644 index 0000000000..8ee087287a --- /dev/null +++ b/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/LibraryGlideModules.kt @@ -0,0 +1,132 @@ +package com.bumptech.glide.annotation.ksp + +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.annotation.ksp.LibraryGlideModuleData.LibraryModuleName +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFile +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.DelicateKotlinPoetApi +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.TypeSpec +import java.util.UUID + +internal data class LibraryGlideModuleData( + val name: LibraryModuleName, val containingFile: KSFile? +) { + data class LibraryModuleName(val qualifiedName: String) +} + +internal class LibraryGlideModulesParser( + private val environment: SymbolProcessorEnvironment, + private val libraryGlideModules: List, +) { + init { + require(libraryGlideModules.isNotEmpty()) + } + + fun parseUnique(): List { + val allLibraryGlideModules = + libraryGlideModules.map { + LibraryGlideModuleData( + LibraryModuleName(it.qualifiedName!!.asString()), + it.containingFile + ) + } + .toList() + val uniqueLibraryGlideModules = + allLibraryGlideModules.associateBy { it.name }.values.toList() + if (uniqueLibraryGlideModules != libraryGlideModules) { + // Find the set of modules that have been included more than once by mapping the qualified + // name of the module to a count of the number of times it's been seen. Duplicates are then + // any keys that have a value > 1. + val duplicateModules: List = + allLibraryGlideModules.toMutableList() + .fold(mutableMapOf()) { acc, current -> + val key = current.name.qualifiedName + acc[key] = (acc[key] ?: 0) + 1 + acc + } + .filter { it.value > 1 } + .keys + .toList() + environment.logger + .warn(GlideSymbolProcessorConstants.DUPLICATE_LIBRARY_MODULE_ERROR.format(duplicateModules)) + } + + return uniqueLibraryGlideModules + } +} + +/** + * Generates an empty class with an annotation containing the class names of one or more + * LibraryGlideModules and/or one or more GlideExtensions. + * + * We use a separate class so that LibraryGlideModules and GlideExtensions written in libraries + * can be bundled into an AAR and later retrieved by the annotation processor when it processes the + * AppGlideModule in an application. + * + * The output file generated by this class with a single LibraryGlideModule looks like this: + * + * ``` + * @com.bumptech.glide.annotation.compiler.Index( + * ["com.bumptech.glide.integration.okhttp3.OkHttpLibraryGlideModule"] + * ) + * class Indexer_GlideModule_com_bumptech_glide_integration_okhttp3_OkHttpLibraryGlideModule + * ``` + * + * This class is not a public API and used only internally by the processor. + */ +internal object IndexGenerator { + private const val INDEXER_NAME_PREFIX = "GlideIndexer_" + private const val MAXIMUM_FILE_NAME_LENGTH = 255 + + @OptIn(DelicateKotlinPoetApi::class) // We're using AnnotationSpec.builder + fun generate( + libraryModuleNames: List, + ): FileSpec { + val libraryModuleQualifiedNames: List = libraryModuleNames.map { it.qualifiedName } + + val indexAnnotation: AnnotationSpec = + AnnotationSpec.builder(Index::class.java) + .addRepeatedMember(libraryModuleQualifiedNames) + .build() + val indexName = generateUniqueName(libraryModuleQualifiedNames) + + return FileSpec.builder(GlideSymbolProcessorConstants.PACKAGE_NAME, indexName) + .addType(TypeSpec.classBuilder(indexName).addAnnotation(indexAnnotation).build()) + .build() + } + + private fun generateUniqueName(libraryModuleQualifiedNames: List): String { + val glideModuleBasedName = generateNameFromLibraryModules(libraryModuleQualifiedNames) + + // If the indexer name has too many packages/modules, it can exceed the file name length + // allowed by the file system, which can break compilation. To avoid that, fall back to a + // deterministic UUID. + return if (glideModuleBasedName.exceedsFileSystemMaxNameLength()) { + generateShortUUIDBasedName(glideModuleBasedName) + } else { + glideModuleBasedName + } + } + + private fun String.exceedsFileSystemMaxNameLength() = + length >= MAXIMUM_FILE_NAME_LENGTH - INDEXER_NAME_PREFIX.length + + private fun generateShortUUIDBasedName(glideModuleBasedName: String) = + INDEXER_NAME_PREFIX + + UUID.nameUUIDFromBytes(glideModuleBasedName.toByteArray()).toString().replace("-", "_") + + private fun generateNameFromLibraryModules(libraryModuleQualifiedNames: List): String { + return libraryModuleQualifiedNames.joinToString( + prefix = INDEXER_NAME_PREFIX + GlideModule::class.java.simpleName + "_", + separator = "_" + ) { + it.replace(".", "_") + } + } + + private fun AnnotationSpec.Builder.addRepeatedMember(repeatedMember: List) = + addMember("[\n" + "%S,\n".repeat(repeatedMember.size) + "]", *repeatedMember.toTypedArray()) +} \ No newline at end of file diff --git a/annotation/ksp/test/build.gradle b/annotation/ksp/test/build.gradle new file mode 100644 index 0000000000..b3a42b5937 --- /dev/null +++ b/annotation/ksp/test/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'org.jetbrains.kotlin.android' + id 'com.android.library' +} + +android { + compileSdkVersion COMPILE_SDK_VERSION as int + + defaultConfig { + minSdkVersion MIN_SDK_VERSION as int + targetSdkVersion TARGET_SDK_VERSION as int + versionName VERSION_NAME as String + } +} + +dependencies { + implementation "junit:junit:$JUNIT_VERSION" + testImplementation project(":annotation:ksp") + testImplementation project(":annotation") + testImplementation project(":glide") + testImplementation "com.github.tschuchortdev:kotlin-compile-testing-ksp:${KOTLIN_COMPILE_TESTING_VERSION}" + testImplementation "com.google.truth:truth:${TRUTH_VERSION}" + testImplementation "org.jetbrains.kotlin:kotlin-test:${JETBRAINS_KOTLIN_TEST_VERSION}" +} \ No newline at end of file diff --git a/annotation/ksp/test/src/main/AndroidManifest.xml b/annotation/ksp/test/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..24d1ba23d8 --- /dev/null +++ b/annotation/ksp/test/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/test/AppGlideModuleOnlyTests.kt b/annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/test/AppGlideModuleOnlyTests.kt new file mode 100644 index 0000000000..a0c07e1125 --- /dev/null +++ b/annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/test/AppGlideModuleOnlyTests.kt @@ -0,0 +1,346 @@ +package com.bumptech.glide.annotation.ksp.test + +import com.bumptech.glide.annotation.ksp.AppGlideModuleConstants +import com.bumptech.glide.annotation.ksp.GlideSymbolProcessor +import com.bumptech.glide.annotation.ksp.GlideSymbolProcessorConstants +import com.google.common.truth.Truth.assertThat +import com.tschuchort.compiletesting.KotlinCompilation +import org.intellij.lang.annotations.Language +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +class OnlyAppGlideModuleTests(override val sourceType: SourceType) : PerSourceTypeTest { + + companion object { + @Parameterized.Parameters(name = "sourceType = {0}") + @JvmStatic + fun data() = SourceType.values() + } + + @Test + fun `with @GlideModule on non-library class, compilation fails`() { + val kotlinSource = + KotlinSourceFile( + "Something.kt", + """ + import com.bumptech.glide.annotation.GlideModule + @GlideModule class Something + """ + ) + + val javaSource = + JavaSourceFile( + "Something.java", + """ + package test; + + import com.bumptech.glide.annotation.GlideModule; + @GlideModule + public class Something {} + """ + ) + + compileCurrentSourceType(kotlinSource, javaSource) { + assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) + assertThat(it.messages) + .containsMatch( + GlideSymbolProcessorConstants.INVALID_ANNOTATED_CLASS.format(".*/Something.*") + ) + } + } + + @Test + fun `with @GlideModule on valid AppGlideModule, generates GeneratedAppGlideModule`() { + val kotlinModule = + KotlinSourceFile( + "Module.kt", + """ + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.AppGlideModule + + @GlideModule class Module : AppGlideModule() + """ + ) + val javaModule = + JavaSourceFile( + "Module.java", + """ + import com.bumptech.glide.annotation.GlideModule; + import com.bumptech.glide.module.AppGlideModule; + + @GlideModule public class Module extends AppGlideModule {} + """.trimIndent() + ) + + compileCurrentSourceType(kotlinModule, javaModule) { + assertThat(it.generatedAppGlideModuleContents()).hasSourceEqualTo(simpleAppGlideModule) + assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + } + } + + @Test + fun `with AppGlideModule constructor that accepts only Context, generates GeneratedAppGlideModule`() { + val kotlinModule = + KotlinSourceFile( + "Module.kt", + """ + import android.content.Context + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.AppGlideModule + + @GlideModule class Module(context: Context) : AppGlideModule() + """ + ) + + val javaModule = + JavaSourceFile( + "Module.java", + """ + import android.content.Context; + import com.bumptech.glide.annotation.GlideModule; + import com.bumptech.glide.module.AppGlideModule; + + @GlideModule public class Module extends AppGlideModule { + public Module(Context context) {} + } + """ + ) + + compileCurrentSourceType(kotlinModule, javaModule) { + assertThat(it.generatedAppGlideModuleContents()).hasSourceEqualTo(appGlideModuleWithContext) + assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + } + } + + @Test + fun `with AppGlideModule constructor that accepts something other than a Context, fails`() { + val kotlinModule = + KotlinSourceFile( + "Module.kt", + """ + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.AppGlideModule + + @GlideModule class Module(value: Int) : AppGlideModule() + """ + ) + val javaModule = + JavaSourceFile( + "Module.java", + """ + import com.bumptech.glide.annotation.GlideModule; + import com.bumptech.glide.module.AppGlideModule; + + @GlideModule public class Module extends AppGlideModule { + public Module(Integer value) {} + } + """ + ) + + compileCurrentSourceType(kotlinModule, javaModule) { + assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) + assertThat(it.messages).contains(AppGlideModuleConstants.INVALID_MODULE_MESSAGE) + } + } + + @Test + fun `with AppGlideModule constructor that accepts multiple arguments, fails`() { + val kotlinModule = + KotlinSourceFile( + "Module.kt", + """ + import android.content.Context + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.AppGlideModule + + @GlideModule class Module(value: Context, otherValue: Int) : AppGlideModule() + """ + ) + val javaModule = + JavaSourceFile( + "Module.java", + """ + import android.content.Context; + import com.bumptech.glide.annotation.GlideModule; + import com.bumptech.glide.module.AppGlideModule; + + @GlideModule public class Module extends AppGlideModule { + public Module(Context value, int otherValue) {} + } + """ + ) + + compileCurrentSourceType(kotlinModule, javaModule) { + assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) + assertThat(it.messages).contains(AppGlideModuleConstants.INVALID_MODULE_MESSAGE) + } + } + + // This is quite weird, we could probably pretty reasonably just assert that this doesn't happen. + @Test + fun `with AppGlideModule with one constructor that accepts Context, one that's empty, uses the Context constructor`() { + val kotlinModule = + KotlinSourceFile( + "Module.kt", + """ + import android.content.Context + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.AppGlideModule + + @GlideModule class Module(context: Context?) : AppGlideModule() { + constructor() : this(null) + } + + """ + ) + val javaModule = + JavaSourceFile( + "Module.java", + """ + import android.content.Context; + import com.bumptech.glide.annotation.GlideModule; + import com.bumptech.glide.module.AppGlideModule; + import javax.annotation.Nullable; + + @GlideModule public class Module extends AppGlideModule { + public Module() {} + public Module(@Nullable Context context) {} + } + """ + ) + + compileCurrentSourceType(kotlinModule, javaModule) { + assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + assertThat(it.generatedAppGlideModuleContents()).hasSourceEqualTo(appGlideModuleWithContext) + } + } + + @Test + fun `with multiple AppGlideModules, fails`() { + val firstKtModule = + KotlinSourceFile( + "Module1.kt", + """ + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.AppGlideModule + + @GlideModule class Module1 : AppGlideModule() + """ + ) + + val secondKtModule = + KotlinSourceFile( + "Module2.kt", + """ + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.AppGlideModule + + @GlideModule class Module2 : AppGlideModule() + """ + ) + + val firstJavaModule = + JavaSourceFile( + "Module1.java", + """ + import com.bumptech.glide.annotation.GlideModule; + import com.bumptech.glide.module.AppGlideModule; + + @GlideModule public class Module1 extends AppGlideModule { + public Module1() {} + } + """ + ) + + val secondJavaModule = + JavaSourceFile( + "Module2.java", + """ + import com.bumptech.glide.annotation.GlideModule; + import com.bumptech.glide.module.AppGlideModule; + + @GlideModule public class Module2 extends AppGlideModule { + public Module2() {} + } + """ + ) + + compileCurrentSourceType(firstKtModule, secondKtModule, firstJavaModule, secondJavaModule) { + assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) + assertThat(it.messages) + .contains( + GlideSymbolProcessorConstants.SINGLE_APP_MODULE_ERROR.format("[Module1, Module2]") + ) + } + } +} + +@Language("kotlin") +const val simpleAppGlideModule = +""" +package com.bumptech.glide + +import Module +import android.content.Context +import kotlin.Boolean +import kotlin.Unit + +internal class GeneratedAppGlideModuleImpl( + context: Context, +) : GeneratedAppGlideModule() { + private val appGlideModule: Module + init { + appGlideModule = Module() + } + + public override fun registerComponents( + context: Context, + glide: Glide, + registry: Registry, + ): Unit { + appGlideModule.registerComponents(context, glide, registry) + } + + public override fun applyOptions(context: Context, builder: GlideBuilder): Unit { + appGlideModule.applyOptions(context, builder) + } + + public override fun isManifestParsingEnabled(): Boolean = false +} +""" + +@Language("kotlin") +const val appGlideModuleWithContext = +""" +package com.bumptech.glide + +import Module +import android.content.Context +import kotlin.Boolean +import kotlin.Unit + +internal class GeneratedAppGlideModuleImpl( + context: Context, +) : GeneratedAppGlideModule() { + private val appGlideModule: Module + init { + appGlideModule = Module(context) + } + + public override fun registerComponents( + context: Context, + glide: Glide, + registry: Registry, + ): Unit { + appGlideModule.registerComponents(context, glide, registry) + } + + public override fun applyOptions(context: Context, builder: GlideBuilder): Unit { + appGlideModule.applyOptions(context, builder) + } + + public override fun isManifestParsingEnabled(): Boolean = false +} +""" diff --git a/annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/test/LibraryGlideModuleTests.kt b/annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/test/LibraryGlideModuleTests.kt new file mode 100644 index 0000000000..d80aee9738 --- /dev/null +++ b/annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/test/LibraryGlideModuleTests.kt @@ -0,0 +1,443 @@ +package com.bumptech.glide.annotation.ksp.test + +import com.bumptech.glide.annotation.ksp.GlideSymbolProcessor +import com.bumptech.glide.annotation.ksp.GlideSymbolProcessorConstants +import com.google.common.truth.Truth.assertThat +import com.tschuchort.compiletesting.KotlinCompilation.ExitCode +import java.io.FileNotFoundException +import kotlin.test.assertFailsWith +import org.intellij.lang.annotations.Language +import org.junit.Assume.assumeTrue +import org.junit.Test +import org.junit.runners.Parameterized +import org.junit.runner.RunWith +import org.junit.runners.Parameterized.Parameters + +@RunWith(Parameterized::class) +class LibraryGlideModuleTests(override val sourceType: SourceType) : PerSourceTypeTest { + + companion object { + @Parameters(name = "sourceType = {0}") + @JvmStatic + fun data() = SourceType.values() + } + + @Test + fun `with @GlideModule on valid LibraryGlideModule, succeeds but does not generate GeneratedAppGlideModule`() { + val kotlinModule = + KotlinSourceFile( + "Module.kt", + """ + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.LibraryGlideModule + + @GlideModule class Module : LibraryGlideModule() + """ + ) + val javaModule = + JavaSourceFile( + "Module.java", + """ + import com.bumptech.glide.annotation.GlideModule; + import com.bumptech.glide.module.LibraryGlideModule; + + @GlideModule public class Module extends LibraryGlideModule {} + """ + ) + + compileCurrentSourceType(kotlinModule, javaModule) { + assertThat(it.exitCode).isEqualTo(ExitCode.OK) + assertFailsWith { it.generatedAppGlideModuleContents() } + } + } + + @Test + fun `with valid LibraryGlideModule, and AppGlideModule, calls LibraryGlideModule from GeneratedAppGlideModule`() { + val kotlinLibraryModule = + KotlinSourceFile( + "LibraryModule.kt", + """ + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.LibraryGlideModule + + @GlideModule class LibraryModule : LibraryGlideModule() + """ + ) + val kotlinAppModule = + KotlinSourceFile( + "AppModule.kt", + """ + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.AppGlideModule + + @GlideModule class AppModule : AppGlideModule() + """ + ) + val javaLibraryModule = + JavaSourceFile( + "LibraryModule.java", + """ + import com.bumptech.glide.annotation.GlideModule; + import com.bumptech.glide.module.LibraryGlideModule; + + @GlideModule public class LibraryModule extends LibraryGlideModule {} + """ + ) + val javaAppModule = + JavaSourceFile( + "AppModule.java", + """ + import com.bumptech.glide.annotation.GlideModule; + import com.bumptech.glide.module.AppGlideModule; + + @GlideModule public class AppModule extends AppGlideModule { + public AppModule() {} + } + """ + ) + + compileCurrentSourceType( + kotlinAppModule, kotlinLibraryModule, javaAppModule, javaLibraryModule + ) { + assertThat(it.exitCode).isEqualTo(ExitCode.OK) + assertThat(it.generatedAppGlideModuleContents()) + .hasSourceEqualTo(appGlideModuleWithLibraryModule) + } + } + + @Test + fun `with multiple LibraryGlideModules, and AppGlideModule, calls LibraryGlideModules from GeneratedAppGlideModule`() { + val kotlinLibraryModule1 = + KotlinSourceFile( + "LibraryModule1.kt", + """ + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.LibraryGlideModule + + @GlideModule class LibraryModule1 : LibraryGlideModule() + """ + ) + val kotlinLibraryModule2 = + KotlinSourceFile( + "LibraryModule2.kt", + """ + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.LibraryGlideModule + + @GlideModule class LibraryModule2 : LibraryGlideModule() + """ + ) + val kotlinAppModule = + KotlinSourceFile( + "AppModule.kt", + """ + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.AppGlideModule + + @GlideModule class AppModule : AppGlideModule() + """ + ) + val javaLibraryModule1 = + JavaSourceFile( + "LibraryModule1.java", + """ + import com.bumptech.glide.annotation.GlideModule; + import com.bumptech.glide.module.LibraryGlideModule; + + @GlideModule public class LibraryModule1 extends LibraryGlideModule {} + """ + ) + val javaLibraryModule2 = + JavaSourceFile( + "LibraryModule2.java", + """ + import com.bumptech.glide.annotation.GlideModule; + import com.bumptech.glide.module.LibraryGlideModule; + + @GlideModule public class LibraryModule2 extends LibraryGlideModule {} + """ + ) + val javaAppModule = + JavaSourceFile( + "AppModule.java", + """ + import com.bumptech.glide.annotation.GlideModule; + import com.bumptech.glide.module.AppGlideModule; + + @GlideModule public class AppModule extends AppGlideModule { + public AppModule() {} + } + """ + ) + + compileCurrentSourceType( + kotlinAppModule, + kotlinLibraryModule1, + kotlinLibraryModule2, + javaAppModule, + javaLibraryModule1, + javaLibraryModule2, + ) { + assertThat(it.generatedAppGlideModuleContents()) + .hasSourceEqualTo(appGlideModuleWithMultipleLibraryModules) + assertThat(it.exitCode).isEqualTo(ExitCode.OK) + } + } + + @Test + fun `with the same LibraryGlideModule in multiple files, and AppGlideModule, calls LibraryGlideModule once from GeneratedAppGlideModule`() { + // Kotlin seems fine with multiple identical classes. For Java this is compile time error + // already, so we don't have to handle it. + assumeTrue(sourceType == SourceType.KOTLIN) + val kotlinLibraryModule1 = + KotlinSourceFile( + "LibraryModule1.kt", + """ + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.LibraryGlideModule + + @GlideModule class LibraryModule : LibraryGlideModule() + """ + ) + val kotlinLibraryModule2 = + KotlinSourceFile( + "LibraryModule2.kt", + """ + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.LibraryGlideModule + + @GlideModule class LibraryModule : LibraryGlideModule() + """ + ) + val kotlinAppModule = + KotlinSourceFile( + "AppModule.kt", + """ + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.AppGlideModule + + @GlideModule class AppModule : AppGlideModule() + """ + ) + + compileCurrentSourceType( + kotlinAppModule, + kotlinLibraryModule1, + kotlinLibraryModule2, + ) { + assertThat(it.generatedAppGlideModuleContents()) + .hasSourceEqualTo(appGlideModuleWithLibraryModule) + assertThat(it.exitCode).isEqualTo(ExitCode.OK) + assertThat(it.messages) + .contains( + GlideSymbolProcessorConstants.DUPLICATE_LIBRARY_MODULE_ERROR.format("[LibraryModule]") + ) + } + } + + @Test + fun `with LibraryGlideModules with different packages but same name in multiple files and AppGlideModule, calls LibraryGlideModule once from GeneratedAppGlideModule`() { + // TODO(judds): The two java classes don't compile when run by the annotation processor, which + // means we can't really test this case for java code. Fix compilation issue and re-enable this + // test for Java code. + assumeTrue(sourceType == SourceType.KOTLIN) + val kotlinLibraryModule1 = + KotlinSourceFile( + "LibraryModule1.kt", + """ + package first_package + + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.LibraryGlideModule + + @GlideModule class LibraryModule : LibraryGlideModule() + """ + ) + val kotlinLibraryModule2 = + KotlinSourceFile( + "LibraryModule2.kt", + """ + package second_package + + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.LibraryGlideModule + + @GlideModule class LibraryModule : LibraryGlideModule() + """ + ) + val kotlinAppModule = + KotlinSourceFile( + "AppModule.kt", + """ + import com.bumptech.glide.annotation.GlideModule + import com.bumptech.glide.module.AppGlideModule + + @GlideModule class AppModule : AppGlideModule() + """ + ) + val javaLibraryModule1 = + JavaSourceFile( + "LibraryModule1.java", + """ + package first_package; + import com.bumptech.glide.annotation.GlideModule; + import com.bumptech.glide.module.LibraryGlideModule; + + public class LibraryModule1 { + @GlideModule public static final class LibraryModule extends LibraryGlideModule {} + } + """ + ) + val javaLibraryModule2 = + JavaSourceFile( + "LibraryModule2.java", + """ + package second_package; + import com.bumptech.glide.annotation.GlideModule; + import com.bumptech.glide.module.LibraryGlideModule; + + public class LibraryModule2 { + @GlideModule public static final class LibraryModule extends LibraryGlideModule {} + } + """ + ) + val javaAppModule = + JavaSourceFile( + "AppModule.java", + """ + import com.bumptech.glide.annotation.GlideModule; + import com.bumptech.glide.module.AppGlideModule; + + @GlideModule public class AppModule extends AppGlideModule { + public AppModule() {} + } + """ + ) + + compileCurrentSourceType( + kotlinAppModule, + kotlinLibraryModule1, + kotlinLibraryModule2, + javaAppModule, + javaLibraryModule1, + javaLibraryModule2, + ) { + assertThat(it.generatedAppGlideModuleContents()) + .hasSourceEqualTo(appGlideModuleWithPackagePrefixedLibraryModules) + assertThat(it.exitCode).isEqualTo(ExitCode.OK) + } + } +} + + +@Language("kotlin") +const val appGlideModuleWithPackagePrefixedLibraryModules = +""" +package com.bumptech.glide + +import AppModule +import android.content.Context +import first_package.LibraryModule +import kotlin.Boolean +import kotlin.Unit + +internal class GeneratedAppGlideModuleImpl( + context: Context, +) : GeneratedAppGlideModule() { + private val appGlideModule: AppModule + init { + appGlideModule = AppModule() + } + + public override fun registerComponents( + context: Context, + glide: Glide, + registry: Registry, + ): Unit { + LibraryModule().registerComponents(context, glide, registry) + second_package.LibraryModule().registerComponents(context, glide, registry) + appGlideModule.registerComponents(context, glide, registry) + } + + public override fun applyOptions(context: Context, builder: GlideBuilder): Unit { + appGlideModule.applyOptions(context, builder) + } + + public override fun isManifestParsingEnabled(): Boolean = false +} +""" + +@Language("kotlin") +const val appGlideModuleWithLibraryModule = +""" +package com.bumptech.glide + +import AppModule +import LibraryModule +import android.content.Context +import kotlin.Boolean +import kotlin.Unit + +internal class GeneratedAppGlideModuleImpl( + context: Context, +) : GeneratedAppGlideModule() { + private val appGlideModule: AppModule + init { + appGlideModule = AppModule() + } + + public override fun registerComponents( + context: Context, + glide: Glide, + registry: Registry, + ): Unit { + LibraryModule().registerComponents(context, glide, registry) + appGlideModule.registerComponents(context, glide, registry) + } + + public override fun applyOptions(context: Context, builder: GlideBuilder): Unit { + appGlideModule.applyOptions(context, builder) + } + + public override fun isManifestParsingEnabled(): Boolean = false +} +""" + + +@Language("kotlin") +const val appGlideModuleWithMultipleLibraryModules = +""" +package com.bumptech.glide + +import AppModule +import LibraryModule1 +import LibraryModule2 +import android.content.Context +import kotlin.Boolean +import kotlin.Unit + +internal class GeneratedAppGlideModuleImpl( + context: Context, +) : GeneratedAppGlideModule() { + private val appGlideModule: AppModule + init { + appGlideModule = AppModule() + } + + public override fun registerComponents( + context: Context, + glide: Glide, + registry: Registry, + ): Unit { + LibraryModule1().registerComponents(context, glide, registry) + LibraryModule2().registerComponents(context, glide, registry) + appGlideModule.registerComponents(context, glide, registry) + } + + public override fun applyOptions(context: Context, builder: GlideBuilder): Unit { + appGlideModule.applyOptions(context, builder) + } + + public override fun isManifestParsingEnabled(): Boolean = false +} +""" diff --git a/annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/test/SourceTestHelpers.kt b/annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/test/SourceTestHelpers.kt new file mode 100644 index 0000000000..84c7504e23 --- /dev/null +++ b/annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/test/SourceTestHelpers.kt @@ -0,0 +1,83 @@ +package com.bumptech.glide.annotation.ksp.test + +import com.bumptech.glide.annotation.ksp.GlideSymbolProcessorProvider +import com.google.common.truth.StringSubject +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import com.tschuchort.compiletesting.kspSourcesDir +import com.tschuchort.compiletesting.symbolProcessorProviders +import java.io.File +import java.io.FileNotFoundException +import org.intellij.lang.annotations.Language + +internal class CompilationResult( + private val compilation: KotlinCompilation, result: KotlinCompilation.Result +) { + val exitCode = result.exitCode + val messages = result.messages + + fun generatedAppGlideModuleContents() = + readFile(findAppGlideModule()) + + private fun readFile(file: File) = file.readLines().joinToString("\n") + + private fun findAppGlideModule(): File { + var currentDir: File? = compilation.kspSourcesDir + listOf("kotlin", "com", "bumptech", "glide") + .forEach { directoryName -> + currentDir = currentDir?.listFiles()?.find { it.name.equals(directoryName) } + } + return currentDir?.listFiles()?.find { it.name.equals("GeneratedAppGlideModuleImpl.kt") } + ?: throw FileNotFoundException( + "GeneratedAppGlideModuleImpl.kt was not generated or not generated in the expected" + + " location") + } +} + +enum class SourceType { KOTLIN, JAVA } + +sealed interface TypedSourceFile { + fun sourceFile(): SourceFile + fun sourceType(): SourceType +} + +internal class KotlinSourceFile( + val name: String, @Language("kotlin") val content: String +) : TypedSourceFile { + override fun sourceFile() = SourceFile.kotlin(name, content) + override fun sourceType() = SourceType.KOTLIN +} + +internal class JavaSourceFile( + val name: String, @Language("java") val content: String +) : TypedSourceFile { + override fun sourceFile() = SourceFile.java(name, content) + override fun sourceType() = SourceType.JAVA +} + +internal interface PerSourceTypeTest { + val sourceType: SourceType + + fun compileCurrentSourceType( + vararg sourceFiles: TypedSourceFile, + test: (input: CompilationResult) -> Unit + ) { + test( + compile(sourceFiles.filter { it.sourceType() == sourceType }.map { it.sourceFile() }.toList()) + ) + } +} + +internal fun compile(sourceFiles: List): CompilationResult { + require(sourceFiles.isNotEmpty()) + val compilation = KotlinCompilation().apply { + sources = sourceFiles + symbolProcessorProviders = listOf(GlideSymbolProcessorProvider()) + inheritClassPath = true + } + val result = compilation.compile() + return CompilationResult(compilation, result) +} + +fun StringSubject.hasSourceEqualTo(sourceContents: String) = + isEqualTo(sourceContents.trimIndent()) \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8578957ec5..2825347dd5 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,7 @@ buildscript { classpath "se.bjurr.violations:violations-gradle-plugin:${VIOLATIONS_PLUGIN_VERSION}" classpath "androidx.benchmark:benchmark-gradle-plugin:${ANDROID_X_BENCHMARK_VERSION}" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${JETBRAINS_KOTLIN_VERSION}" + classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:1.7.0-1.0.6" } } @@ -41,11 +42,21 @@ subprojects { project -> url "https://oss.sonatype.org/content/repositories/snapshots" } gradlePluginPortal() + + } + + afterEvaluate { + if (!project.plugins.hasPlugin("org.jetbrains.kotlin.jvm")) { + tasks.withType(JavaCompile) { + sourceCompatibility = 1.7 + targetCompatibility = 1.7 + } + } } + tasks.withType(JavaCompile) { - sourceCompatibility = 1.7 - targetCompatibility = 1.7 + // gifencoder is a legacy project that has a ton of warnings and is basically never // modified, so we're not going to worry about cleaning it up. diff --git a/gradle.properties b/gradle.properties index 9aef25bd94..02e4cfc5d5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -65,10 +65,12 @@ ANDROID_X_LIFECYCLE_KTX_VERSION=2.4.1 # org.jetbrains versions JETBRAINS_KOTLINX_COROUTINES_VERSION=1.6.3 JETBRAINS_KOTLIN_VERSION=1.7.0 +JETBRAINS_KOTLIN_TEST_VERSION=1.7.0 ## Other dependency versions ANDROID_GRADLE_VERSION=7.2.1 AUTO_SERVICE_VERSION=1.0-rc3 +KOTLIN_COMPILE_TESTING_VERSION=1.4.9 DAGGER_VERSION=2.15 ERROR_PRONE_PLUGIN_VERSION=2.0.2 ERROR_PRONE_VERSION=2.3.4 diff --git a/library/src/main/java/com/bumptech/glide/GeneratedAppGlideModule.java b/library/src/main/java/com/bumptech/glide/GeneratedAppGlideModule.java index 5f58e1b9dd..04d0f1ec2c 100644 --- a/library/src/main/java/com/bumptech/glide/GeneratedAppGlideModule.java +++ b/library/src/main/java/com/bumptech/glide/GeneratedAppGlideModule.java @@ -4,6 +4,7 @@ import androidx.annotation.Nullable; import com.bumptech.glide.manager.RequestManagerRetriever; import com.bumptech.glide.module.AppGlideModule; +import java.util.HashSet; import java.util.Set; /** @@ -15,7 +16,9 @@ abstract class GeneratedAppGlideModule extends AppGlideModule { /** This method can be removed when manifest parsing is no longer supported. */ @NonNull - abstract Set> getExcludedModuleClasses(); + Set> getExcludedModuleClasses() { + return new HashSet<>(); + } @Nullable RequestManagerRetriever.RequestManagerFactory getRequestManagerFactory() { diff --git a/scripts/ci_unit.sh b/scripts/ci_unit.sh index 917bec28b5..9474b01e3d 100755 --- a/scripts/ci_unit.sh +++ b/scripts/ci_unit.sh @@ -2,6 +2,8 @@ set -e +# TODO(judds): Remove the KSP tests when support is available to run them in +# Google3 ./gradlew build \ -x :samples:flickr:build \ -x :samples:giphy:build \ @@ -11,4 +13,5 @@ set -e -x :samples:svg:build \ :instrumentation:assembleAndroidTest \ :benchmark:assembleAndroidTest \ + :annotation:ksp:test:test \ --parallel diff --git a/settings.gradle b/settings.gradle index 4b7c262ba0..f66291a08e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,6 +9,8 @@ include ':instrumentation' include ':annotation' include ':annotation:compiler' //include ':annotation:compiler:test' +include ':annotation:ksp' +include ':annotation:ksp:test' include ':benchmark' include ':glide' include ':third_party:gif_decoder'