Skip to content

Commit c35ad13

Browse files
sjuddglide-copybara-robot
authored andcommittedJul 19, 2022
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. PiperOrigin-RevId: 461943092
1 parent 6640376 commit c35ad13

File tree

15 files changed

+889
-3
lines changed

15 files changed

+889
-3
lines changed
 

‎annotation/ksp/build.gradle

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
plugins {
2+
id 'org.jetbrains.kotlin.jvm'
3+
id 'com.google.devtools.ksp'
4+
}
5+
6+
dependencies {
7+
implementation("com.squareup:kotlinpoet:1.12.0")
8+
implementation project(":annotation")
9+
implementation project(":glide")
10+
implementation 'com.google.devtools.ksp:symbol-processing-api:1.7.0-1.0.6'
11+
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.0.0")
12+
implementation("com.google.auto.service:auto-service-annotations:1.0.1")
13+
}
14+
15+
apply from: "${rootProject.projectDir}/scripts/upload.gradle"

‎annotation/ksp/gradle.properties

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kotlin.code.style=official
2+
3+
POM_NAME=Glide KSP Annotation Processor
4+
POM_ARTIFACT_ID=ksp
5+
POM_PACKAGING=jar
6+
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.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package com.bumptech.glide.annotation.ksp
2+
3+
import com.bumptech.glide.annotation.Excludes
4+
import com.google.devtools.ksp.getConstructors
5+
import com.google.devtools.ksp.processing.Resolver
6+
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
7+
import com.google.devtools.ksp.symbol.KSAnnotation
8+
import com.google.devtools.ksp.symbol.KSClassDeclaration
9+
import com.google.devtools.ksp.symbol.KSDeclaration
10+
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
11+
import com.squareup.kotlinpoet.AnnotationSpec
12+
import com.squareup.kotlinpoet.ClassName
13+
import com.squareup.kotlinpoet.FileSpec
14+
import com.squareup.kotlinpoet.FunSpec
15+
import com.squareup.kotlinpoet.KModifier
16+
import com.squareup.kotlinpoet.ParameterSpec
17+
import com.squareup.kotlinpoet.TypeSpec
18+
import kotlin.reflect.KClass
19+
20+
// This class is visible only for testing
21+
// TODO(b/174783094): Add @VisibleForTesting when internal is supported.
22+
object AppGlideModuleConstants {
23+
// This variable is visible only for testing
24+
// TODO(b/174783094): Add @VisibleForTesting when internal is supported.
25+
const val INVALID_MODULE_MESSAGE =
26+
"Your AppGlideModule must have at least one constructor that has either no parameters or " +
27+
"accepts only a Context."
28+
29+
private const val CONTEXT_NAME = "Context"
30+
internal const val CONTEXT_PACKAGE = "android.content"
31+
internal const val GLIDE_PACKAGE_NAME = "com.bumptech.glide"
32+
internal const val CONTEXT_QUALIFIED_NAME = "$CONTEXT_PACKAGE.$CONTEXT_NAME"
33+
internal const val GENERATED_ROOT_MODULE_PACKAGE_NAME = GLIDE_PACKAGE_NAME
34+
35+
internal val CONTEXT_CLASS_NAME = ClassName(CONTEXT_PACKAGE, CONTEXT_NAME)
36+
}
37+
38+
internal data class AppGlideModuleData(
39+
val name: ClassName,
40+
val constructor: Constructor,
41+
) {
42+
internal data class Constructor(val hasContext: Boolean)
43+
}
44+
45+
/**
46+
* Given a [com.bumptech.glide.module.AppGlideModule] class declaration provided by the developer,
47+
* validate the class and produce a fully parsed [AppGlideModuleData] that allows us to generate a
48+
* valid [com.bumptech.glide.GeneratedAppGlideModule] implementation without further introspection.
49+
*/
50+
internal class AppGlideModuleParser(
51+
private val environment: SymbolProcessorEnvironment,
52+
private val resolver: Resolver,
53+
private val appGlideModuleClass: KSClassDeclaration,
54+
) {
55+
56+
fun parseAppGlideModule(): AppGlideModuleData {
57+
val constructor = parseAppGlideModuleConstructorOrThrow()
58+
val name = ClassName.bestGuess(appGlideModuleClass.qualifiedName!!.asString())
59+
60+
return AppGlideModuleData(name = name, constructor = constructor)
61+
}
62+
63+
private fun parseAppGlideModuleConstructorOrThrow(): AppGlideModuleData.Constructor {
64+
val hasEmptyConstructors = appGlideModuleClass.getConstructors().any { it.parameters.isEmpty() }
65+
val hasContextParamOnlyConstructor =
66+
appGlideModuleClass.getConstructors().any { it.hasSingleContextParameter() }
67+
if (!hasEmptyConstructors && !hasContextParamOnlyConstructor) {
68+
throw InvalidGlideSourceException(AppGlideModuleConstants.INVALID_MODULE_MESSAGE)
69+
}
70+
return AppGlideModuleData.Constructor(hasContextParamOnlyConstructor)
71+
}
72+
73+
private fun KSFunctionDeclaration.hasSingleContextParameter() =
74+
parameters.size == 1 &&
75+
AppGlideModuleConstants.CONTEXT_QUALIFIED_NAME ==
76+
parameters.single().type.resolve().declaration.qualifiedName?.asString()
77+
78+
private data class IndexFilesAndLibraryModuleNames(
79+
val indexFiles: List<KSDeclaration>,
80+
val libraryModuleNames: List<String>,
81+
)
82+
83+
private fun extractGlideModulesFromIndexAnnotation(
84+
index: KSDeclaration,
85+
): List<String> {
86+
val indexAnnotation: KSAnnotation = index.atMostOneIndexAnnotation() ?: return emptyList()
87+
environment.logger.info("Found index annotation: $indexAnnotation")
88+
return indexAnnotation.getModuleArgumentValues().toList()
89+
}
90+
91+
private fun KSAnnotation.getModuleArgumentValues(): List<String> {
92+
val result = arguments.find { it.name?.getShortName().equals("modules") }?.value
93+
if (result is List<*> && result.all { it is String }) {
94+
@Suppress("UNCHECKED_CAST") return result as List<String>
95+
}
96+
throw InvalidGlideSourceException("Found an invalid internal Glide index: $this")
97+
}
98+
99+
private fun KSDeclaration.atMostOneIndexAnnotation() = atMostOneAnnotation(Index::class)
100+
101+
private fun KSDeclaration.atMostOneExcludesAnnotation() = atMostOneAnnotation(Excludes::class)
102+
103+
private fun KSDeclaration.atMostOneAnnotation(
104+
annotation: KClass<out Annotation>,
105+
): KSAnnotation? {
106+
val matchingAnnotations: List<KSAnnotation> =
107+
annotations
108+
.filter {
109+
annotation.qualifiedName?.equals(
110+
it.annotationType.resolve().declaration.qualifiedName?.asString()
111+
)
112+
?: false
113+
}
114+
.toList()
115+
if (matchingAnnotations.size > 1) {
116+
throw InvalidGlideSourceException(
117+
"""Expected 0 or 1 $annotation annotations on ${this.qualifiedName}, but found:
118+
${matchingAnnotations.size}"""
119+
)
120+
}
121+
return matchingAnnotations.singleOrNull()
122+
}
123+
}
124+
125+
/**
126+
* Given valid [AppGlideModuleData], writes a Kotlin implementation of
127+
* [com.bumptech.glide.GeneratedAppGlideModule].
128+
*
129+
* This class should obtain all of its data from [AppGlideModuleData] and should not interact with
130+
* any ksp classes. In the long run, the restriction may allow us to share code between the Java and
131+
* Kotlin processors.
132+
*/
133+
internal class AppGlideModuleGenerator(private val appGlideModuleData: AppGlideModuleData) {
134+
135+
fun generateAppGlideModule(): FileSpec {
136+
val generatedAppGlideModuleClass = generateAppGlideModuleClass(appGlideModuleData)
137+
return FileSpec.builder(
138+
AppGlideModuleConstants.GLIDE_PACKAGE_NAME,
139+
"GeneratedAppGlideModuleImpl"
140+
)
141+
.addType(generatedAppGlideModuleClass)
142+
.build()
143+
}
144+
145+
private fun generateAppGlideModuleClass(
146+
data: AppGlideModuleData,
147+
): TypeSpec {
148+
return TypeSpec.classBuilder("GeneratedAppGlideModuleImpl")
149+
.superclass(
150+
ClassName(
151+
AppGlideModuleConstants.GENERATED_ROOT_MODULE_PACKAGE_NAME,
152+
"GeneratedAppGlideModule"
153+
)
154+
)
155+
.addModifiers(KModifier.INTERNAL)
156+
.addProperty("appGlideModule", data.name, KModifier.PRIVATE)
157+
.primaryConstructor(generateConstructor(data))
158+
.addFunction(generateRegisterComponents())
159+
.addFunction(generateApplyOptions())
160+
.addFunction(generateManifestParsingDisabled())
161+
.build()
162+
}
163+
164+
private fun generateConstructor(data: AppGlideModuleData): FunSpec {
165+
val contextParameterBuilder =
166+
ParameterSpec.builder("context", AppGlideModuleConstants.CONTEXT_CLASS_NAME)
167+
if (!data.constructor.hasContext) {
168+
contextParameterBuilder.addAnnotation(
169+
AnnotationSpec.builder(ClassName("kotlin", "Suppress"))
170+
.addMember("%S", "UNUSED_VARIABLE")
171+
.build()
172+
)
173+
}
174+
175+
return FunSpec.constructorBuilder()
176+
.addParameter(contextParameterBuilder.build())
177+
.addStatement(
178+
"appGlideModule = %T(${if (data.constructor.hasContext) "context" else ""})",
179+
data.name
180+
)
181+
.build()
182+
183+
// TODO(judds): Log the discovered modules here.
184+
}
185+
186+
// TODO(judds): call registerComponents on LibraryGlideModules here.
187+
private fun generateRegisterComponents() =
188+
FunSpec.builder("registerComponents")
189+
.addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE)
190+
.addParameter("context", AppGlideModuleConstants.CONTEXT_CLASS_NAME)
191+
.addParameter("glide", ClassName(AppGlideModuleConstants.GLIDE_PACKAGE_NAME, "Glide"))
192+
.addParameter("registry", ClassName(AppGlideModuleConstants.GLIDE_PACKAGE_NAME, "Registry"))
193+
.addStatement("appGlideModule.registerComponents(context, glide, registry)")
194+
.build()
195+
196+
private fun generateApplyOptions() =
197+
FunSpec.builder("applyOptions")
198+
.addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE)
199+
.addParameter("context", AppGlideModuleConstants.CONTEXT_CLASS_NAME)
200+
.addParameter(
201+
"builder",
202+
ClassName(AppGlideModuleConstants.GLIDE_PACKAGE_NAME, "GlideBuilder")
203+
)
204+
.addStatement("appGlideModule.applyOptions(context, builder)")
205+
.build()
206+
207+
private fun generateManifestParsingDisabled() =
208+
FunSpec.builder("isManifestParsingEnabled")
209+
.addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE)
210+
.returns(Boolean::class)
211+
.addStatement("return false")
212+
.build()
213+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package com.bumptech.glide.annotation.ksp
2+
3+
import com.google.devtools.ksp.processing.Dependencies
4+
import com.google.devtools.ksp.processing.Resolver
5+
import com.google.devtools.ksp.processing.SymbolProcessor
6+
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
7+
import com.google.devtools.ksp.symbol.KSAnnotated
8+
import com.google.devtools.ksp.symbol.KSClassDeclaration
9+
import com.google.devtools.ksp.symbol.KSFile
10+
import com.google.devtools.ksp.validate
11+
import com.squareup.kotlinpoet.FileSpec
12+
13+
/**
14+
* Glide's KSP annotation processor.
15+
*
16+
* This class recognizes and parses [com.bumptech.glide.module.AppGlideModule]s and
17+
* [com.bumptech.glide.module.LibraryGlideModule]s that are annotated with
18+
* [com.bumptech.glide.annotation.GlideModule].
19+
*
20+
* `LibraryGlideModule`s are merged into indexes, or classes generated in Glide's package. When a
21+
* `AppGlideModule` is found, we then generate Glide's configuration so that it calls the
22+
* `AppGlideModule` and anay included `LibraryGlideModules`. Using indexes allows us to process
23+
* `LibraryGlideModules` in multiple rounds and/or libraries.
24+
*
25+
* TODO(b/239086146): Finish implementing the behavior described here.
26+
*/
27+
class GlideSymbolProcessor(private val environment: SymbolProcessorEnvironment) : SymbolProcessor {
28+
var isAppGlideModuleGenerated = false
29+
30+
override fun process(resolver: Resolver): List<KSAnnotated> {
31+
val symbols = resolver.getSymbolsWithAnnotation("com.bumptech.glide.annotation.GlideModule")
32+
val (validSymbols, invalidSymbols) = symbols.partition { it.validate() }.toList()
33+
return try {
34+
processChecked(resolver, symbols, validSymbols, invalidSymbols)
35+
} catch (e: InvalidGlideSourceException) {
36+
environment.logger.error(e.userMessage)
37+
invalidSymbols
38+
}
39+
}
40+
41+
private fun processChecked(
42+
resolver: Resolver,
43+
symbols: Sequence<KSAnnotated>,
44+
validSymbols: List<KSAnnotated>,
45+
invalidSymbols: List<KSAnnotated>,
46+
): List<KSAnnotated> {
47+
environment.logger.logging("Found symbols, valid: $validSymbols, invalid: $invalidSymbols")
48+
49+
val (appGlideModules, libraryGlideModules) = extractGlideModules(validSymbols)
50+
51+
if (libraryGlideModules.size + appGlideModules.size != validSymbols.count()) {
52+
val invalidModules =
53+
symbols
54+
.filter { !libraryGlideModules.contains(it) && !appGlideModules.contains(it) }
55+
.map { it.location.toString() }
56+
.toList()
57+
58+
throw InvalidGlideSourceException(
59+
GlideSymbolProcessorConstants.INVALID_ANNOTATED_CLASS.format(invalidModules)
60+
)
61+
}
62+
63+
if (appGlideModules.size > 1) {
64+
throw InvalidGlideSourceException(
65+
GlideSymbolProcessorConstants.SINGLE_APP_MODULE_ERROR.format(appGlideModules)
66+
)
67+
}
68+
69+
environment.logger.logging(
70+
"Found AppGlideModules: $appGlideModules, LibraryGlideModules: $libraryGlideModules"
71+
)
72+
// TODO(judds): Add support for parsing LibraryGlideModules here.
73+
74+
if (appGlideModules.isNotEmpty()) {
75+
parseAppGlideModuleAndWriteGeneratedAppGlideModule(resolver, appGlideModules.single())
76+
}
77+
78+
return invalidSymbols
79+
}
80+
81+
private fun parseAppGlideModuleAndWriteGeneratedAppGlideModule(
82+
resolver: Resolver,
83+
appGlideModule: KSClassDeclaration,
84+
) {
85+
val appGlideModuleData =
86+
AppGlideModuleParser(environment, resolver, appGlideModule).parseAppGlideModule()
87+
val appGlideModuleGenerator = AppGlideModuleGenerator(appGlideModuleData)
88+
val appGlideModuleFileSpec: FileSpec = appGlideModuleGenerator.generateAppGlideModule()
89+
writeFile(
90+
appGlideModuleFileSpec,
91+
listOfNotNull(appGlideModule.containingFile),
92+
)
93+
}
94+
95+
private fun writeFile(file: FileSpec, sources: List<KSFile>) {
96+
environment.codeGenerator
97+
.createNewFile(
98+
Dependencies(
99+
aggregating = false,
100+
sources = sources.toTypedArray(),
101+
),
102+
file.packageName,
103+
file.name
104+
)
105+
.writer()
106+
.use { file.writeTo(it) }
107+
108+
environment.logger.logging("Wrote file: $file")
109+
}
110+
111+
internal data class GlideModules(
112+
val appModules: List<KSClassDeclaration>,
113+
val libraryModules: List<KSClassDeclaration>,
114+
)
115+
116+
private fun extractGlideModules(annotatedModules: List<KSAnnotated>): GlideModules {
117+
val appAndLibraryModuleNames = listOf(APP_MODULE_QUALIFIED_NAME, LIBRARY_MODULE_QUALIFIED_NAME)
118+
val modulesBySuperType: Map<String?, List<KSClassDeclaration>> =
119+
annotatedModules.filterIsInstance<KSClassDeclaration>().groupBy { classDeclaration ->
120+
appAndLibraryModuleNames.singleOrNull { classDeclaration.hasSuperType(it) }
121+
}
122+
123+
val (appModules, libraryModules) =
124+
appAndLibraryModuleNames.map { modulesBySuperType[it] ?: emptyList() }
125+
return GlideModules(appModules, libraryModules)
126+
}
127+
128+
private fun KSClassDeclaration.hasSuperType(superTypeQualifiedName: String) =
129+
superTypes
130+
.map { superType -> superType.resolve().declaration.qualifiedName!!.asString() }
131+
.contains(superTypeQualifiedName)
132+
}
133+
134+
// This class is visible only for testing
135+
// TODO(b/174783094): Add @VisibleForTesting when internal is supported.
136+
object GlideSymbolProcessorConstants {
137+
// This variable is visible only for testing
138+
// TODO(b/174783094): Add @VisibleForTesting when internal is supported.
139+
val PACKAGE_NAME: String = GlideSymbolProcessor::class.java.`package`.name
140+
const val SINGLE_APP_MODULE_ERROR = "You can have at most one AppGlideModule, but found: %s"
141+
const val DUPLICATE_LIBRARY_MODULE_ERROR =
142+
"LibraryGlideModules %s are included more than once, keeping only one!"
143+
const val INVALID_ANNOTATED_CLASS =
144+
"@GlideModule annotated classes must implement AppGlideModule or LibraryGlideModule: %s"
145+
}
146+
147+
internal class InvalidGlideSourceException(val userMessage: String) : Exception(userMessage)
148+
149+
private const val APP_MODULE_QUALIFIED_NAME = "com.bumptech.glide.module.AppGlideModule"
150+
private const val LIBRARY_MODULE_QUALIFIED_NAME = "com.bumptech.glide.module.LibraryGlideModule"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.bumptech.glide.annotation.ksp
2+
3+
import com.google.auto.service.AutoService
4+
import com.google.devtools.ksp.processing.SymbolProcessor
5+
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
6+
import com.google.devtools.ksp.processing.SymbolProcessorProvider
7+
8+
@AutoService(SymbolProcessorProvider::class)
9+
class GlideSymbolProcessorProvider : SymbolProcessorProvider {
10+
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
11+
return GlideSymbolProcessor(environment)
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.bumptech.glide.annotation.ksp
2+
3+
@Target(AnnotationTarget.CLASS)
4+
@Retention(AnnotationRetention.BINARY)
5+
annotation class Index(val modules: Array<String>)

‎annotation/ksp/test/build.gradle

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
plugins {
2+
id 'org.jetbrains.kotlin.android'
3+
id 'com.android.library'
4+
}
5+
6+
android {
7+
compileSdkVersion COMPILE_SDK_VERSION as int
8+
9+
defaultConfig {
10+
minSdkVersion MIN_SDK_VERSION as int
11+
targetSdkVersion TARGET_SDK_VERSION as int
12+
versionName VERSION_NAME as String
13+
}
14+
}
15+
16+
dependencies {
17+
implementation "junit:junit:$JUNIT_VERSION"
18+
testImplementation project(":annotation:ksp")
19+
testImplementation project(":annotation")
20+
testImplementation project(":glide")
21+
testImplementation "com.github.tschuchortdev:kotlin-compile-testing-ksp:${KOTLIN_COMPILE_TESTING_VERSION}"
22+
testImplementation "com.google.truth:truth:${TRUTH_VERSION}"
23+
testImplementation "org.jetbrains.kotlin:kotlin-test:${JETBRAINS_KOTLIN_TEST_VERSION}"
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools"
4+
package="com.bumptech.glide.annotation.ksp.test">
5+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
package com.bumptech.glide.annotation.ksp.test
2+
3+
import com.bumptech.glide.annotation.ksp.AppGlideModuleConstants
4+
import com.bumptech.glide.annotation.ksp.GlideSymbolProcessorConstants
5+
import com.google.common.truth.Truth.assertThat
6+
import com.tschuchort.compiletesting.KotlinCompilation
7+
import org.intellij.lang.annotations.Language
8+
import org.junit.Test
9+
import org.junit.runner.RunWith
10+
import org.junit.runners.Parameterized
11+
12+
@RunWith(Parameterized::class)
13+
class OnlyAppGlideModuleTests(override val sourceType: SourceType) : PerSourceTypeTest {
14+
15+
companion object {
16+
@Parameterized.Parameters(name = "sourceType = {0}") @JvmStatic fun data() = SourceType.values()
17+
}
18+
19+
@Test
20+
fun compile_withGlideModuleOnNonLibraryClass_fails() {
21+
val kotlinSource =
22+
KotlinSourceFile(
23+
"Something.kt",
24+
"""
25+
import com.bumptech.glide.annotation.GlideModule
26+
@GlideModule class Something
27+
"""
28+
)
29+
30+
val javaSource =
31+
JavaSourceFile(
32+
"Something.java",
33+
"""
34+
package test;
35+
36+
import com.bumptech.glide.annotation.GlideModule;
37+
@GlideModule
38+
public class Something {}
39+
"""
40+
)
41+
42+
compileCurrentSourceType(kotlinSource, javaSource) {
43+
assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
44+
assertThat(it.messages)
45+
.containsMatch(
46+
GlideSymbolProcessorConstants.INVALID_ANNOTATED_CLASS.format(".*/Something.*")
47+
)
48+
}
49+
}
50+
51+
@Test
52+
fun compile_withGlideModuleOnValidAppGlideModule_generatedGeneratedAppGlideModule() {
53+
val kotlinModule =
54+
KotlinSourceFile(
55+
"Module.kt",
56+
"""
57+
import com.bumptech.glide.annotation.GlideModule
58+
import com.bumptech.glide.module.AppGlideModule
59+
60+
@GlideModule class Module : AppGlideModule()
61+
"""
62+
)
63+
val javaModule =
64+
JavaSourceFile(
65+
"Module.java",
66+
"""
67+
import com.bumptech.glide.annotation.GlideModule;
68+
import com.bumptech.glide.module.AppGlideModule;
69+
70+
@GlideModule public class Module extends AppGlideModule {}
71+
""".trimIndent()
72+
)
73+
74+
compileCurrentSourceType(kotlinModule, javaModule) {
75+
assertThat(it.generatedAppGlideModuleContents()).hasSourceEqualTo(simpleAppGlideModule)
76+
assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
77+
}
78+
}
79+
80+
@Test
81+
fun compile_withAppGlideModuleConstructorAcceptingOnlyContext_generatesGeneratedAppGlideModule() {
82+
val kotlinModule =
83+
KotlinSourceFile(
84+
"Module.kt",
85+
"""
86+
import android.content.Context
87+
import com.bumptech.glide.annotation.GlideModule
88+
import com.bumptech.glide.module.AppGlideModule
89+
90+
@GlideModule class Module(context: Context) : AppGlideModule()
91+
"""
92+
)
93+
94+
val javaModule =
95+
JavaSourceFile(
96+
"Module.java",
97+
"""
98+
import android.content.Context;
99+
import com.bumptech.glide.annotation.GlideModule;
100+
import com.bumptech.glide.module.AppGlideModule;
101+
102+
@GlideModule public class Module extends AppGlideModule {
103+
public Module(Context context) {}
104+
}
105+
"""
106+
)
107+
108+
compileCurrentSourceType(kotlinModule, javaModule) {
109+
assertThat(it.generatedAppGlideModuleContents()).hasSourceEqualTo(appGlideModuleWithContext)
110+
assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
111+
}
112+
}
113+
114+
@Test
115+
fun compile_withAppGlideModuleConstructorRequiringOtherThanContext_fails() {
116+
val kotlinModule =
117+
KotlinSourceFile(
118+
"Module.kt",
119+
"""
120+
import com.bumptech.glide.annotation.GlideModule
121+
import com.bumptech.glide.module.AppGlideModule
122+
123+
@GlideModule class Module(value: Int) : AppGlideModule()
124+
"""
125+
)
126+
val javaModule =
127+
JavaSourceFile(
128+
"Module.java",
129+
"""
130+
import com.bumptech.glide.annotation.GlideModule;
131+
import com.bumptech.glide.module.AppGlideModule;
132+
133+
@GlideModule public class Module extends AppGlideModule {
134+
public Module(Integer value) {}
135+
}
136+
"""
137+
)
138+
139+
compileCurrentSourceType(kotlinModule, javaModule) {
140+
assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
141+
assertThat(it.messages).contains(AppGlideModuleConstants.INVALID_MODULE_MESSAGE)
142+
}
143+
}
144+
145+
@Test
146+
fun compile_withAppGlideModuleConstructorRequiringMultipleArguments_fails() {
147+
val kotlinModule =
148+
KotlinSourceFile(
149+
"Module.kt",
150+
"""
151+
import android.content.Context
152+
import com.bumptech.glide.annotation.GlideModule
153+
import com.bumptech.glide.module.AppGlideModule
154+
155+
@GlideModule class Module(value: Context, otherValue: Int) : AppGlideModule()
156+
"""
157+
)
158+
val javaModule =
159+
JavaSourceFile(
160+
"Module.java",
161+
"""
162+
import android.content.Context;
163+
import com.bumptech.glide.annotation.GlideModule;
164+
import com.bumptech.glide.module.AppGlideModule;
165+
166+
@GlideModule public class Module extends AppGlideModule {
167+
public Module(Context value, int otherValue) {}
168+
}
169+
"""
170+
)
171+
172+
compileCurrentSourceType(kotlinModule, javaModule) {
173+
assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
174+
assertThat(it.messages).contains(AppGlideModuleConstants.INVALID_MODULE_MESSAGE)
175+
}
176+
}
177+
178+
// This is quite weird, we could probably pretty reasonably just assert that this doesn't happen.
179+
@Test
180+
fun compile_withAppGlideModuleWithOneEmptyrConstructor_andOneContextOnlyConstructor_usesTheContextOnlyConstructor() {
181+
val kotlinModule =
182+
KotlinSourceFile(
183+
"Module.kt",
184+
"""
185+
import android.content.Context
186+
import com.bumptech.glide.annotation.GlideModule
187+
import com.bumptech.glide.module.AppGlideModule
188+
189+
@GlideModule class Module(context: Context?) : AppGlideModule() {
190+
constructor() : this(null)
191+
}
192+
193+
"""
194+
)
195+
val javaModule =
196+
JavaSourceFile(
197+
"Module.java",
198+
"""
199+
import android.content.Context;
200+
import com.bumptech.glide.annotation.GlideModule;
201+
import com.bumptech.glide.module.AppGlideModule;
202+
import javax.annotation.Nullable;
203+
204+
@GlideModule public class Module extends AppGlideModule {
205+
public Module() {}
206+
public Module(@Nullable Context context) {}
207+
}
208+
"""
209+
)
210+
211+
compileCurrentSourceType(kotlinModule, javaModule) {
212+
assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
213+
assertThat(it.generatedAppGlideModuleContents()).hasSourceEqualTo(appGlideModuleWithContext)
214+
}
215+
}
216+
217+
@Test
218+
fun copmile_withMultipleAppGlideModules_failes() {
219+
val firstKtModule =
220+
KotlinSourceFile(
221+
"Module1.kt",
222+
"""
223+
import com.bumptech.glide.annotation.GlideModule
224+
import com.bumptech.glide.module.AppGlideModule
225+
226+
@GlideModule class Module1 : AppGlideModule()
227+
"""
228+
)
229+
230+
val secondKtModule =
231+
KotlinSourceFile(
232+
"Module2.kt",
233+
"""
234+
import com.bumptech.glide.annotation.GlideModule
235+
import com.bumptech.glide.module.AppGlideModule
236+
237+
@GlideModule class Module2 : AppGlideModule()
238+
"""
239+
)
240+
241+
val firstJavaModule =
242+
JavaSourceFile(
243+
"Module1.java",
244+
"""
245+
import com.bumptech.glide.annotation.GlideModule;
246+
import com.bumptech.glide.module.AppGlideModule;
247+
248+
@GlideModule public class Module1 extends AppGlideModule {
249+
public Module1() {}
250+
}
251+
"""
252+
)
253+
254+
val secondJavaModule =
255+
JavaSourceFile(
256+
"Module2.java",
257+
"""
258+
import com.bumptech.glide.annotation.GlideModule;
259+
import com.bumptech.glide.module.AppGlideModule;
260+
261+
@GlideModule public class Module2 extends AppGlideModule {
262+
public Module2() {}
263+
}
264+
"""
265+
)
266+
267+
compileCurrentSourceType(firstKtModule, secondKtModule, firstJavaModule, secondJavaModule) {
268+
assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
269+
assertThat(it.messages)
270+
.contains(
271+
GlideSymbolProcessorConstants.SINGLE_APP_MODULE_ERROR.format("[Module1, Module2]")
272+
)
273+
}
274+
}
275+
}
276+
277+
@Language("kotlin")
278+
const val simpleAppGlideModule =
279+
"""
280+
package com.bumptech.glide
281+
282+
import Module
283+
import android.content.Context
284+
import kotlin.Boolean
285+
import kotlin.Suppress
286+
import kotlin.Unit
287+
288+
internal class GeneratedAppGlideModuleImpl(
289+
@Suppress("UNUSED_VARIABLE")
290+
context: Context,
291+
) : GeneratedAppGlideModule() {
292+
private val appGlideModule: Module
293+
init {
294+
appGlideModule = Module()
295+
}
296+
297+
public override fun registerComponents(
298+
context: Context,
299+
glide: Glide,
300+
registry: Registry,
301+
): Unit {
302+
appGlideModule.registerComponents(context, glide, registry)
303+
}
304+
305+
public override fun applyOptions(context: Context, builder: GlideBuilder): Unit {
306+
appGlideModule.applyOptions(context, builder)
307+
}
308+
309+
public override fun isManifestParsingEnabled(): Boolean = false
310+
}
311+
"""
312+
313+
@Language("kotlin")
314+
const val appGlideModuleWithContext =
315+
"""
316+
package com.bumptech.glide
317+
318+
import Module
319+
import android.content.Context
320+
import kotlin.Boolean
321+
import kotlin.Unit
322+
323+
internal class GeneratedAppGlideModuleImpl(
324+
context: Context,
325+
) : GeneratedAppGlideModule() {
326+
private val appGlideModule: Module
327+
init {
328+
appGlideModule = Module(context)
329+
}
330+
331+
public override fun registerComponents(
332+
context: Context,
333+
glide: Glide,
334+
registry: Registry,
335+
): Unit {
336+
appGlideModule.registerComponents(context, glide, registry)
337+
}
338+
339+
public override fun applyOptions(context: Context, builder: GlideBuilder): Unit {
340+
appGlideModule.applyOptions(context, builder)
341+
}
342+
343+
public override fun isManifestParsingEnabled(): Boolean = false
344+
}
345+
"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.bumptech.glide.annotation.ksp.test
2+
3+
import com.bumptech.glide.annotation.ksp.GlideSymbolProcessorProvider
4+
import com.google.common.truth.StringSubject
5+
import com.tschuchort.compiletesting.KotlinCompilation
6+
import com.tschuchort.compiletesting.SourceFile
7+
import com.tschuchort.compiletesting.kspSourcesDir
8+
import com.tschuchort.compiletesting.symbolProcessorProviders
9+
import java.io.File
10+
import java.io.FileNotFoundException
11+
import org.intellij.lang.annotations.Language
12+
13+
internal class CompilationResult(
14+
private val compilation: KotlinCompilation,
15+
result: KotlinCompilation.Result,
16+
) {
17+
val exitCode = result.exitCode
18+
val messages = result.messages
19+
20+
fun generatedAppGlideModuleContents() = readFile(findAppGlideModule())
21+
22+
private fun readFile(file: File) = file.readLines().joinToString("\n")
23+
24+
private fun findAppGlideModule(): File {
25+
var currentDir: File? = compilation.kspSourcesDir
26+
listOf("kotlin", "com", "bumptech", "glide").forEach { directoryName ->
27+
currentDir = currentDir?.listFiles()?.find { it.name.equals(directoryName) }
28+
}
29+
return currentDir?.listFiles()?.find { it.name.equals("GeneratedAppGlideModuleImpl.kt") }
30+
?: throw FileNotFoundException(
31+
"GeneratedAppGlideModuleImpl.kt was not generated or not generated in the expected" +
32+
"location"
33+
)
34+
}
35+
}
36+
37+
enum class SourceType {
38+
KOTLIN,
39+
JAVA
40+
}
41+
42+
sealed interface TypedSourceFile {
43+
fun sourceFile(): SourceFile
44+
fun sourceType(): SourceType
45+
}
46+
47+
internal class KotlinSourceFile(
48+
val name: String,
49+
@Language("kotlin") val content: String,
50+
) : TypedSourceFile {
51+
override fun sourceFile() = SourceFile.kotlin(name, content)
52+
override fun sourceType() = SourceType.KOTLIN
53+
}
54+
55+
internal class JavaSourceFile(
56+
val name: String,
57+
@Language("java") val content: String,
58+
) : TypedSourceFile {
59+
override fun sourceFile() = SourceFile.java(name, content)
60+
override fun sourceType() = SourceType.JAVA
61+
}
62+
63+
internal interface PerSourceTypeTest {
64+
val sourceType: SourceType
65+
66+
fun compileCurrentSourceType(
67+
vararg sourceFiles: TypedSourceFile,
68+
test: (input: CompilationResult) -> Unit,
69+
) {
70+
test(
71+
compile(sourceFiles.filter { it.sourceType() == sourceType }.map { it.sourceFile() }.toList())
72+
)
73+
}
74+
}
75+
76+
internal fun compile(sourceFiles: List<SourceFile>): CompilationResult {
77+
require(sourceFiles.isNotEmpty())
78+
val compilation =
79+
KotlinCompilation().apply {
80+
sources = sourceFiles
81+
symbolProcessorProviders = listOf(GlideSymbolProcessorProvider())
82+
inheritClassPath = true
83+
}
84+
val result = compilation.compile()
85+
return CompilationResult(compilation, result)
86+
}
87+
88+
fun StringSubject.hasSourceEqualTo(sourceContents: String) = isEqualTo(sourceContents.trimIndent())

‎build.gradle

+13-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ buildscript {
2020
classpath "se.bjurr.violations:violations-gradle-plugin:${VIOLATIONS_PLUGIN_VERSION}"
2121
classpath "androidx.benchmark:benchmark-gradle-plugin:${ANDROID_X_BENCHMARK_VERSION}"
2222
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${JETBRAINS_KOTLIN_VERSION}"
23+
classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$KSP_GRADLE_PLUGIN_VERSION"
2324
}
2425
}
2526

@@ -41,11 +42,21 @@ subprojects { project ->
4142
url "https://oss.sonatype.org/content/repositories/snapshots"
4243
}
4344
gradlePluginPortal()
45+
46+
}
47+
48+
afterEvaluate {
49+
if (!project.plugins.hasPlugin("org.jetbrains.kotlin.jvm")) {
50+
tasks.withType(JavaCompile) {
51+
sourceCompatibility = 1.7
52+
targetCompatibility = 1.7
53+
}
54+
}
4455
}
4556

57+
4658
tasks.withType(JavaCompile) {
47-
sourceCompatibility = 1.7
48-
targetCompatibility = 1.7
59+
4960

5061
// gifencoder is a legacy project that has a ton of warnings and is basically never
5162
// modified, so we're not going to worry about cleaning it up.

‎gradle.properties

+3
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,12 @@ ANDROID_X_LIFECYCLE_KTX_VERSION=2.4.1
6565
# org.jetbrains versions
6666
JETBRAINS_KOTLINX_COROUTINES_VERSION=1.6.3
6767
JETBRAINS_KOTLIN_VERSION=1.7.0
68+
JETBRAINS_KOTLIN_TEST_VERSION=1.7.0
6869

6970
## Other dependency versions
7071
ANDROID_GRADLE_VERSION=7.2.1
7172
AUTO_SERVICE_VERSION=1.0-rc3
73+
KOTLIN_COMPILE_TESTING_VERSION=1.4.9
7274
DAGGER_VERSION=2.15
7375
ERROR_PRONE_PLUGIN_VERSION=2.0.2
7476
ERROR_PRONE_VERSION=2.3.4
@@ -77,6 +79,7 @@ GUAVA_VERSION=28.1-android
7779
JAVAPOET_VERSION=1.9.0
7880
JSR_305_VERSION=3.0.2
7981
JUNIT_VERSION=4.13.2
82+
KSP_GRADLE_PLUGIN_VERSION=1.7.0-1.0.6
8083
MOCKITO_ANDROID_VERSION=2.24.0
8184
MOCKITO_VERSION=2.24.0
8285
MOCKWEBSERVER_VERSION=3.0.0-RC1

‎library/src/main/java/com/bumptech/glide/GeneratedAppGlideModule.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import androidx.annotation.Nullable;
55
import com.bumptech.glide.manager.RequestManagerRetriever;
66
import com.bumptech.glide.module.AppGlideModule;
7+
import java.util.HashSet;
78
import java.util.Set;
89

910
/**
@@ -15,7 +16,9 @@
1516
abstract class GeneratedAppGlideModule extends AppGlideModule {
1617
/** This method can be removed when manifest parsing is no longer supported. */
1718
@NonNull
18-
abstract Set<Class<?>> getExcludedModuleClasses();
19+
Set<Class<?>> getExcludedModuleClasses() {
20+
return new HashSet<>();
21+
}
1922

2023
@Nullable
2124
RequestManagerRetriever.RequestManagerFactory getRequestManagerFactory() {

‎scripts/ci_unit.sh

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
set -e
44

5+
# TODO(judds): Remove the KSP tests when support is available to run them in
6+
# Google3
57
./gradlew build \
68
-x :samples:flickr:build \
79
-x :samples:giphy:build \
@@ -12,4 +14,5 @@ set -e
1214
:instrumentation:assembleAndroidTest \
1315
:benchmark:assembleAndroidTest \
1416
:glide:debugJavadoc \
17+
:annotation:ksp:test:test \
1518
--parallel

‎settings.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ include ':instrumentation'
99
include ':annotation'
1010
include ':annotation:compiler'
1111
//include ':annotation:compiler:test'
12+
include ':annotation:ksp'
13+
include ':annotation:ksp:test'
1214
include ':benchmark'
1315
include ':glide'
1416
include ':third_party:gif_decoder'

0 commit comments

Comments
 (0)
Please sign in to comment.