diff --git a/annotation/ksp/build.gradle b/annotation/ksp/build.gradle new file mode 100644 index 0000000000..33049bacb5 --- /dev/null +++ b/annotation/ksp/build.gradle @@ -0,0 +1,15 @@ +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") +} + +apply from: "${rootProject.projectDir}/scripts/upload.gradle" diff --git a/annotation/ksp/gradle.properties b/annotation/ksp/gradle.properties new file mode 100644 index 0000000000..d5a6e443f8 --- /dev/null +++ b/annotation/ksp/gradle.properties @@ -0,0 +1,6 @@ +kotlin.code.style=official + +POM_NAME=Glide KSP Annotation Processor +POM_ARTIFACT_ID=ksp +POM_PACKAGING=jar +POM_DESCRIPTION=Glide's KSP based annotation processor. Should be included in all Kotlin applications and libraries that use Glide's modules for configuration and do not require the more advanced features of the Java based compiler. diff --git a/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/AppGlideModules.kt b/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/AppGlideModules.kt new file mode 100644 index 0000000000..7ae72ec12f --- /dev/null +++ b/annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/AppGlideModules.kt @@ -0,0 +1,213 @@ +package com.bumptech.glide.annotation.ksp + +import com.bumptech.glide.annotation.Excludes +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.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.TypeSpec +import kotlin.reflect.KClass + +// This class is visible only for testing +// TODO(b/174783094): Add @VisibleForTesting when internal is supported. +object AppGlideModuleConstants { + // This variable is visible only for testing + // TODO(b/174783094): Add @VisibleForTesting when internal is supported. + 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 CONTEXT_NAME = "Context" + internal const val CONTEXT_PACKAGE = "android.content" + internal const val GLIDE_PACKAGE_NAME = "com.bumptech.glide" + internal const val CONTEXT_QUALIFIED_NAME = "$CONTEXT_PACKAGE.$CONTEXT_NAME" + internal const val GENERATED_ROOT_MODULE_PACKAGE_NAME = GLIDE_PACKAGE_NAME + + internal val CONTEXT_CLASS_NAME = ClassName(CONTEXT_PACKAGE, CONTEXT_NAME) +} + +internal data class AppGlideModuleData( + val name: ClassName, + val constructor: Constructor, +) { + internal data class Constructor(val hasContext: Boolean) +} + +/** + * Given a [com.bumptech.glide.module.AppGlideModule] class declaration provided by the developer, + * validate the class and produce a fully parsed [AppGlideModuleData] that allows us to generate a + * valid [com.bumptech.glide.GeneratedAppGlideModule] implementation without further introspection. + */ +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()) + + return AppGlideModuleData(name = name, constructor = constructor) + } + + 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 && + AppGlideModuleConstants.CONTEXT_QUALIFIED_NAME == + parameters.single().type.resolve().declaration.qualifiedName?.asString() + + private data class IndexFilesAndLibraryModuleNames( + val indexFiles: List, + val libraryModuleNames: List, + ) + + private fun extractGlideModulesFromIndexAnnotation( + index: KSDeclaration, + ): List { + val indexAnnotation: KSAnnotation = index.atMostOneIndexAnnotation() ?: return emptyList() + environment.logger.info("Found index annotation: $indexAnnotation") + return indexAnnotation.getModuleArgumentValues().toList() + } + + private fun KSAnnotation.getModuleArgumentValues(): List { + val result = arguments.find { it.name?.getShortName().equals("modules") }?.value + if (result is List<*> && result.all { it is String }) { + @Suppress("UNCHECKED_CAST") return result as List + } + throw InvalidGlideSourceException("Found an invalid internal Glide index: $this") + } + + 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 ${this.qualifiedName}, but found: + ${matchingAnnotations.size}""" + ) + } + return matchingAnnotations.singleOrNull() + } +} + +/** + * Given valid [AppGlideModuleData], writes a Kotlin implementation of + * [com.bumptech.glide.GeneratedAppGlideModule]. + * + * This class should obtain all of its data from [AppGlideModuleData] and should not interact with + * any ksp classes. In the long run, the restriction may allow us to share code between the Java and + * Kotlin processors. + */ +internal class AppGlideModuleGenerator(private val appGlideModuleData: AppGlideModuleData) { + + fun generateAppGlideModule(): FileSpec { + val generatedAppGlideModuleClass = generateAppGlideModuleClass(appGlideModuleData) + return FileSpec.builder( + AppGlideModuleConstants.GLIDE_PACKAGE_NAME, + "GeneratedAppGlideModuleImpl" + ) + .addType(generatedAppGlideModuleClass) + .build() + } + + private fun generateAppGlideModuleClass( + data: AppGlideModuleData, + ): TypeSpec { + return TypeSpec.classBuilder("GeneratedAppGlideModuleImpl") + .superclass( + ClassName( + AppGlideModuleConstants.GENERATED_ROOT_MODULE_PACKAGE_NAME, + "GeneratedAppGlideModule" + ) + ) + .addModifiers(KModifier.INTERNAL) + .addProperty("appGlideModule", data.name, KModifier.PRIVATE) + .primaryConstructor(generateConstructor(data)) + .addFunction(generateRegisterComponents()) + .addFunction(generateApplyOptions()) + .addFunction(generateManifestParsingDisabled()) + .build() + } + + private fun generateConstructor(data: AppGlideModuleData): FunSpec { + val contextParameterBuilder = + ParameterSpec.builder("context", AppGlideModuleConstants.CONTEXT_CLASS_NAME) + if (!data.constructor.hasContext) { + contextParameterBuilder.addAnnotation( + AnnotationSpec.builder(ClassName("kotlin", "Suppress")) + .addMember("%S", "UNUSED_VARIABLE") + .build() + ) + } + + return FunSpec.constructorBuilder() + .addParameter(contextParameterBuilder.build()) + .addStatement( + "appGlideModule = %T(${if (data.constructor.hasContext) "context" else ""})", + data.name + ) + .build() + + // TODO(judds): Log the discovered modules here. + } + + // TODO(judds): call registerComponents on LibraryGlideModules here. + private fun generateRegisterComponents() = + FunSpec.builder("registerComponents") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .addParameter("context", AppGlideModuleConstants.CONTEXT_CLASS_NAME) + .addParameter("glide", ClassName(AppGlideModuleConstants.GLIDE_PACKAGE_NAME, "Glide")) + .addParameter("registry", ClassName(AppGlideModuleConstants.GLIDE_PACKAGE_NAME, "Registry")) + .addStatement("appGlideModule.registerComponents(context, glide, registry)") + .build() + + private fun generateApplyOptions() = + FunSpec.builder("applyOptions") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .addParameter("context", AppGlideModuleConstants.CONTEXT_CLASS_NAME) + .addParameter( + "builder", + ClassName(AppGlideModuleConstants.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..b783be6b00 --- /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 + +/** + * Glide's KSP annotation processor. + * + * This class recognizes and parses [com.bumptech.glide.module.AppGlideModule]s and + * [com.bumptech.glide.module.LibraryGlideModule]s that are annotated with + * [com.bumptech.glide.annotation.GlideModule]. + * + * `LibraryGlideModule`s are merged into indexes, or classes generated in Glide's package. When a + * `AppGlideModule` is found, we then generate Glide's configuration so that it calls the + * `AppGlideModule` and anay included `LibraryGlideModules`. Using indexes allows us to process + * `LibraryGlideModules` in multiple rounds and/or libraries. + * + * TODO(b/239086146): Finish implementing the behavior described here. + */ +class GlideSymbolProcessor(private val environment: SymbolProcessorEnvironment) : SymbolProcessor { + var isAppGlideModuleGenerated = false + + override fun process(resolver: Resolver): List { + val symbols = resolver.getSymbolsWithAnnotation("com.bumptech.glide.annotation.GlideModule") + val (validSymbols, invalidSymbols) = symbols.partition { 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): Add support for parsing LibraryGlideModules here. + + if (appGlideModules.isNotEmpty()) { + parseAppGlideModuleAndWriteGeneratedAppGlideModule(resolver, appGlideModules.single()) + } + + return invalidSymbols + } + + private fun parseAppGlideModuleAndWriteGeneratedAppGlideModule( + resolver: Resolver, + appGlideModule: KSClassDeclaration, + ) { + val appGlideModuleData = + AppGlideModuleParser(environment, resolver, appGlideModule).parseAppGlideModule() + val appGlideModuleGenerator = AppGlideModuleGenerator(appGlideModuleData) + val appGlideModuleFileSpec: FileSpec = appGlideModuleGenerator.generateAppGlideModule() + writeFile( + appGlideModuleFileSpec, + listOfNotNull(appGlideModule.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] ?: emptyList() } + return GlideModules(appModules, libraryModules) + } + + private fun KSClassDeclaration.hasSuperType(superTypeQualifiedName: String) = + superTypes + .map { superType -> superType.resolve().declaration.qualifiedName!!.asString() } + .contains(superTypeQualifiedName) +} + +// This class is visible only for testing +// TODO(b/174783094): Add @VisibleForTesting when internal is supported. +object GlideSymbolProcessorConstants { + // This variable is visible only for testing + // TODO(b/174783094): Add @VisibleForTesting when internal is supported. + 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" +} + +internal class InvalidGlideSourceException(val userMessage: String) : Exception(userMessage) + +private const val APP_MODULE_QUALIFIED_NAME = "com.bumptech.glide.module.AppGlideModule" +private const val LIBRARY_MODULE_QUALIFIED_NAME = "com.bumptech.glide.module.LibraryGlideModule" 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..3fb36dda9f --- /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) + } +} 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/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/AppGlideModuleOnlyTests.kt b/annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/AppGlideModuleOnlyTests.kt new file mode 100644 index 0000000000..6d5d49995d --- /dev/null +++ b/annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/AppGlideModuleOnlyTests.kt @@ -0,0 +1,345 @@ +package com.bumptech.glide.annotation.ksp.test + +import com.bumptech.glide.annotation.ksp.AppGlideModuleConstants +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 compile_withGlideModuleOnNonLibraryClass_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 compile_withGlideModuleOnValidAppGlideModule_generatedGeneratedAppGlideModule() { + 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 compile_withAppGlideModuleConstructorAcceptingOnlyContext_generatesGeneratedAppGlideModule() { + 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 compile_withAppGlideModuleConstructorRequiringOtherThanContext_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 compile_withAppGlideModuleConstructorRequiringMultipleArguments_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 compile_withAppGlideModuleWithOneEmptyrConstructor_andOneContextOnlyConstructor_usesTheContextOnlyConstructor() { + 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 copmile_withMultipleAppGlideModules_failes() { + 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.Suppress +import kotlin.Unit + +internal class GeneratedAppGlideModuleImpl( + @Suppress("UNUSED_VARIABLE") + 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/SourceTestHelpers.kt b/annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/SourceTestHelpers.kt new file mode 100644 index 0000000000..3c81e9f939 --- /dev/null +++ b/annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/SourceTestHelpers.kt @@ -0,0 +1,88 @@ +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()) diff --git a/build.gradle b/build.gradle index 9d11d66701..8371bdb2a3 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:$KSP_GRADLE_PLUGIN_VERSION" } } @@ -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 21eca1c445..c935761bcc 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 @@ -77,6 +79,7 @@ GUAVA_VERSION=28.1-android JAVAPOET_VERSION=1.9.0 JSR_305_VERSION=3.0.2 JUNIT_VERSION=4.13.2 +KSP_GRADLE_PLUGIN_VERSION=1.7.0-1.0.6 MOCKITO_ANDROID_VERSION=2.24.0 MOCKITO_VERSION=2.24.0 MOCKWEBSERVER_VERSION=3.0.0-RC1 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 1b5e86b267..7c7c2d235e 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 \ @@ -12,4 +14,5 @@ set -e :instrumentation:assembleAndroidTest \ :benchmark:assembleAndroidTest \ :glide:debugJavadoc \ + :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'