Add ProGuard / R8 integration tests & add default ProGuard rules (#2397)

* Add code shrinking tools integration test

* Keep no-args constructor of classes usable with JsonAdapter

* Add library ProGuard rules for Gson

They are automatically applied for all users of Gson, see
https://developer.android.com/build/shrink-code#configuration-files

* Skip japicmp-maven-plugin for shrinker-test

* Add more tests for JsonAdapter, add tests for generic classes

* Extend default constructor test

* Add Troubleshooting Guide entry for TypeToken
This commit is contained in:
Marcono1234 2023-05-28 21:24:05 +02:00 committed by GitHub
parent 4c65a82871
commit 43396e45fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1218 additions and 29 deletions

View File

@ -281,3 +281,49 @@ Class.forName(jsonString, false, getClass().getClassLoader()).asSubclass(MyBaseC
```
This will not initialize arbitrary classes, and it will throw a `ClassCastException` if the loaded class is not the same as or a subclass of `MyBaseClass`.
## <a id="type-token-raw"></a> `IllegalStateException`: 'TypeToken must be created with a type argument' <br> `RuntimeException`: 'Missing type parameter'
**Symptom:** An `IllegalStateException` with the message 'TypeToken must be created with a type argument' is thrown.
For older Gson versions a `RuntimeException` with message 'Missing type parameter' is thrown.
**Reason:**
- You created a `TypeToken` without type argument, for example `new TypeToken() {}` (note the missing `<...>`). You always have to provide the type argument, for example like this: `new TypeToken<List<String>>() {}`. Normally the compiler will also emit a 'raw types' warning when you forget the `<...>`.
- You are using a code shrinking tool such as ProGuard or R8 (Android app builds normally have this enabled by default) but have not configured it correctly for usage with Gson.
**Solution:** When you are using a code shrinking tool such as ProGuard or R8 you have to adjust your configuration to include the following rules:
```
# Keep generic signatures; needed for correct type resolution
-keepattributes Signature
# Keep class TypeToken (respectively its generic signature)
-keep class com.google.gson.reflect.TypeToken { *; }
# Keep any (anonymous) classes extending TypeToken
-keep class * extends com.google.gson.reflect.TypeToken
```
See also the [Android example](examples/android-proguard-example/README.md) for more information.
Note: For newer Gson versions these rules might be applied automatically; make sure you are using the latest Gson version and the latest version of the code shrinking tool.
## <a id="r8-abstract-class"></a> `JsonIOException`: 'Abstract classes can't be instantiated!' (R8)
**Symptom:** A `JsonIOException` with the message 'Abstract classes can't be instantiated!' is thrown; the class mentioned in the exception message is not actually `abstract` in your source code, and you are using the code shrinking tool R8 (Android app builds normally have this configured by default).
**Reason:** The code shrinking tool R8 performs optimizations where it removes the no-args constructor from a class and makes the class `abstract`. Due to this Gson cannot create an instance of the class.
**Solution:** Make sure the class has a no-args constructor, then adjust your R8 configuration file to keep the constructor of the class. For example:
```
# Keep the no-args constructor of the deserialized class
-keepclassmembers class com.example.MyClass {
<init>();
}
```
For Android you can add this rule to the `proguard-rules.pro` file, see also the [Android documentation](https://developer.android.com/build/shrink-code#keep-code). In case the class name in the exception message is obfuscated, see the Android documentation about [retracing](https://developer.android.com/build/shrink-code#retracing).
Note: If the class which you are trying to deserialize is actually abstract, then this exception is probably unrelated to R8 and you will have to implement a custom [`InstanceCreator`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/InstanceCreator.html) or [`TypeAdapter`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html) which creates an instance of a non-abstract subclass of the class.

View File

@ -6,7 +6,12 @@ or remove them if they appear to be unused. This can cause issues for Gson which
access the fields of a class. It is necessary to configure ProGuard to make sure that Gson works correctly.
Also have a look at the [ProGuard manual](https://www.guardsquare.com/manual/configuration/usage#keepoverview)
for more details on how ProGuard can be configured.
and the [ProGuard Gson examples](https://www.guardsquare.com/manual/configuration/examples#gson) for more
details on how ProGuard can be configured.
The R8 code shrinker uses the same rule format as ProGuard, but there are differences between these two
tools. Have a look at R8's Compatibility FAQ, and especially at the [Gson section](https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#gson).
Note that newer Gson versions apply some of the rules shown in `proguard.cfg` automatically by default,
see the file [`gson/META-INF/proguard/gson.pro`](/gson/src/main/resources/META-INF/proguard/gson.pro) for
the Gson version you are using.

View File

@ -50,6 +50,7 @@
<artifactId>jsr250-api</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>

View File

@ -57,7 +57,6 @@
<dependency>
<groupId>com.google.truth</groupId>
<artifactId>truth</artifactId>
<version>1.1.3</version>
<scope>test</scope>
</dependency>
<dependency>
@ -84,7 +83,7 @@
<goal>filter-sources</goal>
</goals>
<configuration>
<sourceDirectory>${basedir}/src/main/java-templates</sourceDirectory>
<sourceDirectory>${project.basedir}/src/main/java-templates</sourceDirectory>
<outputDirectory>${project.build.directory}/generated-sources/java-templates</outputDirectory>
</configuration>
</execution>
@ -192,7 +191,7 @@
<injar>test-classes-obfuscated-injar</injar>
<outjar>test-classes-obfuscated-outjar</outjar>
<inFilter>**/*.class</inFilter>
<proguardInclude>${basedir}/src/test/resources/testcases-proguard.conf</proguardInclude>
<proguardInclude>${project.basedir}/src/test/resources/testcases-proguard.conf</proguardInclude>
<libs>
<lib>${project.build.directory}/classes</lib>
<lib>${java.home}/jmods/java.base.jmod</lib>

View File

@ -70,12 +70,20 @@ public final class ConstructorConstructor {
static String checkInstantiable(Class<?> c) {
int modifiers = c.getModifiers();
if (Modifier.isInterface(modifiers)) {
return "Interfaces can't be instantiated! Register an InstanceCreator "
+ "or a TypeAdapter for this type. Interface name: " + c.getName();
return "Interfaces can't be instantiated! Register an InstanceCreator"
+ " or a TypeAdapter for this type. Interface name: " + c.getName();
}
if (Modifier.isAbstract(modifiers)) {
return "Abstract classes can't be instantiated! Register an InstanceCreator "
+ "or a TypeAdapter for this type. Class name: " + c.getName();
// R8 performs aggressive optimizations where it removes the default constructor of a class
// and makes the class `abstract`; check for that here explicitly
if (c.getDeclaredConstructors().length == 0) {
return "Abstract classes can't be instantiated! Adjust the R8 configuration or register"
+ " an InstanceCreator or a TypeAdapter for this type. Class name: " + c.getName()
+ "\nSee " + TroubleshootingGuide.createUrl("r8-abstract-class");
}
return "Abstract classes can't be instantiated! Register an InstanceCreator"
+ " or a TypeAdapter for this type. Class name: " + c.getName();
}
return null;
}
@ -144,9 +152,9 @@ public final class ConstructorConstructor {
// finally try unsafe
return newUnsafeAllocator(rawType);
} else {
final String message = "Unable to create instance of " + rawType + "; ReflectionAccessFilter "
+ "does not permit using reflection or Unsafe. Register an InstanceCreator or a TypeAdapter "
+ "for this type or adjust the access filter to allow using reflection.";
final String message = "Unable to create instance of " + rawType + "; ReflectionAccessFilter"
+ " does not permit using reflection or Unsafe. Register an InstanceCreator or a TypeAdapter"
+ " for this type or adjust the access filter to allow using reflection.";
return new ObjectConstructor<T>() {
@Override public T construct() {
throw new JsonIOException(message);
@ -219,10 +227,10 @@ public final class ConstructorConstructor {
&& (filterResult != FilterResult.BLOCK_ALL || Modifier.isPublic(constructor.getModifiers())));
if (!canAccess) {
final String message = "Unable to invoke no-args constructor of " + rawType + "; "
+ "constructor is not accessible and ReflectionAccessFilter does not permit making "
+ "it accessible. Register an InstanceCreator or a TypeAdapter for this type, change "
+ "the visibility of the constructor or adjust the access filter.";
final String message = "Unable to invoke no-args constructor of " + rawType + ";"
+ " constructor is not accessible and ReflectionAccessFilter does not permit making"
+ " it accessible. Register an InstanceCreator or a TypeAdapter for this type, change"
+ " the visibility of the constructor or adjust the access filter.";
return new ObjectConstructor<T>() {
@Override public T construct() {
throw new JsonIOException(message);
@ -370,19 +378,29 @@ public final class ConstructorConstructor {
T newInstance = (T) UnsafeAllocator.INSTANCE.newInstance(rawType);
return newInstance;
} catch (Exception e) {
throw new RuntimeException(("Unable to create instance of " + rawType + ". "
+ "Registering an InstanceCreator or a TypeAdapter for this type, or adding a no-args "
+ "constructor may fix this problem."), e);
throw new RuntimeException(("Unable to create instance of " + rawType + "."
+ " Registering an InstanceCreator or a TypeAdapter for this type, or adding a no-args"
+ " constructor may fix this problem."), e);
}
}
};
} else {
final String exceptionMessage = "Unable to create instance of " + rawType + "; usage of JDK Unsafe "
+ "is disabled. Registering an InstanceCreator or a TypeAdapter for this type, adding a no-args "
+ "constructor, or enabling usage of JDK Unsafe may fix this problem.";
String exceptionMessage = "Unable to create instance of " + rawType + "; usage of JDK Unsafe"
+ " is disabled. Registering an InstanceCreator or a TypeAdapter for this type, adding a no-args"
+ " constructor, or enabling usage of JDK Unsafe may fix this problem.";
// Check if R8 removed all constructors
if (rawType.getDeclaredConstructors().length == 0) {
// R8 with Unsafe disabled might not be common enough to warrant a separate Troubleshooting Guide entry
exceptionMessage += " Or adjust your R8 configuration to keep the no-args constructor of the class.";
}
// Explicit final variable to allow usage in the anonymous class below
final String exceptionMessageF = exceptionMessage;
return new ObjectConstructor<T>() {
@Override public T construct() {
throw new JsonIOException(exceptionMessage);
throw new JsonIOException(exceptionMessageF);
}
};
}

View File

@ -51,6 +51,8 @@ public final class JsonAdapterAnnotationTypeAdapterFactory implements TypeAdapte
TypeAdapter<?> getTypeAdapter(ConstructorConstructor constructorConstructor, Gson gson,
TypeToken<?> type, JsonAdapter annotation) {
// TODO: The exception messages created by ConstructorConstructor are currently written in the context of
// deserialization and for example suggest usage of TypeAdapter, which would not work for @JsonAdapter usage
Object instance = constructorConstructor.get(TypeToken.get(annotation.value())).construct();
TypeAdapter<?> typeAdapter;

View File

@ -17,6 +17,7 @@
package com.google.gson.reflect;
import com.google.gson.internal.$Gson$Types;
import com.google.gson.internal.TroubleshootingGuide;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
@ -97,8 +98,10 @@ public class TypeToken<T> {
}
// Check for raw TypeToken as superclass
else if (superclass == TypeToken.class) {
throw new IllegalStateException("TypeToken must be created with a type argument: new TypeToken<...>() {}; "
+ "When using code shrinkers (ProGuard, R8, ...) make sure that generic signatures are preserved.");
throw new IllegalStateException("TypeToken must be created with a type argument: new TypeToken<...>() {};"
+ " When using code shrinkers (ProGuard, R8, ...) make sure that generic signatures are preserved."
+ "\nSee " + TroubleshootingGuide.createUrl("type-token-raw")
);
}
// User created subclass of subclass of TypeToken

View File

@ -0,0 +1,58 @@
### Gson ProGuard and R8 rules which are relevant for all users
### This file is automatically recognized by ProGuard and R8, see https://developer.android.com/build/shrink-code#configuration-files
###
### IMPORTANT:
### - These rules are additive; don't include anything here which is not specific to Gson (such as completely
### disabling obfuscation for all classes); the user would be unable to disable that then
### - These rules are not complete; users will most likely have to add additional rules for their specific
### classes, for example to disable obfuscation for certain fields or to keep no-args constructors
###
# Keep generic signatures; needed for correct type resolution
-keepattributes Signature
# Keep Gson annotations
# Note: Cannot perform finer selection here to only cover Gson annotations, see also https://stackoverflow.com/q/47515093
-keepattributes *Annotation*
### The following rules are needed for R8 in "full mode" which only adheres to `-keepattribtues` if
### the corresponding class or field is matches by a `-keep` rule as well, see
### https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md#r8-full-mode
# Keep class TypeToken (respectively its generic signature)
-keep class com.google.gson.reflect.TypeToken { *; }
# Keep any (anonymous) classes extending TypeToken
-keep class * extends com.google.gson.reflect.TypeToken
# Keep classes with @JsonAdapter annotation
-keep @com.google.gson.annotations.JsonAdapter class *
# Keep fields with @SerializedName annotation, but allow obfuscation of their names
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# Keep fields with any other Gson annotation
-keepclassmembers class * {
@com.google.gson.annotations.Expose <fields>;
@com.google.gson.annotations.JsonAdapter <fields>;
@com.google.gson.annotations.Since <fields>;
@com.google.gson.annotations.Until <fields>;
}
# Keep no-args constructor of classes which can be used with @JsonAdapter
# By default their no-args constructor is invoked to create an adapter instance
-keep class * extends com.google.gson.TypeAdapter {
<init>();
}
-keep class * implements com.google.gson.TypeAdapterFactory {
<init>();
}
-keep class * implements com.google.gson.JsonSerializer {
<init>();
}
-keep class * implements com.google.gson.JsonDeserializer {
<init>();
}

View File

@ -260,8 +260,10 @@ public final class TypeTokenTest {
new TypeToken() {};
fail();
} catch (IllegalStateException expected) {
assertThat(expected).hasMessageThat().isEqualTo("TypeToken must be created with a type argument: new TypeToken<...>() {}; "
+ "When using code shrinkers (ProGuard, R8, ...) make sure that generic signatures are preserved.");
assertThat(expected).hasMessageThat().isEqualTo("TypeToken must be created with a type argument: new TypeToken<...>() {};"
+ " When using code shrinkers (ProGuard, R8, ...) make sure that generic signatures are preserved."
+ "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#type-token-raw"
);
}
}
}

View File

@ -63,7 +63,6 @@
<plugin>
<groupId>com.github.siom79.japicmp</groupId>
<artifactId>japicmp-maven-plugin</artifactId>
<version>0.17.2</version>
<configuration>
<!-- This module is not supposed to be consumed as library, so no need to check API -->
<skip>true</skip>

View File

@ -29,6 +29,7 @@
<modules>
<module>gson</module>
<module>shrinker-test</module>
<module>extras</module>
<module>metrics</module>
<module>proto</module>
@ -83,7 +84,12 @@
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.truth</groupId>
<artifactId>truth</artifactId>
<version>1.1.3</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -62,7 +62,6 @@
<dependency>
<groupId>com.google.truth</groupId>
<artifactId>truth</artifactId>
<version>1.1.3</version>
<scope>test</scope>
</dependency>
</dependencies>

9
shrinker-test/README.md Normal file
View File

@ -0,0 +1,9 @@
# shrinker-test
This Maven module contains integration tests which check the behavior of Gson when used in combination with code shrinking and obfuscation tools, such as ProGuard or R8.
The code which is shrunken is under `src/main/java`; it should not contain any important assertions in case the code shrinking tools affect these assertions in any way. The test code under `src/test/java` executes the shrunken and obfuscated JAR and verifies that it behaves as expected.
The tests might be a bit brittle, especially the R8 test setup. Future ProGuard and R8 versions might cause the tests to behave differently. In case tests fail the ProGuard and R8 mapping files created in the `target` directory can help with debugging. If necessary rewrite tests or even remove them if they cannot be implemented anymore for newer ProGuard or R8 versions.
**Important:** Because execution of the code shrinking tools is performed during the Maven build, trying to directly run the integration tests from the IDE might not work, or might use stale results if you changed the configuration in between. Run `mvn clean verify` before trying to run the integration tests from the IDE.

218
shrinker-test/pom.xml Normal file
View File

@ -0,0 +1,218 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2023 Google 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.google.code.gson</groupId>
<artifactId>gson-parent</artifactId>
<version>2.10.2-SNAPSHOT</version>
</parent>
<artifactId>shrinker-test</artifactId>
<properties>
<maven.compiler.release>8</maven.compiler.release>
</properties>
<pluginRepositories>
<!-- R8 currently only exists in Google Maven repository -->
<pluginRepository>
<id>google</id>
<url>https://maven.google.com</url>
</pluginRepository>
</pluginRepositories>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.truth</groupId>
<artifactId>truth</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>com.github.siom79.japicmp</groupId>
<artifactId>japicmp-maven-plugin</artifactId>
<configuration>
<!-- This module is not supposed to be consumed as library, so no need to check API -->
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<!-- Not deployed -->
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<!-- Process JAR with ProGuard -->
<plugin>
<groupId>com.github.wvengen</groupId>
<artifactId>proguard-maven-plugin</artifactId>
<version>2.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>proguard</goal>
</goals>
</execution>
</executions>
<configuration>
<obfuscate>true</obfuscate>
<proguardInclude>${project.basedir}/proguard.pro</proguardInclude>
<options>
<!-- Hacky solution to make ProGuard use the library rules file; only the Android plugin of ProGuard
seems to consider it automatically at the moment, see https://github.com/Guardsquare/proguard/issues/337
However, R8 defined further below always considers it automatically -->
<option>-include</option><option>${project.basedir}/../gson/src/main/resources/META-INF/proguard/gson.pro</option>
</options>
<libs>
<lib>${java.home}/jmods/java.base.jmod</lib>
<!-- Used by Gson for optional SQL types support -->
<lib>${java.home}/jmods/java.sql.jmod</lib>
<!-- Used by transitive Error Prone annotations dependency -->
<lib>${java.home}/jmods/java.compiler.jmod</lib>
</libs>
<!-- Include dependencies in the final JAR -->
<includeDependencyInjar>true</includeDependencyInjar>
<outjar>proguard-output.jar</outjar>
</configuration>
</plugin>
<!-- Prepare a JAR with dependencies for R8 -->
<!-- Once there is a proper R8 Maven plugin in the future, prefer that and provide
dependencies as additional input JARs there instead of using the Shade plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<!-- Replace the main JAR -->
<shadedArtifactAttached>false</shadedArtifactAttached>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<!-- Ignore duplicate files in dependencies -->
<exclude>META-INF/MANIFEST.MF</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
<!-- Process JAR with R8; currently has no dedicated plugin so use Exec Maven Plugin instead -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>r8</id>
<phase>package</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<!-- R8 runs as standalone JAR, does not need any of the project classes -->
<addOutputToClasspath>false</addOutputToClasspath>
<includeProjectDependencies>false</includeProjectDependencies>
<!-- R8 is specified as plugin dependency, see further below -->
<includePluginDependencies>true</includePluginDependencies>
<executableDependency>
<!-- Uses R8 dependency declared below -->
<groupId>com.android.tools</groupId>
<artifactId>r8</artifactId>
</executableDependency>
<!-- See https://r8.googlesource.com/r8/+/refs/heads/main/README.md#running-r8 -->
<!-- Without `pg-compat` argument this acts like "full mode", see
https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md#r8-full-mode -->
<mainClass>com.android.tools.r8.R8</mainClass>
<arguments>
<argument>--release</argument>
<!-- Produce Java class files instead of Android DEX files -->
<argument>--classfile</argument>
<argument>--lib</argument><argument>${java.home}</argument>
<argument>--pg-conf</argument><argument>${project.basedir}/r8.pro</argument>
<!-- Create mapping file to make debugging test failures easier -->
<argument>--pg-map-output</argument><argument>${project.build.directory}/r8_map.txt</argument>
<argument>--output</argument><argument>${project.build.directory}/r8-output.jar</argument>
<argument>${project.build.directory}/${project.build.finalName}.jar</argument>
</arguments>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<!-- R8 dependency used above -->
<groupId>com.android.tools</groupId>
<artifactId>r8</artifactId>
<version>8.0.40</version>
</dependency>
</dependencies>
</plugin>
<!-- Run integration tests to verify shrunken JAR behavior -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

29
shrinker-test/proguard.pro vendored Normal file
View File

@ -0,0 +1,29 @@
### Common rules for ProGuard and R8
### Should only contains rules needed specifically for the integration test;
### any general rules which are relevant for all users should not be here but in `META-INF/proguard` of Gson
-allowaccessmodification
# On Windows mixed case class names might cause problems
-dontusemixedcaseclassnames
# Ignore notes about duplicate JDK classes
-dontnote module-info,jdk.internal.**
# Keep test entrypoints
-keep class com.example.Main {
public static void runTests(...);
}
-keep class com.example.DefaultConstructorMain {
public static java.lang.String runTest();
public static java.lang.String runTestNoJdkUnsafe();
}
### Test data setup
# Keep fields without annotations which should be preserved
-keepclassmembers class com.example.ClassWithNamedFields {
!transient <fields>;
}

36
shrinker-test/r8.pro Normal file
View File

@ -0,0 +1,36 @@
# Extend the ProGuard rules
-include proguard.pro
### The following rules are needed for R8 in "full mode", which performs more aggressive optimizations than ProGuard
### See https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md#r8-full-mode
# Keep the no-args constructor of deserialized classes
-keepclassmembers class com.example.ClassWithDefaultConstructor {
<init>();
}
-keepclassmembers class com.example.GenericClasses$GenericClass {
<init>();
}
-keepclassmembers class com.example.GenericClasses$UsingGenericClass {
<init>();
}
-keepclassmembers class com.example.GenericClasses$GenericUsingGenericClass {
<init>();
}
# For classes with generic type parameter R8 in "full mode" requires to have a keep rule to
# preserve the generic signature
-keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.example.GenericClasses$GenericClass
-keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.example.GenericClasses$GenericUsingGenericClass
# Don't obfuscate class name, to check it in exception message
-keep,allowshrinking,allowoptimization class com.example.DefaultConstructorMain$TestClass
# This rule has the side-effect that R8 still removes the no-args constructor, but does not make the class abstract
-keep class com.example.DefaultConstructorMain$TestClassNotAbstract {
@com.google.gson.annotations.SerializedName <fields>;
}
# Keep enum constants which are not explicitly used in code
-keep class com.example.EnumClass {
** SECOND;
}

View File

@ -0,0 +1,44 @@
package com.example;
import com.google.gson.TypeAdapter;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
@JsonAdapter(ClassWithAdapter.Adapter.class)
public class ClassWithAdapter {
static class Adapter extends TypeAdapter<ClassWithAdapter> {
@Override
public ClassWithAdapter read(JsonReader in) throws IOException {
in.beginObject();
String name = in.nextName();
if (!name.equals("custom")) {
throw new IllegalArgumentException("Unexpected name: " + name);
}
int i = in.nextInt();
in.endObject();
return new ClassWithAdapter(i);
}
@Override
public void write(JsonWriter out, ClassWithAdapter value) throws IOException {
out.beginObject();
out.name("custom");
out.value(value.i);
out.endObject();
}
}
public Integer i;
public ClassWithAdapter(int i) {
this.i = i;
}
@Override
public String toString() {
return "ClassWithAdapter[" + i + "]";
}
}

View File

@ -0,0 +1,12 @@
package com.example;
import com.google.gson.annotations.SerializedName;
public class ClassWithDefaultConstructor {
@SerializedName("myField")
public int i;
public ClassWithDefaultConstructor() {
i = -3;
}
}

View File

@ -0,0 +1,13 @@
package com.example;
import com.google.gson.annotations.Expose;
/**
* Uses {@link Expose} annotation.
*/
public class ClassWithExposeAnnotation {
@Expose
int i;
int i2;
}

View File

@ -0,0 +1,126 @@
package com.example;
import com.google.gson.Gson;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.lang.reflect.Type;
/**
* Uses {@link JsonAdapter} annotation on fields.
*/
public class ClassWithJsonAdapterAnnotation {
// For this field don't use @SerializedName and ignore it for deserialization
@JsonAdapter(value = Adapter.class, nullSafe = false)
DummyClass f;
@SerializedName("f1")
@JsonAdapter(Adapter.class)
DummyClass f1;
@SerializedName("f2")
@JsonAdapter(Factory.class)
DummyClass f2;
@SerializedName("f3")
@JsonAdapter(Serializer.class)
DummyClass f3;
@SerializedName("f4")
@JsonAdapter(Deserializer.class)
DummyClass f4;
public ClassWithJsonAdapterAnnotation() {
}
// Note: R8 seems to make this constructor the no-args constructor and initialize fields
// by default; currently this is not visible in the deserialization test because the JSON data
// contains values for all fields; but it is noticeable once the JSON data is missing fields
public ClassWithJsonAdapterAnnotation(int i1, int i2, int i3, int i4) {
f1 = new DummyClass(Integer.toString(i1));
f2 = new DummyClass(Integer.toString(i2));
f3 = new DummyClass(Integer.toString(i3));
f4 = new DummyClass(Integer.toString(i4));
// Note: Deliberately don't initialize field `f` here to not refer to it anywhere in code
}
@Override
public String toString() {
return "ClassWithJsonAdapterAnnotation[f1=" + f1 + ", f2=" + f2 + ", f3=" + f3 + ", f4=" + f4 + "]";
}
static class Adapter extends TypeAdapter<DummyClass> {
@Override
public DummyClass read(JsonReader in) throws IOException {
return new DummyClass("adapter-" + in.nextInt());
}
@Override
public void write(JsonWriter out, DummyClass value) throws IOException {
out.value("adapter-" + value);
}
}
static class Factory implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
@SuppressWarnings("unchecked") // the code below is not type safe, but does not matter for this test
TypeAdapter<T> r = (TypeAdapter<T>) new TypeAdapter<DummyClass>() {
@Override
public DummyClass read(JsonReader in) throws IOException {
return new DummyClass("factory-" + in.nextInt());
}
@Override
public void write(JsonWriter out, DummyClass value) throws IOException {
out.value("factory-" + value.s);
}
};
return r;
}
}
static class Serializer implements JsonSerializer<DummyClass> {
@Override
public JsonElement serialize(DummyClass src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive("serializer-" + src.s);
}
}
static class Deserializer implements JsonDeserializer<DummyClass> {
@Override
public DummyClass deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return new DummyClass("deserializer-" + json.getAsInt());
}
}
// Use this separate class mainly to work around incorrect delegation behavior for JsonSerializer
// and JsonDeserializer used with @JsonAdapter, see https://github.com/google/gson/issues/1783
static class DummyClass {
@SerializedName("s")
String s;
DummyClass(String s) {
this.s = s;
}
@Override
public String toString() {
return s;
}
}
}

View File

@ -0,0 +1,10 @@
package com.example;
public class ClassWithNamedFields {
public int myField;
public short notAccessedField = -1;
public ClassWithNamedFields(int i) {
myField = i;
}
}

View File

@ -0,0 +1,15 @@
package com.example;
import com.google.gson.annotations.SerializedName;
public class ClassWithSerializedName {
@SerializedName("myField")
public int i;
@SerializedName("notAccessed")
public short notAccessedField = -1;
public ClassWithSerializedName(int i) {
this.i = i;
}
}

View File

@ -0,0 +1,21 @@
package com.example;
import com.google.gson.annotations.Since;
import com.google.gson.annotations.Until;
/**
* Uses {@link Since} and {@link Until} annotations.
*/
public class ClassWithVersionAnnotations {
@Since(1)
int i1;
@Until(1) // will be ignored with GsonBuilder.setVersion(1)
int i2;
@Since(2) // will be ignored with GsonBuilder.setVersion(1)
int i3;
@Until(2)
int i4;
}

View File

@ -0,0 +1,37 @@
package com.example;
import static com.example.TestExecutor.same;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
public class DefaultConstructorMain {
static class TestClass {
@SerializedName("s")
public String s;
}
// R8 rule for this class still removes no-args constructor, but doesn't make class abstract
static class TestClassNotAbstract {
@SerializedName("s")
public String s;
}
/**
* Main entrypoint, called by {@code ShrinkingIT.testDefaultConstructor()}.
*/
public static String runTest() {
TestClass deserialized = new Gson().fromJson("{\"s\":\"value\"}", same(TestClass.class));
return deserialized.s;
}
/**
* Main entrypoint, called by {@code ShrinkingIT.testDefaultConstructorNoJdkUnsafe()}.
*/
public static String runTestNoJdkUnsafe() {
Gson gson = new GsonBuilder().disableJdkUnsafe().create();
TestClassNotAbstract deserialized = gson.fromJson("{\"s\": \"value\"}", same(TestClassNotAbstract.class));
return deserialized.s;
}
}

View File

@ -0,0 +1,6 @@
package com.example;
public enum EnumClass {
FIRST,
SECOND
}

View File

@ -0,0 +1,10 @@
package com.example;
import com.google.gson.annotations.SerializedName;
public enum EnumClassWithSerializedName {
@SerializedName("one")
FIRST,
@SerializedName("two")
SECOND
}

View File

@ -0,0 +1,66 @@
package com.example;
import com.google.gson.TypeAdapter;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
public class GenericClasses {
static class GenericClass<T> {
@SerializedName("t")
T t;
@Override
public String toString() {
return "{t=" + t + "}";
}
}
static class UsingGenericClass {
@SerializedName("g")
GenericClass<DummyClass> g;
@Override
public String toString() {
return "{g=" + g + "}";
}
}
static class GenericUsingGenericClass<T> {
@SerializedName("g")
GenericClass<T> g;
@Override
public String toString() {
return "{g=" + g + "}";
}
}
@JsonAdapter(DummyClass.Adapter.class)
static class DummyClass {
String s;
DummyClass(String s) {
this.s = s;
}
@Override
public String toString() {
return s;
}
static class Adapter extends TypeAdapter<DummyClass> {
@Override
public DummyClass read(JsonReader in) throws IOException {
return new DummyClass("read-" + in.nextInt());
}
@Override
public void write(JsonWriter out, DummyClass value) throws IOException {
throw new UnsupportedOperationException();
}
}
}
}

View File

@ -0,0 +1,142 @@
package com.example;
import static com.example.TestExecutor.same;
import com.example.GenericClasses.DummyClass;
import com.example.GenericClasses.GenericClass;
import com.example.GenericClasses.GenericUsingGenericClass;
import com.example.GenericClasses.UsingGenericClass;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
public class Main {
/**
* Main entrypoint, called by {@code ShrinkingIT.test()}.
*
* <p>To be safe let all tests put their output to the consumer and let integration test verify it;
* don't perform any relevant assertions in this code because code shrinkers could affect it.
*
* @param outputConsumer consumes the test output: {@code name, content} pairs
*/
public static void runTests(BiConsumer<String, String> outputConsumer) {
// Create the TypeToken instances on demand because creation of them can fail when
// generic signatures were erased
testTypeTokenWriteRead(outputConsumer, "anonymous", () -> new TypeToken<List<ClassWithAdapter>>() {});
testTypeTokenWriteRead(outputConsumer, "manual", () -> TypeToken.getParameterized(List.class, ClassWithAdapter.class));
testNamedFields(outputConsumer);
testSerializedName(outputConsumer);
testNoJdkUnsafe(outputConsumer);
testEnum(outputConsumer);
testEnumSerializedName(outputConsumer);
testExposeAnnotation(outputConsumer);
testVersionAnnotations(outputConsumer);
testJsonAdapterAnnotation(outputConsumer);
testGenericClasses(outputConsumer);
}
private static void testTypeTokenWriteRead(BiConsumer<String, String> outputConsumer, String description, Supplier<TypeToken<?>> typeTokenSupplier) {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
TestExecutor.run(outputConsumer, "Write: TypeToken " + description,
() -> gson.toJson(Arrays.asList(new ClassWithAdapter(1)), typeTokenSupplier.get().getType()));
TestExecutor.run(outputConsumer, "Read: TypeToken " + description, () -> {
Object deserialized = gson.fromJson("[{\"custom\": 3}]", typeTokenSupplier.get());
return deserialized.toString();
});
}
/**
* Calls {@link Gson#toJson}, but (hopefully) in a way which prevents code shrinkers
* from understanding that reflection is used for {@code obj}.
*/
private static String toJson(Gson gson, Object obj) {
return gson.toJson(same(obj));
}
/**
* Calls {@link Gson#fromJson}, but (hopefully) in a way which prevents code shrinkers
* from understanding that reflection is used for {@code c}.
*/
private static <T> T fromJson(Gson gson, String json, Class<T> c) {
return gson.fromJson(json, same(c));
}
private static void testNamedFields(BiConsumer<String, String> outputConsumer) {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
TestExecutor.run(outputConsumer, "Write: Named fields", () -> toJson(gson, new ClassWithNamedFields(2)));
TestExecutor.run(outputConsumer, "Read: Named fields", () -> {
ClassWithNamedFields deserialized = fromJson(gson, "{\"myField\": 3}", ClassWithNamedFields.class);
return Integer.toString(deserialized.myField);
});
}
private static void testSerializedName(BiConsumer<String, String> outputConsumer) {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
TestExecutor.run(outputConsumer, "Write: SerializedName", () -> toJson(gson, new ClassWithSerializedName(2)));
TestExecutor.run(outputConsumer, "Read: SerializedName", () -> {
ClassWithSerializedName deserialized = fromJson(gson, "{\"myField\": 3}", ClassWithSerializedName.class);
return Integer.toString(deserialized.i);
});
}
private static void testNoJdkUnsafe(BiConsumer<String, String> outputConsumer) {
Gson gson = new GsonBuilder().disableJdkUnsafe().create();
TestExecutor.run(outputConsumer, "Read: No JDK Unsafe; initial constructor value", () -> {
ClassWithDefaultConstructor deserialized = fromJson(gson, "{}", ClassWithDefaultConstructor.class);
return Integer.toString(deserialized.i);
});
TestExecutor.run(outputConsumer, "Read: No JDK Unsafe; custom value", () -> {
ClassWithDefaultConstructor deserialized = fromJson(gson, "{\"myField\": 3}", ClassWithDefaultConstructor.class);
return Integer.toString(deserialized.i);
});
}
private static void testEnum(BiConsumer<String, String> outputConsumer) {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
TestExecutor.run(outputConsumer, "Write: Enum", () -> toJson(gson, EnumClass.FIRST));
TestExecutor.run(outputConsumer, "Read: Enum", () -> fromJson(gson, "\"SECOND\"", EnumClass.class).toString());
}
private static void testEnumSerializedName(BiConsumer<String, String> outputConsumer) {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
TestExecutor.run(outputConsumer, "Write: Enum SerializedName",
() -> toJson(gson, EnumClassWithSerializedName.FIRST));
TestExecutor.run(outputConsumer, "Read: Enum SerializedName",
() -> fromJson(gson, "\"two\"", EnumClassWithSerializedName.class).toString());
}
private static void testExposeAnnotation(BiConsumer<String, String> outputConsumer) {
Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
TestExecutor.run(outputConsumer, "Write: @Expose", () -> toJson(gson, new ClassWithExposeAnnotation()));
}
private static void testVersionAnnotations(BiConsumer<String, String> outputConsumer) {
Gson gson = new GsonBuilder().setVersion(1).create();
TestExecutor.run(outputConsumer, "Write: Version annotations", () -> toJson(gson, new ClassWithVersionAnnotations()));
}
private static void testJsonAdapterAnnotation(BiConsumer<String, String> outputConsumer) {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
TestExecutor.run(outputConsumer, "Write: JsonAdapter on fields", () -> toJson(gson, new ClassWithJsonAdapterAnnotation(1, 2, 3, 4)));
String json = "{\"f1\": 1, \"f2\": 2, \"f3\": {\"s\": \"3\"}, \"f4\": 4}";
TestExecutor.run(outputConsumer, "Read: JsonAdapter on fields", () -> fromJson(gson, json, ClassWithJsonAdapterAnnotation.class).toString());
}
private static void testGenericClasses(BiConsumer<String, String> outputConsumer) {
Gson gson = new Gson();
TestExecutor.run(outputConsumer, "Read: Generic TypeToken", () -> gson.fromJson("{\"t\": 1}", new TypeToken<GenericClass<DummyClass>>() {}).toString());
TestExecutor.run(outputConsumer, "Read: Using Generic", () -> fromJson(gson, "{\"g\": {\"t\": 1}}", UsingGenericClass.class).toString());
TestExecutor.run(outputConsumer, "Read: Using Generic TypeToken", () -> gson.fromJson("{\"g\": {\"t\": 1}}", new TypeToken<GenericUsingGenericClass<DummyClass>>() {}).toString());
}
}

View File

@ -0,0 +1,34 @@
package com.example;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
public class TestExecutor {
/**
* Helper method for running individual tests. In case of an exception wraps it and
* includes the {@code name} of the test to make debugging issues with the obfuscated
* JARs a bit easier.
*/
public static void run(BiConsumer<String, String> outputConsumer, String name, Supplier<String> resultSupplier) {
String result;
try {
result = resultSupplier.get();
} catch (Throwable t) {
throw new RuntimeException("Test failed: " + name, t);
}
outputConsumer.accept(name, result);
}
/**
* Returns {@code t}, but in a way which (hopefully) prevents code shrinkers from
* simplifying this.
*/
public static <T> T same(T t) {
// This is essentially `return t`, but contains some redundant code to try
// prevent the code shrinkers from simplifying this
return Optional.of(t)
.map(v -> Optional.of(v).get())
.orElseThrow(() -> new AssertionError("unreachable"));
}
}

View File

@ -0,0 +1,223 @@
/*
* Copyright (C) 2023 Google 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 com.google.gson.it;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
/**
* Integration test verifying behavior of shrunken and obfuscated JARs.
*/
@RunWith(Parameterized.class)
public class ShrinkingIT {
// These JAR files are prepared by the Maven build
public static final Path PROGUARD_RESULT_PATH = Paths.get("target/proguard-output.jar");
public static final Path R8_RESULT_PATH = Paths.get("target/r8-output.jar");
@Parameters(name = "{index}: {0}")
public static List<Path> jarsToTest() {
return Arrays.asList(PROGUARD_RESULT_PATH, R8_RESULT_PATH);
}
@Parameter
public Path jarToTest;
@Before
public void verifyJarExists() {
if (!Files.isRegularFile(jarToTest)) {
fail("JAR file " + jarToTest + " does not exist; run this test with `mvn clean verify`");
}
}
@FunctionalInterface
interface TestAction {
void run(Class<?> c) throws Exception;
}
private void runTest(String className, TestAction testAction) throws Exception {
// Use bootstrap class loader; load all custom classes from JAR and not
// from dependencies of this test
ClassLoader classLoader = null;
// Load the shrunken and obfuscated JARs with a separate class loader, then load
// the main test class from it and let the test action invoke its test methods
try (URLClassLoader loader = new URLClassLoader(new URL[] {jarToTest.toUri().toURL()}, classLoader)) {
Class<?> c = loader.loadClass(className);
testAction.run(c);
}
}
@Test
public void test() throws Exception {
StringBuilder output = new StringBuilder();
runTest("com.example.Main", c -> {
Method m = c.getMethod("runTests", BiConsumer.class);
m.invoke(null, (BiConsumer<String, String>) (name, content) -> output.append(name + "\n" + content + "\n===\n"));
});
assertThat(output.toString()).isEqualTo(String.join("\n",
"Write: TypeToken anonymous",
"[",
" {",
" \"custom\": 1",
" }",
"]",
"===",
"Read: TypeToken anonymous",
"[ClassWithAdapter[3]]",
"===",
"Write: TypeToken manual",
"[",
" {",
" \"custom\": 1",
" }",
"]",
"===",
"Read: TypeToken manual",
"[ClassWithAdapter[3]]",
"===",
"Write: Named fields",
"{",
" \"myField\": 2,",
" \"notAccessedField\": -1",
"}",
"===",
"Read: Named fields",
"3",
"===",
"Write: SerializedName",
"{",
" \"myField\": 2,",
" \"notAccessed\": -1",
"}",
"===",
"Read: SerializedName",
"3",
"===",
"Read: No JDK Unsafe; initial constructor value",
"-3",
"===",
"Read: No JDK Unsafe; custom value",
"3",
"===",
"Write: Enum",
"\"FIRST\"",
"===",
"Read: Enum",
"SECOND",
"===",
"Write: Enum SerializedName",
"\"one\"",
"===",
"Read: Enum SerializedName",
"SECOND",
"===",
"Write: @Expose",
"{\"i\":0}",
"===",
"Write: Version annotations",
"{\"i1\":0,\"i4\":0}",
"===",
"Write: JsonAdapter on fields",
"{",
" \"f\": \"adapter-null\",",
" \"f1\": \"adapter-1\",",
" \"f2\": \"factory-2\",",
" \"f3\": \"serializer-3\",",
// For f4 only a JsonDeserializer is registered, so serialization falls back to reflection
" \"f4\": {",
" \"s\": \"4\"",
" }",
"}",
"===",
"Read: JsonAdapter on fields",
// For f3 only a JsonSerializer is registered, so for deserialization value is read as is using reflection
"ClassWithJsonAdapterAnnotation[f1=adapter-1, f2=factory-2, f3=3, f4=deserializer-4]",
"===",
"Read: Generic TypeToken",
"{t=read-1}",
"===",
"Read: Using Generic",
"{g={t=read-1}}",
"===",
"Read: Using Generic TypeToken",
"{g={t=read-1}}",
"===",
""
));
}
@Test
public void testDefaultConstructor() throws Exception {
runTest("com.example.DefaultConstructorMain", c -> {
Method m = c.getMethod("runTest");
if (jarToTest.equals(PROGUARD_RESULT_PATH)) {
Object result = m.invoke(null);
assertThat(result).isEqualTo("value");
} else {
// R8 performs more aggressive optimizations
Exception e = assertThrows(InvocationTargetException.class, () -> m.invoke(null));
assertThat(e).hasCauseThat().hasMessageThat().isEqualTo(
"Abstract classes can't be instantiated! Adjust the R8 configuration or register an InstanceCreator"
+ " or a TypeAdapter for this type. Class name: com.example.DefaultConstructorMain$TestClass"
+ "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#r8-abstract-class"
);
}
});
}
@Test
public void testDefaultConstructorNoJdkUnsafe() throws Exception {
runTest("com.example.DefaultConstructorMain", c -> {
Method m = c.getMethod("runTestNoJdkUnsafe");
if (jarToTest.equals(PROGUARD_RESULT_PATH)) {
Object result = m.invoke(null);
assertThat(result).isEqualTo("value");
} else {
// R8 performs more aggressive optimizations
Exception e = assertThrows(InvocationTargetException.class, () -> m.invoke(null));
assertThat(e).hasCauseThat().hasMessageThat().isEqualTo(
"Unable to create instance of class com.example.DefaultConstructorMain$TestClassNotAbstract;"
+ " usage of JDK Unsafe is disabled. Registering an InstanceCreator or a TypeAdapter for this type,"
+ " adding a no-args constructor, or enabling usage of JDK Unsafe may fix this problem. Or adjust"
+ " your R8 configuration to keep the no-args constructor of the class."
);
}
});
}
}