Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Add explicit keep rules for RxJava `Result` types to prevent their generic information from being removed.
- Add `allowoptimization` flags for most kept types.
- Add `Invocation.annotationUrl` which returns the original URL from the method annotation.
- Support using response type keeper with KSP.

**Changed**

Expand Down
5 changes: 5 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ okhttp = "5.3.0"
protobuf = "3.25.8"
robovm = "2.3.14"
kotlinx-serialization = "1.9.0"
kct = "0.10.0"
autoService = "1.1.1"
incap = "1.0.0"
jackson = "2.20.1"
Expand All @@ -33,6 +34,8 @@ kotlin-stdLib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-serializationPlugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" }

ksp-api = "com.google.devtools.ksp:symbol-processing-api:2.3.0"

errorpronePlugin = "net.ltgt.gradle:gradle-errorprone-plugin:4.3.0"
errorproneCore = { module = "com.google.errorprone:error_prone_core", version = "2.10.0" }
errorproneJavac = { module = "com.google.errorprone:javac", version = "9+181-r4173-1" }
Expand Down Expand Up @@ -78,3 +81,5 @@ googleJavaFormat = "com.google.googlejavaformat:google-java-format:1.31.0"
ktlint = "com.pinterest.ktlint:ktlint-cli:1.7.1"
compileTesting = "com.google.testing.compile:compile-testing:0.23.0"
testParameterInjector = "com.google.testparameterinjector:test-parameter-injector:1.19"
kct-core = { module = "dev.zacsweers.kctfork:core", version.ref = "kct" }
kct-ksp = { module = "dev.zacsweers.kctfork:ksp", version.ref = "kct" }
12 changes: 6 additions & 6 deletions retrofit-response-type-keeper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,21 @@ annotationProcessor 'com.squareup.retrofit2:response-type-keeper:<version>'
```
Or Gradle Kotlin projects with
```groovy
kapt 'com.squareup.retrofit2:response-type-keeper:<version>'
ksp 'com.squareup.retrofit2:response-type-keeper:<version>'
```

For other build systems, the `com.squareup.retrofit2:response-type-keeper` needs added to the Java
compiler `-processor` classpath.

For the example above, the annotation processor's generated file would contain
```
-keep com.example.User
```proguard
-keep,allowoptimization,allowshrinking,allowobfuscation class com.example.User
```

This works for nested generics, such as `Call<ApiResponse<User>>`, which would produce:
```
-keep com.example.ApiResponse
-keep com.example.User
```proguard
-keep,allowoptimization,allowshrinking,allowobfuscation class com.example.ApiResponse
-keep,allowoptimization,allowshrinking,allowobfuscation class com.example.User
```

It also works on Kotlin `suspend` functions which turn into a type like
Expand Down
5 changes: 5 additions & 0 deletions retrofit-response-type-keeper/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ apply plugin: 'org.jetbrains.kotlin.jvm'
apply plugin: 'com.vanniktech.maven.publish'

dependencies {
compileOnly libs.ksp.api

testImplementation libs.junit
testImplementation libs.compileTesting
testImplementation libs.kct.core
testImplementation libs.kct.ksp
testImplementation libs.truth
testImplementation libs.testParameterInjector
testImplementation projects.retrofit
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,7 @@ import javax.tools.StandardLocation.CLASS_OUTPUT

class RetrofitResponseTypeKeepProcessor : AbstractProcessor() {
override fun getSupportedSourceVersion() = SourceVersion.latestSupported()
override fun getSupportedAnnotationTypes() = setOf(
"retrofit2.http.DELETE",
"retrofit2.http.GET",
"retrofit2.http.HEAD",
"retrofit2.http.HTTP",
"retrofit2.http.OPTIONS",
"retrofit2.http.PATCH",
"retrofit2.http.POST",
"retrofit2.http.PUT",
)
override fun getSupportedAnnotationTypes() = annotationNames

override fun process(
annotations: Set<TypeElement>,
Expand Down Expand Up @@ -71,12 +62,11 @@ class RetrofitResponseTypeKeepProcessor : AbstractProcessor() {

for ((element, referencedTypes) in elementToReferencedTypes) {
val typeName = element.qualifiedName.toString()
val outputFile = "META-INF/proguard/retrofit-response-type-keeper-$typeName.pro"
val rules = processingEnv.filer.createResource(CLASS_OUTPUT, "", outputFile, element)
val rules = processingEnv.filer.createResource(CLASS_OUTPUT, "", proguardFilePath(typeName), element)
rules.openWriter().buffered().use { w ->
w.write("# $typeName\n")
for (referencedType in referencedTypes.sorted()) {
w.write("-keep,allowoptimization,allowshrinking,allowobfuscation class $referencedType\n")
w.write(keepRuleForType(referencedType))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (C) 2025 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2.keeper
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copyright header please


import com.google.devtools.ksp.processing.CodeGenerator
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.processing.SymbolProcessorProvider
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.Modifier

class RetrofitResponseTypeKeepSymbolProcessor(
environment: SymbolProcessorEnvironment,
) : SymbolProcessor {
private val codeGenerator: CodeGenerator = environment.codeGenerator

override fun process(resolver: Resolver): List<KSAnnotated> {
val elementToReferencedTypes = mutableMapOf<KSClassDeclaration, MutableSet<String>>()

annotationNames.flatMap { resolver.getSymbolsWithAnnotation(it) }
.filterIsInstance<KSFunctionDeclaration>()
.forEach { function ->
val serviceType = function.parentDeclaration as? KSClassDeclaration ?: return@forEach
val referenced = elementToReferencedTypes.getOrPut(serviceType, ::LinkedHashSet)

// Retrofit has special support for 'suspend fun' in Kotlin which manifests as a
// final Continuation parameter whose generic type is the declared return type.
if (function.modifiers.contains(Modifier.SUSPEND)) {
function.parameters.forEach {
it.type.resolve().recursiveParameterizedTypesTo(referenced)
}
}

val returnType = function.returnType?.resolve() ?: return@forEach
returnType.recursiveParameterizedTypesTo(referenced)
}

elementToReferencedTypes.forEach { (element, referencedTypes) ->
val containingFile = element.containingFile ?: return@forEach
val typeName = element.qualifiedName?.asString() ?: return@forEach

val dependencies = Dependencies(aggregating = false, containingFile)
codeGenerator.createNewFile(dependencies, "", proguardFilePath(typeName), "")
.bufferedWriter().use { w ->
w.write("# $typeName\n")
for (referencedType in referencedTypes.sorted()) {
w.write(keepRuleForType(referencedType))
}
}
}

return emptyList()
}

private fun KSType.recursiveParameterizedTypesTo(types: MutableSet<String>) {
val declaration = this.declaration
if (declaration is KSClassDeclaration) {
var qualifiedName = declaration.qualifiedName?.asString()
if (qualifiedName == "kotlin.Any") {
qualifiedName = "java.lang.Object"
}
Comment on lines +77 to +79
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object is kept in the annotation processor, so I patched the logic for the symbol processor. Seems we have no need to do so, I can address a new PR to remove them.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I left it in the annotation processor because it was harmless. I'm fine filtering them out if you want to do that in both.

qualifiedName?.let { types.add(it) }
}

for (typeArgument in arguments) {
typeArgument.type?.resolve()?.recursiveParameterizedTypesTo(types)
}
}

class Provider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor =
RetrofitResponseTypeKeepSymbolProcessor(environment)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2025 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2.keeper
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copyright header please


internal val annotationNames = setOf(
"retrofit2.http.DELETE",
"retrofit2.http.GET",
"retrofit2.http.HEAD",
"retrofit2.http.HTTP",
"retrofit2.http.OPTIONS",
"retrofit2.http.PATCH",
"retrofit2.http.POST",
"retrofit2.http.PUT",
)

internal fun keepRuleForType(referencedType: String): String =
"-keep,allowoptimization,allowshrinking,allowobfuscation class $referencedType\n"

internal fun proguardFilePath(typeName: String) =
"META-INF/proguard/retrofit-response-type-keeper-$typeName.pro"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
retrofit2.keeper.RetrofitResponseTypeKeepSymbolProcessor$Provider
Loading