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'