-
Notifications
You must be signed in to change notification settings - Fork 7.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4067 from square/jw.apt.2024-01-29
Import response type keeper
- Loading branch information
Showing
7 changed files
with
345 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# Response Type Keeper | ||
|
||
Generates keep rules for types mentioned in generic parameter positions of Retrofit service methods. | ||
|
||
## Problem | ||
|
||
Given a service method like | ||
```java | ||
@GET("users/{id}") | ||
Call<User> getUser( | ||
@Path("id") String id); | ||
``` | ||
|
||
If you execute this request and do not actually use the returned `User` instance, R8 will remove it | ||
and replace the return type as `Call<?>`. This fails Retrofit's runtime validation since a wildcard | ||
is not a valid type to pass to a converter. Note: this removal only occurs if the Retrofit's service | ||
method definition is the only reference to `User`. | ||
|
||
## Solution | ||
|
||
This module contains an annotation processor which looks at each Retrofit method and generates | ||
explicit `-keep` rules for the types mentioned. | ||
|
||
Add it to Gradle Java projects with | ||
```groovy | ||
annotationProcessor 'com.squareup.retrofit2:response-type-keeper:<version>' | ||
``` | ||
Or Gradle Kotlin projects with | ||
```groovy | ||
kapt '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 | ||
``` | ||
|
||
This works for nested generics, such as `Call<ApiResponse<User>>`, which would produce: | ||
``` | ||
-keep com.example.ApiResponse | ||
-keep com.example.User | ||
``` | ||
|
||
It also works on Kotlin `suspend` functions which turn into a type like | ||
`Continuation<? extends User>` in the Java bytecode. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
apply plugin: 'org.jetbrains.kotlin.jvm' | ||
apply plugin: 'org.jetbrains.kotlin.kapt' | ||
apply plugin: 'com.vanniktech.maven.publish' | ||
|
||
dependencies { | ||
compileOnly libs.autoService.annotations | ||
compileOnly libs.incap.runtime | ||
kapt libs.autoService.compiler | ||
kapt libs.incap.processor | ||
|
||
testImplementation libs.junit | ||
testImplementation libs.compileTesting | ||
testImplementation libs.truth | ||
testImplementation projects.retrofit | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
POM_ARTIFACT_ID=response-type-keeper | ||
POM_NAME=Response Type Keeper | ||
POM_DESCRIPTION=Annotation processor to generate R8 keep rules for types mentioned in generics. |
106 changes: 106 additions & 0 deletions
106
...esponse-type-keeper/src/main/kotlin/retrofit2/keeper/RetrofitResponseTypeKeepProcessor.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
/* | ||
* Copyright (C) 2024 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 | ||
|
||
import com.google.auto.service.AutoService | ||
import javax.annotation.processing.AbstractProcessor | ||
import javax.annotation.processing.Processor | ||
import javax.annotation.processing.RoundEnvironment | ||
import javax.lang.model.SourceVersion | ||
import javax.lang.model.element.ExecutableElement | ||
import javax.lang.model.element.TypeElement | ||
import javax.lang.model.type.DeclaredType | ||
import javax.lang.model.type.TypeMirror | ||
import javax.lang.model.type.WildcardType | ||
import javax.tools.StandardLocation.CLASS_OUTPUT | ||
import net.ltgt.gradle.incap.IncrementalAnnotationProcessor | ||
import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType.ISOLATING | ||
|
||
@AutoService(Processor::class) | ||
@IncrementalAnnotationProcessor(ISOLATING) | ||
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 process( | ||
annotations: Set<TypeElement>, | ||
roundEnv: RoundEnvironment, | ||
): Boolean { | ||
val elements = processingEnv.elementUtils | ||
val types = processingEnv.typeUtils | ||
|
||
val methods = supportedAnnotationTypes | ||
.mapNotNull(elements::getTypeElement) | ||
.flatMap(roundEnv::getElementsAnnotatedWith) | ||
|
||
val elementToReferencedTypes = mutableMapOf<TypeElement, MutableSet<String>>() | ||
for (method in methods) { | ||
val executableElement = method as ExecutableElement | ||
|
||
val serviceType = method.enclosingElement as TypeElement | ||
val referenced = elementToReferencedTypes.getOrPut(serviceType, ::LinkedHashSet) | ||
|
||
val returnType = executableElement.returnType as DeclaredType | ||
returnType.recursiveParameterizedTypesTo(referenced) | ||
|
||
// Retrofit has special support for 'suspend fun' in Kotlin which manifests as a | ||
// final Continuation parameter whose generic type is the declared return type. | ||
executableElement.parameters | ||
.lastOrNull() | ||
?.asType() | ||
?.takeIf { types.erasure(it).toString() == "kotlin.coroutines.Continuation" } | ||
?.let { (it as DeclaredType).typeArguments.single() } | ||
?.recursiveParameterizedTypesTo(referenced) | ||
} | ||
|
||
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) | ||
rules.openWriter().buffered().use { w -> | ||
w.write("# $typeName\n") | ||
for (referencedType in referencedTypes.sorted()) { | ||
w.write("-keep,allowobfuscation,allowoptimization class $referencedType\n") | ||
} | ||
} | ||
} | ||
return false | ||
} | ||
|
||
private fun TypeMirror.recursiveParameterizedTypesTo(types: MutableSet<String>) { | ||
when (this) { | ||
is WildcardType -> { | ||
extendsBound?.recursiveParameterizedTypesTo(types) | ||
superBound?.recursiveParameterizedTypesTo(types) | ||
} | ||
is DeclaredType -> { | ||
for (typeArgument in typeArguments) { | ||
typeArgument.recursiveParameterizedTypesTo(types) | ||
} | ||
types += (asElement() as TypeElement).qualifiedName.toString() | ||
} | ||
} | ||
} | ||
} |
162 changes: 162 additions & 0 deletions
162
...nse-type-keeper/src/test/kotlin/retrofit2/keeper/RetrofitResponseTypeKeepProcessorTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
/* | ||
* Copyright (C) 2024 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 | ||
|
||
import com.google.common.truth.Truth.assertAbout | ||
import com.google.testing.compile.JavaFileObjects | ||
import com.google.testing.compile.JavaSourceSubjectFactory.javaSource | ||
import java.nio.charset.StandardCharsets.UTF_8 | ||
import javax.tools.StandardLocation.CLASS_OUTPUT | ||
import org.junit.Test | ||
|
||
class RetrofitResponseTypeKeepProcessorTest { | ||
@Test | ||
fun allHttpMethods() { | ||
val service = JavaFileObjects.forSourceString( | ||
"test.Service", | ||
""" | ||
package test; | ||
import retrofit2.*; | ||
import retrofit2.http.*; | ||
class DeleteUser {} | ||
class GetUser {} | ||
class HeadUser {} | ||
class HttpUser {} | ||
class OptionsUser {} | ||
class PatchUser {} | ||
class PostUser {} | ||
class PutUser {} | ||
interface Service { | ||
@DELETE("/") Call<DeleteUser> delete(); | ||
@GET("/") Call<GetUser> get(); | ||
@HEAD("/") Call<HeadUser> head(); | ||
@HTTP(method = "CUSTOM", path = "/") Call<HttpUser> http(); | ||
@OPTIONS("/") Call<OptionsUser> options(); | ||
@PATCH("/") Call<PatchUser> patch(); | ||
@POST("/") Call<PostUser> post(); | ||
@PUT("/") Call<PutUser> put(); | ||
} | ||
""".trimIndent(), | ||
) | ||
|
||
assertAbout(javaSource()) | ||
.that(service) | ||
.processedWith(RetrofitResponseTypeKeepProcessor()) | ||
.compilesWithoutError() | ||
.and() | ||
.generatesFileNamed( | ||
CLASS_OUTPUT, | ||
"", | ||
"META-INF/proguard/retrofit-response-type-keeper-test.Service.pro", | ||
).withStringContents( | ||
UTF_8, | ||
""" | ||
|# test.Service | ||
|-keep,allowobfuscation,allowoptimization class retrofit2.Call | ||
|-keep,allowobfuscation,allowoptimization class test.DeleteUser | ||
|-keep,allowobfuscation,allowoptimization class test.GetUser | ||
|-keep,allowobfuscation,allowoptimization class test.HeadUser | ||
|-keep,allowobfuscation,allowoptimization class test.HttpUser | ||
|-keep,allowobfuscation,allowoptimization class test.OptionsUser | ||
|-keep,allowobfuscation,allowoptimization class test.PatchUser | ||
|-keep,allowobfuscation,allowoptimization class test.PostUser | ||
|-keep,allowobfuscation,allowoptimization class test.PutUser | ||
| | ||
""".trimMargin(), | ||
) | ||
} | ||
|
||
@Test | ||
fun nesting() { | ||
val service = JavaFileObjects.forSourceString( | ||
"test.Service", | ||
""" | ||
package test; | ||
import retrofit2.*; | ||
import retrofit2.http.*; | ||
class One<T> {} | ||
class Two<T> {} | ||
class Three {} | ||
interface Service { | ||
@GET("/") Call<One<Two<Three>>> get(); | ||
} | ||
""".trimIndent(), | ||
) | ||
|
||
assertAbout(javaSource()) | ||
.that(service) | ||
.processedWith(RetrofitResponseTypeKeepProcessor()) | ||
.compilesWithoutError() | ||
.and() | ||
.generatesFileNamed( | ||
CLASS_OUTPUT, | ||
"", | ||
"META-INF/proguard/retrofit-response-type-keeper-test.Service.pro", | ||
).withStringContents( | ||
UTF_8, | ||
""" | ||
|# test.Service | ||
|-keep,allowobfuscation,allowoptimization class retrofit2.Call | ||
|-keep,allowobfuscation,allowoptimization class test.One | ||
|-keep,allowobfuscation,allowoptimization class test.Three | ||
|-keep,allowobfuscation,allowoptimization class test.Two | ||
| | ||
""".trimMargin(), | ||
) | ||
} | ||
|
||
@Test | ||
fun kotlinSuspend() { | ||
val service = JavaFileObjects.forSourceString( | ||
"test.Service", | ||
""" | ||
package test; | ||
import kotlin.coroutines.Continuation; | ||
import retrofit2.*; | ||
import retrofit2.http.*; | ||
class Body {} | ||
interface Service { | ||
@GET("/") Object get(Continuation<? extends Body> c); | ||
} | ||
""".trimIndent(), | ||
) | ||
|
||
assertAbout(javaSource()) | ||
.that(service) | ||
.processedWith(RetrofitResponseTypeKeepProcessor()) | ||
.compilesWithoutError() | ||
.and() | ||
.generatesFileNamed( | ||
CLASS_OUTPUT, | ||
"", | ||
"META-INF/proguard/retrofit-response-type-keeper-test.Service.pro", | ||
).withStringContents( | ||
UTF_8, | ||
""" | ||
|# test.Service | ||
|-keep,allowobfuscation,allowoptimization class java.lang.Object | ||
|-keep,allowobfuscation,allowoptimization class test.Body | ||
| | ||
""".trimMargin(), | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters