From f1b4a71de56ce9a8008cba897924e70d52509077 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 09:06:16 -0700 Subject: [PATCH 01/95] Bump maven-enforcer-plugin from 3.2.1 to 3.3.0 (#2366) Bumps [maven-enforcer-plugin](https://github.com/apache/maven-enforcer) from 3.2.1 to 3.3.0. - [Release notes](https://github.com/apache/maven-enforcer/releases) - [Commits](https://github.com/apache/maven-enforcer/compare/enforcer-3.2.1...enforcer-3.3.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-enforcer-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 93abb76ed5..a57ca468d1 100644 --- a/pom.xml +++ b/pom.xml @@ -93,7 +93,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.2.1 + 3.3.0 enforce-versions From b43ccee88927fa65d5e39a8ad4d0bebca7bf9994 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 8 Apr 2023 10:05:20 -0700 Subject: [PATCH 02/95] Bump maven-surefire-plugin from 3.0.0-M9 to 3.0.0 (#2342) Bumps [maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.0.0-M9 to 3.0.0. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.0.0-M9...surefire-3.0.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gson/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/pom.xml b/gson/pom.xml index 8af1ecdee3..afd6ac7183 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -136,7 +136,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M9 + 3.0.0 + + +## `ClassCastException` when using deserialized object **Symptom:** `ClassCastException` is thrown when accessing an object deserialized by Gson @@ -16,7 +19,7 @@ This guide describes how to troubleshoot common issues when using Gson. The overloads with `Type` parameter do not provide any type-safety guarantees. - When using `TypeToken` make sure you don't capture a type variable. For example avoid something like `new TypeToken>()` (where `T` is a type variable). Due to Java type erasure the actual type of `T` is not available at runtime. Refactor your code to pass around `TypeToken` instances or use [`TypeToken.getParameterized(...)`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/reflect/TypeToken.html#getParameterized(java.lang.reflect.Type,java.lang.reflect.Type...)), for example `TypeToken.getParameterized(List.class, elementClass)`. -## `InaccessibleObjectException`: 'module ... does not "opens ..." to unnamed module' +## `InaccessibleObjectException`: 'module ... does not "opens ..." to unnamed module' **Symptom:** An exception with a message in the form 'module ... does not "opens ..." to unnamed module' is thrown @@ -30,7 +33,7 @@ When no built-in adapter for a type exists and no custom adapter has been regist If you want to prevent using reflection on third-party classes in the future you can write your own [`ReflectionAccessFilter`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/ReflectionAccessFilter.html) or use one of the predefined ones, such as `ReflectionAccessFilter.BLOCK_ALL_PLATFORM`. -## `InaccessibleObjectException`: 'module ... does not "opens ..." to module com.google.gson' +## `InaccessibleObjectException`: 'module ... does not "opens ..." to module com.google.gson' **Symptom:** An exception with a message in the form 'module ... does not "opens ..." to module com.google.gson' is thrown @@ -51,7 +54,7 @@ module mymodule { Or in case this occurs for a field in one of your classes which you did not actually want to serialize or deserialize in the first place, you can exclude that field, see the [user guide](UserGuide.md#excluding-fields-from-serialization-and-deserialization). -## Android app not working in Release mode; random property names +## Android app not working in Release mode; random property names **Symptom:** Your Android app is working fine in Debug mode but fails in Release mode and the JSON properties have seemingly random names such as `a`, `b`, ... @@ -59,7 +62,7 @@ Or in case this occurs for a field in one of your classes which you did not actu **Solution:** Make sure you have configured ProGuard / R8 correctly to preserve the names of your fields. See the [Android example](examples/android-proguard-example/README.md) for more information. -## Android app unable to parse JSON after app update +## Android app unable to parse JSON after app update **Symptom:** You released a new version of your Android app and it fails to parse JSON data created by the previous version of your app @@ -71,7 +74,7 @@ If you want to preserve backward compatibility for you app you can use [`@Serial Normally ProGuard and R8 produce a mapping file, this makes it easier to find out the obfuscated field names instead of having to find them out through trial and error or other means. See the [Android Studio user guide](https://developer.android.com/studio/build/shrink-code.html#retracing) for more information. -## Default field values not present after deserialization +## Default field values not present after deserialization **Symptom:** You have assign default values to fields but after deserialization the fields have their standard value (such as `null` or `0`) @@ -84,7 +87,7 @@ Normally ProGuard and R8 produce a mapping file, this makes it easier to find ou Otherwise Gson will by default try to use JDK `Unsafe` or similar means to create an instance of your class without invoking the constructor and without running any initializers. You can also disable that behavior through [`GsonBuilder.disableJdkUnsafe()`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#disableJdkUnsafe()) to notice such issues early on. -## `null` values for anonymous and local classes +## `null` values for anonymous and local classes **Symptom:** Objects of a class are always serialized as JSON `null` / always deserialized as Java `null` @@ -97,7 +100,7 @@ Notes: - "double brace-initialization" also creates anonymous classes - Local record classes (feature added in Java 16) are supported by Gson and are not affected by this -## Map keys having unexpected format in JSON +## Map keys having unexpected format in JSON **Symptom:** JSON output for `Map` keys is unexpected / cannot be deserialized again @@ -105,15 +108,32 @@ Notes: **Solution:** Use [`GsonBuilder.enableComplexMapKeySerialization()`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#enableComplexMapKeySerialization()). See also the [user guide](UserGuide.md#maps-examples) for more information. -## Parsing JSON fails with `MalformedJsonException` +## Parsing JSON fails with `MalformedJsonException` **Symptom:** JSON parsing fails with `MalformedJsonException` **Reason:** The JSON data is actually malformed -**Solution:** During debugging log the JSON data right before calling Gson methods or set a breakpoint to inspect the data and make sure it has the expected format. Sometimes APIs might return HTML error pages (instead of JSON data) when reaching rate limits or when other errors occur. Also read the location information of the `MalformedJsonException` exception message, it indicates where exactly in the document the malformed data was detected, including the [JSONPath](https://goessner.net/articles/JsonPath/). +**Solution:** During debugging, log the JSON data right before calling Gson methods or set a breakpoint to inspect the data and make sure it has the expected format. Sometimes APIs might return HTML error pages (instead of JSON data) when reaching rate limits or when other errors occur. Also read the location information of the `MalformedJsonException` exception message, it indicates where exactly in the document the malformed data was detected, including the [JSONPath](https://goessner.net/articles/JsonPath/). + +For example, let's assume you want to deserialize the following JSON data: + +```json +{ + "languages": [ + "English", + "French", + ] +} +``` -## Integral JSON number is parsed as `double` +This will fail with an exception similar to this one: `MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 5 column 4 path $.languages[2]` +The problem here is the trailing comma (`,`) after `"French"`, trailing commas are not allowed by the JSON specification. The location information "line 5 column 4" points to the `]` in the JSON data (with some slight inaccuracies) because Gson expected another value after `,` instead of the closing `]`. The JSONPath `$.languages[2]` in the exception message also points there: `$.` refers to the root object, `languages` refers to its member of that name and `[2]` refers to the (missing) third value in the JSON array value of that member (numbering starts at 0, so it is `[2]` instead of `[3]`). +The proper solution here is to fix the malformed JSON data. + +To spot syntax errors in the JSON data easily you can open it in an editor with support for JSON, for example Visual Studio Code. It will highlight within the JSON data the error location and show why the JSON data is considered invalid. + +## Integral JSON number is parsed as `double` **Symptom:** JSON data contains an integral number such as `45` but Gson returns it as `double` @@ -121,17 +141,17 @@ Notes: **Solution:** Use [`GsonBuilder.setObjectToNumberStrategy`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#setObjectToNumberStrategy(com.google.gson.ToNumberStrategy)) to specify what type of number should be returned -## Malformed JSON not rejected +## Malformed JSON not rejected **Symptom:** Gson parses malformed JSON without throwing any exceptions **Reason:** Due to legacy reasons Gson performs parsing by default in lenient mode -**Solution:** See [`Gson` class documentation](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/Gson.html) section "Lenient JSON handling" +**Solution:** See [`Gson` class documentation](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/Gson.html#default-lenient) section "Lenient JSON handling" Note: Even in non-lenient mode Gson deviates slightly from the JSON specification, see [`JsonReader.setLenient`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/stream/JsonReader.html#setLenient(boolean)) for more details. -## `IllegalStateException`: "Expected ... but was ..." +## `IllegalStateException`: "Expected ... but was ..." **Symptom:** An `IllegalStateException` with a message in the form "Expected ... but was ..." is thrown @@ -139,13 +159,36 @@ Note: Even in non-lenient mode Gson deviates slightly from the JSON specificatio **Solution:** Make sure that your classes correctly model the JSON data. Also during debugging log the JSON data right before calling Gson methods or set a breakpoint to inspect the data and make sure it has the expected format. Read the location information of the exception message, it indicates where exactly in the document the error occurred, including the [JSONPath](https://goessner.net/articles/JsonPath/). -## `IllegalStateException`: "Expected ... but was NULL" +For example, let's assume you have the following Java class: + +```java +class WebPage { + String languages; +} +``` + +And you want to deserialize the following JSON data: + +```json +{ + "languages": ["English", "French"] +} +``` + +This will fail with an exception similar to this one: `IllegalStateException: Expected a string but was BEGIN_ARRAY at line 2 column 17 path $.languages` +This means Gson expected a JSON string value but found the beginning of a JSON array (`[`). The location information "line 2 column 17" points to the `[` in the JSON data (with some slight inaccuracies), so does the JSONPath `$.languages` in the exception message. It refers to the `languages` member of the root object (`$.`). +The solution here is to change in the `WebPage` class the field `String languages` to `List languages`. + +## `IllegalStateException`: "Expected ... but was NULL" **Symptom:** An `IllegalStateException` with a message in the form "Expected ... but was NULL" is thrown -**Reason:** You have written a custom `TypeAdapter` which does not properly handle a JSON null value +**Reason:** -**Solution:** Add code similar to the following at the beginning of the `read` method of your adapter: +- A built-in adapter does not support JSON null values +- You have written a custom `TypeAdapter` which does not properly handle JSON null values + +**Solution:** If this occurs for a custom adapter you wrote, add code similar to the following at the beginning of its `read` method: ```java @Override @@ -154,14 +197,14 @@ public MyClass read(JsonReader in) throws IOException { in.nextNull(); return null; } - + ... } ``` Alternatively you can call [`nullSafe()`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html#nullSafe()) on the adapter instance you created. -## Properties missing in JSON +## Properties missing in JSON **Symptom:** Properties are missing in the JSON output @@ -171,7 +214,7 @@ Alternatively you can call [`nullSafe()`](https://www.javadoc.io/doc/com.google. Note: Gson does not support anonymous and local classes and will serialize them as JSON null, see the [related troubleshooting point](#null-values-for-anonymous-and-local-classes). -## JSON output changes for newer Android versions +## JSON output changes for newer Android versions **Symptom:** The JSON output differs when running on newer Android versions @@ -185,7 +228,7 @@ When no built-in adapter for a type exists and no custom adapter has been regist If you want to prevent using reflection on third-party classes in the future you can write your own [`ReflectionAccessFilter`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/ReflectionAccessFilter.html) or use one of the predefined ones, such as `ReflectionAccessFilter.BLOCK_ALL_PLATFORM`. -## JSON output contains values of `static` fields +## JSON output contains values of `static` fields **Symptom:** The JSON output contains values of `static` fields @@ -193,7 +236,7 @@ If you want to prevent using reflection on third-party classes in the future you **Solution:** When calling `GsonBuilder.excludeFieldsWithModifiers` you overwrite the default excluded modifiers. Therefore, you have to explicitly exclude `static` fields if desired. This can be done by adding `Modifier.STATIC` as additional argument. -## `NoSuchMethodError` when calling Gson methods +## `NoSuchMethodError` when calling Gson methods **Symptom:** A `java.lang.NoSuchMethodError` is thrown when trying to call certain Gson methods @@ -210,3 +253,31 @@ System.out.println(Gson.class.getProtectionDomain().getCodeSource().getLocation( ``` If that fails with a `NullPointerException` you have to try one of the other ways to find out where a class is loaded from. + +## `IllegalArgumentException`: 'Class ... declares multiple JSON fields named '...'' + +**Symptom:** An exception with the message 'Class ... declares multiple JSON fields named '...'' is thrown + +**Reason:** + +- The name you have specified with a [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html) annotation for a field collides with the name of another field +- The [`FieldNamingStrategy`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/FieldNamingStrategy.html) you have specified produces conflicting field names +- A field of your class has the same name as the field of a superclass + +Gson prevents multiple fields with the same name because during deserialization it would be ambiguous for which field the JSON data should be deserialized. For serialization it would cause the same field to appear multiple times in JSON. While the JSON specification permits this, it is likely that the application parsing the JSON data will not handle it correctly. + +**Solution:** First identify the fields with conflicting names based on the exception message. Then decide if you want to rename one of them using the [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html) annotation, or if you want to [exclude](UserGuide.md#excluding-fields-from-serialization-and-deserialization) one of them. When excluding one of the fields you have to include it for both serialization and deserialization (even if your application only performs one of these actions) because the duplicate field check cannot differentiate between these actions. + +## `UnsupportedOperationException` when serializing or deserializing `java.lang.Class` + +**Symptom:** An `UnsupportedOperationException` is thrown when trying to serialize or deserialize `java.lang.Class` + +**Reason:** Gson intentionally does not permit serializing and deserializing `java.lang.Class` for security reasons. Otherwise a malicious user could make your application load an arbitrary class from the classpath and, depending on what your application does with the `Class`, in the worst case perform a remote code execution attack. + +**Solution:** First check if you really need to serialize or deserialize a `Class`. Often it is possible to use string aliases and then map them to the known `Class`; you could write a custom [`TypeAdapter`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html) to do this. If the `Class` values are not known in advance, try to introduce a common base class or interface for all these classes and then verify that the deserialized class is a subclass. For example assuming the base class is called `MyBaseClass`, your custom `TypeAdapter` should load the class like this: + +```java +Class.forName(jsonString, false, getClass().getClassLoader()).asSubclass(MyBaseClass.cla‌​ss) +``` + +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`. diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index e90831046f..ec5f69f63e 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -105,7 +105,7 @@ *

See the Gson User Guide * for a more complete set of examples.

* - *

Lenient JSON handling

+ *

Lenient JSON handling

* For legacy reasons most of the {@code Gson} methods allow JSON data which does not * comply with the JSON specification, regardless of whether {@link GsonBuilder#setLenient()} * is used or not. If this behavior is not desired, the following workarounds can be used: diff --git a/gson/src/main/java/com/google/gson/internal/TroubleshootingGuide.java b/gson/src/main/java/com/google/gson/internal/TroubleshootingGuide.java new file mode 100644 index 0000000000..69c1b06d90 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/TroubleshootingGuide.java @@ -0,0 +1,12 @@ +package com.google.gson.internal; + +public class TroubleshootingGuide { + private TroubleshootingGuide() {} + + /** + * Creates a URL referring to the specified troubleshooting section. + */ + public static String createUrl(String id) { + return "https://github.com/google/gson/blob/master/Troubleshooting.md#" + id; + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java index 9194fc33bf..6a43b817c9 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java @@ -33,6 +33,7 @@ import com.google.gson.internal.ObjectConstructor; import com.google.gson.internal.Primitives; import com.google.gson.internal.ReflectionAccessFilterHelper; +import com.google.gson.internal.TroubleshootingGuide; import com.google.gson.internal.reflect.ReflectionHelper; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; @@ -114,7 +115,7 @@ public TypeAdapter create(Gson gson, final TypeToken type) { if (filterResult == FilterResult.BLOCK_ALL) { throw new JsonIOException( "ReflectionAccessFilter does not permit using reflection for " + raw - + ". Register a TypeAdapter for this type or adjust the access filter."); + + ". Register a TypeAdapter for this type or adjust the access filter."); } boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE; @@ -306,7 +307,8 @@ private Map getBoundFields(Gson context, TypeToken type, if (previous != null) { throw new IllegalArgumentException("Class " + originalRaw.getName() + " declares multiple JSON fields named '" + previous.name + "'; conflict is caused" - + " by fields " + ReflectionHelper.fieldToString(previous.field) + " and " + ReflectionHelper.fieldToString(field)); + + " by fields " + ReflectionHelper.fieldToString(previous.field) + " and " + ReflectionHelper.fieldToString(field) + + "\nSee " + TroubleshootingGuide.createUrl("duplicate-fields")); } } type = TypeToken.get($Gson$Types.resolve(type.getType(), raw, raw.getGenericSuperclass())); diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java index b76a0f0e3c..0f414e81e9 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java @@ -28,6 +28,7 @@ import com.google.gson.TypeAdapterFactory; import com.google.gson.annotations.SerializedName; import com.google.gson.internal.LazilyParsedNumber; +import com.google.gson.internal.TroubleshootingGuide; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; @@ -73,12 +74,14 @@ private TypeAdapters() { @Override public void write(JsonWriter out, Class value) throws IOException { throw new UnsupportedOperationException("Attempted to serialize java.lang.Class: " - + value.getName() + ". Forgot to register a type adapter?"); + + value.getName() + ". Forgot to register a type adapter?" + + "\nSee " + TroubleshootingGuide.createUrl("java-lang-class-unsupported")); } @Override public Class read(JsonReader in) throws IOException { throw new UnsupportedOperationException( - "Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?"); + "Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?" + + "\nSee " + TroubleshootingGuide.createUrl("java-lang-class-unsupported")); } }.nullSafe(); diff --git a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java index 41dd4cf372..de1df3552d 100644 --- a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java +++ b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java @@ -18,6 +18,7 @@ import com.google.gson.JsonIOException; import com.google.gson.internal.GsonBuildConfig; +import com.google.gson.internal.TroubleshootingGuide; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -40,6 +41,17 @@ public class ReflectionHelper { private ReflectionHelper() {} + private static String getInaccessibleTroubleshootingSuffix(Exception e) { + // Class was added in Java 9, therefore cannot use instanceof + if (e.getClass().getName().equals("java.lang.reflect.InaccessibleObjectException")) { + String message = e.getMessage(); + String troubleshootingId = message != null && message.contains("to module com.google.gson") + ? "reflection-inaccessible-to-module-gson" : "reflection-inaccessible"; + return "\nSee " + TroubleshootingGuide.createUrl(troubleshootingId); + } + return ""; + } + /** * Internal implementation of making an {@link AccessibleObject} accessible. * @@ -52,7 +64,8 @@ public static void makeAccessible(AccessibleObject object) throws JsonIOExceptio } catch (Exception exception) { String description = getAccessibleObjectDescription(object, false); throw new JsonIOException("Failed making " + description + " accessible; either increase its visibility" - + " or write a custom TypeAdapter for its declaring type.", exception); + + " or write a custom TypeAdapter for its declaring type." + getInaccessibleTroubleshootingSuffix(exception), + exception); } } @@ -142,7 +155,7 @@ public static String tryMakeAccessible(Constructor constructor) { return "Failed making constructor '" + constructorToString(constructor) + "' accessible;" + " either increase its visibility or write a custom InstanceCreator or TypeAdapter for" // Include the message since it might contain more detailed information - + " its declaring type: " + exception.getMessage(); + + " its declaring type: " + exception.getMessage() + getInaccessibleTroubleshootingSuffix(exception); } } diff --git a/gson/src/main/java/com/google/gson/stream/JsonReader.java b/gson/src/main/java/com/google/gson/stream/JsonReader.java index e0931320d1..de7aef5ff5 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -17,6 +17,7 @@ package com.google.gson.stream; import com.google.gson.internal.JsonReaderInternalAccess; +import com.google.gson.internal.TroubleshootingGuide; import com.google.gson.internal.bind.JsonTreeReader; import java.io.Closeable; import java.io.EOFException; @@ -355,7 +356,7 @@ public void beginArray() throws IOException { pathIndices[stackSize - 1] = 0; peeked = PEEKED_NONE; } else { - throw new IllegalStateException("Expected BEGIN_ARRAY but was " + peek() + locationString()); + throw unexpectedTokenError("BEGIN_ARRAY"); } } @@ -373,7 +374,7 @@ public void endArray() throws IOException { pathIndices[stackSize - 1]++; peeked = PEEKED_NONE; } else { - throw new IllegalStateException("Expected END_ARRAY but was " + peek() + locationString()); + throw unexpectedTokenError("END_ARRAY"); } } @@ -390,7 +391,7 @@ public void beginObject() throws IOException { push(JsonScope.EMPTY_OBJECT); peeked = PEEKED_NONE; } else { - throw new IllegalStateException("Expected BEGIN_OBJECT but was " + peek() + locationString()); + throw unexpectedTokenError("BEGIN_OBJECT"); } } @@ -409,7 +410,7 @@ public void endObject() throws IOException { pathIndices[stackSize - 1]++; peeked = PEEKED_NONE; } else { - throw new IllegalStateException("Expected END_OBJECT but was " + peek() + locationString()); + throw unexpectedTokenError("END_OBJECT"); } } @@ -797,7 +798,7 @@ public String nextName() throws IOException { } else if (p == PEEKED_DOUBLE_QUOTED_NAME) { result = nextQuotedValue('"'); } else { - throw new IllegalStateException("Expected a name but was " + peek() + locationString()); + throw unexpectedTokenError("a name"); } peeked = PEEKED_NONE; pathNames[stackSize - 1] = result; @@ -833,7 +834,7 @@ public String nextString() throws IOException { result = new String(buffer, pos, peekedNumberLength); pos += peekedNumberLength; } else { - throw new IllegalStateException("Expected a string but was " + peek() + locationString()); + throw unexpectedTokenError("a string"); } peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; @@ -861,7 +862,7 @@ public boolean nextBoolean() throws IOException { pathIndices[stackSize - 1]++; return false; } - throw new IllegalStateException("Expected a boolean but was " + peek() + locationString()); + throw unexpectedTokenError("a boolean"); } /** @@ -880,7 +881,7 @@ public void nextNull() throws IOException { peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; } else { - throw new IllegalStateException("Expected null but was " + peek() + locationString()); + throw unexpectedTokenError("null"); } } @@ -915,14 +916,13 @@ public double nextDouble() throws IOException { } else if (p == PEEKED_UNQUOTED) { peekedString = nextUnquotedValue(); } else if (p != PEEKED_BUFFERED) { - throw new IllegalStateException("Expected a double but was " + peek() + locationString()); + throw unexpectedTokenError("a double"); } peeked = PEEKED_BUFFERED; double result = Double.parseDouble(peekedString); // don't catch this NumberFormatException. if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) { - throw new MalformedJsonException( - "JSON forbids NaN and infinities: " + result + locationString()); + throw syntaxError("JSON forbids NaN and infinities: " + result); } peekedString = null; peeked = PEEKED_NONE; @@ -970,7 +970,7 @@ public long nextLong() throws IOException { // Fall back to parse as a double below. } } else { - throw new IllegalStateException("Expected a long but was " + peek() + locationString()); + throw unexpectedTokenError("a long"); } peeked = PEEKED_BUFFERED; @@ -1208,7 +1208,7 @@ public int nextInt() throws IOException { // Fall back to parse as a double below. } } else { - throw new IllegalStateException("Expected an int but was " + peek() + locationString()); + throw unexpectedTokenError("an int"); } peeked = PEEKED_BUFFERED; @@ -1584,10 +1584,10 @@ public String getPath() { /** * Unescapes the character identified by the character or characters that * immediately follow a backslash. The backslash '\' should have already - * been read. This supports both unicode escapes "u000A" and two-character + * been read. This supports both Unicode escapes "u000A" and two-character * escapes "\n". * - * @throws MalformedJsonException if any unicode escape sequences are + * @throws MalformedJsonException if any Unicode escape sequences are * malformed. */ @SuppressWarnings("fallthrough") @@ -1614,7 +1614,7 @@ private char readEscapeCharacter() throws IOException { } else if (c >= 'A' && c <= 'F') { result += (c - 'A' + 10); } else { - throw new MalformedJsonException("\\u" + new String(buffer, pos, 4)); + throw syntaxError("Malformed Unicode escape \\u" + new String(buffer, pos, 4)); } } pos += 4; @@ -1656,7 +1656,16 @@ private char readEscapeCharacter() throws IOException { * with this reader's content. */ private IOException syntaxError(String message) throws IOException { - throw new MalformedJsonException(message + locationString()); + throw new MalformedJsonException(message + locationString() + + "\nSee " + TroubleshootingGuide.createUrl("malformed-json")); + } + + private IllegalStateException unexpectedTokenError(String expected) throws IOException { + JsonToken peeked = peek(); + String troubleshootingId = peeked == JsonToken.NULL + ? "adapter-not-null-safe" : "unexpected-json-structure"; + return new IllegalStateException("Expected " + expected + " but was " + peek() + locationString() + + "\nSee " + TroubleshootingGuide.createUrl(troubleshootingId)); } /** @@ -1699,8 +1708,7 @@ private void consumeNonExecutePrefix() throws IOException { } else if (p == PEEKED_UNQUOTED_NAME) { reader.peeked = PEEKED_UNQUOTED; } else { - throw new IllegalStateException( - "Expected a name but was " + reader.peek() + reader.locationString()); + throw reader.unexpectedTokenError("a name"); } } }; diff --git a/gson/src/test/java/com/google/gson/JsonArrayTest.java b/gson/src/test/java/com/google/gson/JsonArrayTest.java index c3c0184fd6..21a5d58401 100644 --- a/gson/src/test/java/com/google/gson/JsonArrayTest.java +++ b/gson/src/test/java/com/google/gson/JsonArrayTest.java @@ -17,7 +17,6 @@ package com.google.gson; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.fail; import com.google.common.testing.EqualsTester; @@ -140,22 +139,19 @@ public void testFailedGetArrayValues() { jsonArray.getAsBoolean(); fail("expected getBoolean to fail"); } catch (UnsupportedOperationException e) { - assertWithMessage("Expected an exception message") - .that(e).hasMessageThat().isEqualTo("JsonObject"); + assertThat(e).hasMessageThat().isEqualTo("JsonObject"); } try { jsonArray.get(-1); fail("expected get to fail"); } catch (IndexOutOfBoundsException e) { - assertWithMessage("Expected an exception message") - .that(e).hasMessageThat().isEqualTo("Index -1 out of bounds for length 1"); + assertThat(e).hasMessageThat().isEqualTo("Index -1 out of bounds for length 1"); } try { jsonArray.getAsString(); fail("expected getString to fail"); } catch (UnsupportedOperationException e) { - assertWithMessage("Expected an exception message") - .that(e).hasMessageThat().isEqualTo("JsonObject"); + assertThat(e).hasMessageThat().isEqualTo("JsonObject"); } jsonArray.remove(0); @@ -164,36 +160,31 @@ public void testFailedGetArrayValues() { jsonArray.getAsDouble(); fail("expected getDouble to fail"); } catch (NumberFormatException e) { - assertWithMessage("Expected an exception message") - .that(e).hasMessageThat().isEqualTo("For input string: \"hello\""); + assertThat(e).hasMessageThat().isEqualTo("For input string: \"hello\""); } try { jsonArray.getAsInt(); fail("expected getInt to fail"); } catch (NumberFormatException e) { - assertWithMessage("Expected an exception message") - .that(e).hasMessageThat().isEqualTo("For input string: \"hello\""); + assertThat(e).hasMessageThat().isEqualTo("For input string: \"hello\""); } try { jsonArray.get(0).getAsJsonArray(); fail("expected getJSONArray to fail"); } catch (IllegalStateException e) { - assertWithMessage("Expected an exception message") - .that(e).hasMessageThat().isEqualTo("Not a JSON Array: \"hello\""); + assertThat(e).hasMessageThat().isEqualTo("Not a JSON Array: \"hello\""); } try { jsonArray.getAsJsonObject(); fail("expected getJSONObject to fail"); } catch (IllegalStateException e) { - assertWithMessage("Expected an exception message") - .that(e).hasMessageThat().isEqualTo( "Not a JSON Object: [\"hello\"]"); + assertThat(e).hasMessageThat().isEqualTo("Not a JSON Object: [\"hello\"]"); } try { jsonArray.getAsLong(); fail("expected getLong to fail"); } catch (NumberFormatException e) { - assertWithMessage("Expected an exception message") - .that(e).hasMessageThat().isEqualTo("For input string: \"hello\""); + assertThat(e).hasMessageThat().isEqualTo("For input string: \"hello\""); } } diff --git a/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java b/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java index 4750d24fae..2e40867f8f 100644 --- a/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java +++ b/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java @@ -37,7 +37,9 @@ public void testDouble() throws IOException { strategy.readNumber(fromString("1e400")); fail(); } catch (MalformedJsonException expected) { - assertThat(expected).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: Infinity at line 1 column 6 path $"); + assertThat(expected).hasMessageThat().isEqualTo( + "JSON forbids NaN and infinities: Infinity at line 1 column 6 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } try { strategy.readNumber(fromString("\"not-a-number\"")); @@ -80,19 +82,25 @@ public void testLongOrDouble() throws IOException { strategy.readNumber(fromString("NaN")); fail(); } catch (MalformedJsonException expected) { - assertThat(expected).hasMessageThat().isEqualTo("Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $"); + assertThat(expected).hasMessageThat().isEqualTo( + "Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } try { strategy.readNumber(fromString("Infinity")); fail(); } catch (MalformedJsonException expected) { - assertThat(expected).hasMessageThat().isEqualTo("Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $"); + assertThat(expected).hasMessageThat().isEqualTo( + "Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } try { strategy.readNumber(fromString("-Infinity")); fail(); } catch (MalformedJsonException expected) { - assertThat(expected).hasMessageThat().isEqualTo("Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $"); + assertThat(expected).hasMessageThat().isEqualTo( + "Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -117,21 +125,29 @@ public void testNullsAreNeverExpected() throws IOException { ToNumberPolicy.DOUBLE.readNumber(fromString("null")); fail(); } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected a double but was NULL at line 1 column 5 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#adapter-not-null-safe"); } try { ToNumberPolicy.LAZILY_PARSED_NUMBER.readNumber(fromString("null")); fail(); } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#adapter-not-null-safe"); } try { ToNumberPolicy.LONG_OR_DOUBLE.readNumber(fromString("null")); fail(); } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#adapter-not-null-safe"); } try { ToNumberPolicy.BIG_DECIMAL.readNumber(fromString("null")); fail(); } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#adapter-not-null-safe"); } } diff --git a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java index d0d2eff78c..841b01251e 100644 --- a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java +++ b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java @@ -95,6 +95,8 @@ public void testClassSerialization() { gson.toJson(String.class); fail(); } catch (UnsupportedOperationException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Attempted to serialize java.lang.Class: java.lang.String. Forgot to register a type adapter?" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#java-lang-class-unsupported"); } // Override with a custom type adapter for class. gson = new GsonBuilder().registerTypeAdapter(Class.class, new MyClassTypeAdapter()).create(); @@ -107,6 +109,8 @@ public void testClassDeserialization() { gson.fromJson("String.class", Class.class); fail(); } catch (UnsupportedOperationException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#java-lang-class-unsupported"); } // Override with a custom type adapter for class. gson = new GsonBuilder().registerTypeAdapter(Class.class, new MyClassTypeAdapter()).create(); @@ -365,7 +369,7 @@ public void testBitSetDeserialization() { gson.fromJson("[1, []]", BitSet.class); fail(); } catch (JsonSyntaxException e) { - assertThat(e.getMessage()).isEqualTo("Invalid bitset value type: BEGIN_ARRAY; at path $[1]"); + assertThat(e).hasMessageThat().isEqualTo("Invalid bitset value type: BEGIN_ARRAY; at path $[1]"); } try { @@ -631,7 +635,7 @@ public void testJsonElementTypeMismatch() { gson.fromJson("\"abc\"", JsonObject.class); fail(); } catch (JsonSyntaxException expected) { - assertThat(expected.getMessage()).isEqualTo("Expected a com.google.gson.JsonObject but was com.google.gson.JsonPrimitive; at path $"); + assertThat(expected).hasMessageThat().isEqualTo("Expected a com.google.gson.JsonObject but was com.google.gson.JsonPrimitive; at path $"); } } diff --git a/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java b/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java index b7029487e8..a1a048fcd7 100644 --- a/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java +++ b/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java @@ -53,8 +53,8 @@ public void setUp() { @Test public void testVersionPattern() { - assertThat(GSON_VERSION_PATTERN.matcher("(GSON 2.8.5)").matches()).isTrue(); - assertThat(GSON_VERSION_PATTERN.matcher("(GSON 2.8.5-SNAPSHOT)").matches()).isTrue(); + assertThat("(GSON 2.8.5)").matches(GSON_VERSION_PATTERN); + assertThat("(GSON 2.8.5-SNAPSHOT)").matches(GSON_VERSION_PATTERN); } @Test @@ -80,7 +80,7 @@ private void ensureAssertionErrorPrintsGsonVersion(AssertionError expected) { assertThat(end > 0 && end > start + 6).isTrue(); String version = msg.substring(start, end); // System.err.println(version); - assertThat(GSON_VERSION_PATTERN.matcher(version).matches()).isTrue(); + assertThat(version).matches(GSON_VERSION_PATTERN); } private static final class TestType { diff --git a/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java b/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java index 38a45644ba..5d863f1538 100644 --- a/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java +++ b/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java @@ -59,7 +59,7 @@ public void testSerializeComplexMapWithTypeAdapter() { @Test @Ignore - public void disabled_testTwoTypesCollapseToOneSerialize() { + public void testTwoTypesCollapseToOneSerialize() { Gson gson = new GsonBuilder() .enableComplexMapKeySerialization() .create(); diff --git a/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java b/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java index 25a5b5ff84..f360e84d97 100644 --- a/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java +++ b/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java @@ -17,7 +17,6 @@ package com.google.gson.functional; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; import com.google.gson.Gson; import com.google.gson.JsonObject; @@ -57,8 +56,8 @@ public void testListOfSubclassFields() { list.add(new Sub(2, 3)); ClassWithContainersOfBaseFields target = new ClassWithContainersOfBaseFields(list, null); String json = gson.toJson(target); - assertWithMessage(json).that(json).contains("{\"b\":1}"); - assertWithMessage(json).that(json).contains("{\"s\":3,\"b\":2}"); + assertThat(json).contains("{\"b\":1}"); + assertThat(json).contains("{\"s\":3,\"b\":2}"); } @Test @@ -98,8 +97,8 @@ public void testListOfParameterizedSubclassFields() { ClassWithContainersOfParameterizedBaseFields target = new ClassWithContainersOfParameterizedBaseFields(list, null); String json = gson.toJson(target); - assertWithMessage(json).that(json).contains("{\"t\":\"one\"}"); - assertWithMessage(json).that(json).doesNotContain("\"s\":"); + assertThat(json).contains("{\"t\":\"one\"}"); + assertThat(json).doesNotContain("\"s\":"); } /** diff --git a/gson/src/test/java/com/google/gson/functional/NamingPolicyTest.java b/gson/src/test/java/com/google/gson/functional/NamingPolicyTest.java index 5527d36381..8234df8ecc 100644 --- a/gson/src/test/java/com/google/gson/functional/NamingPolicyTest.java +++ b/gson/src/test/java/com/google/gson/functional/NamingPolicyTest.java @@ -26,6 +26,7 @@ import com.google.gson.common.TestTypes.ClassWithSerializedNameFields; import com.google.gson.common.TestTypes.StringWrapper; import java.lang.reflect.Field; +import java.util.Locale; import org.junit.Before; import org.junit.Test; @@ -137,7 +138,29 @@ public void testGsonDuplicateNameUsingSerializedNameFieldNamingPolicySerializati assertThat(expected).hasMessageThat() .isEqualTo("Class com.google.gson.functional.NamingPolicyTest$ClassWithDuplicateFields declares multiple JSON fields named 'a';" + " conflict is caused by fields com.google.gson.functional.NamingPolicyTest$ClassWithDuplicateFields#a and" - + " com.google.gson.functional.NamingPolicyTest$ClassWithDuplicateFields#b"); + + " com.google.gson.functional.NamingPolicyTest$ClassWithDuplicateFields#b" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#duplicate-fields"); + } + } + + @Test + public void testGsonDuplicateNameDueToBadNamingPolicy() { + Gson gson = builder.setFieldNamingStrategy(new FieldNamingStrategy() { + @Override + public String translateName(Field f) { + return "x"; + } + }).create(); + + try { + gson.toJson(new ClassWithTwoFields()); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat() + .isEqualTo("Class com.google.gson.functional.NamingPolicyTest$ClassWithTwoFields declares multiple JSON fields named 'x';" + + " conflict is caused by fields com.google.gson.functional.NamingPolicyTest$ClassWithTwoFields#a and" + + " com.google.gson.functional.NamingPolicyTest$ClassWithTwoFields#b" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#duplicate-fields"); } } @@ -209,7 +232,7 @@ static final class AtName { private static final class UpperCaseNamingStrategy implements FieldNamingStrategy { @Override public String translateName(Field f) { - return f.getName().toUpperCase(); + return f.getName().toUpperCase(Locale.ROOT); } } @@ -239,4 +262,12 @@ private static class ClassWithComplexFieldName { this.value = value; } } + + @SuppressWarnings("unused") + private static class ClassWithTwoFields { + public int a; + public int b; + + public ClassWithTwoFields() {} + } } diff --git a/gson/src/test/java/com/google/gson/functional/ObjectTest.java b/gson/src/test/java/com/google/gson/functional/ObjectTest.java index 208db3a01e..46324e98d8 100644 --- a/gson/src/test/java/com/google/gson/functional/ObjectTest.java +++ b/gson/src/test/java/com/google/gson/functional/ObjectTest.java @@ -177,7 +177,8 @@ public void testClassWithDuplicateFields() { } catch (IllegalArgumentException e) { assertThat(e).hasMessageThat().isEqualTo("Class com.google.gson.functional.ObjectTest$Subclass declares multiple JSON fields named 's';" + " conflict is caused by fields com.google.gson.functional.ObjectTest$Superclass1#s and" - + " com.google.gson.functional.ObjectTest$Superclass2#s"); + + " com.google.gson.functional.ObjectTest$Superclass2#s" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#duplicate-fields"); } } @@ -196,6 +197,7 @@ public void testNestedDeserialization() { Nested target = gson.fromJson(json, Nested.class); assertThat(target.getExpectedJson()).isEqualTo(json); } + @Test public void testNullSerialization() { assertThat(gson.toJson(null)).isEqualTo("null"); @@ -632,7 +634,7 @@ public void testStaticFieldDeserialization() { gson.fromJson("{\"s\":\"custom\"}", ClassWithStaticFinalField.class); fail(); } catch (JsonIOException e) { - assertThat( e.getMessage()).isEqualTo("Cannot set value of 'static final' field 'com.google.gson.functional.ObjectTest$ClassWithStaticFinalField#s'"); + assertThat(e).hasMessageThat().isEqualTo("Cannot set value of 'static final' field 'com.google.gson.functional.ObjectTest$ClassWithStaticFinalField#s'"); } } @@ -652,7 +654,7 @@ public void testThrowingDefaultConstructor() { } // TODO: Adjust this once Gson throws more specific exception type catch (RuntimeException e) { - assertThat( e.getMessage()).isEqualTo("Failed to invoke constructor 'com.google.gson.functional.ObjectTest$ClassWithThrowingConstructor()' with no args"); + assertThat(e).hasMessageThat().isEqualTo("Failed to invoke constructor 'com.google.gson.functional.ObjectTest$ClassWithThrowingConstructor()' with no args"); assertThat(e).hasCauseThat().isSameInstanceAs(ClassWithThrowingConstructor.thrownException); } } diff --git a/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java b/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java index f7bccb46e3..244d288371 100644 --- a/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java +++ b/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java @@ -92,21 +92,21 @@ public void testByteDeserializationLossy() { gson.fromJson("-129", byte.class); fail(); } catch (JsonSyntaxException e) { - assertThat(e.getMessage()).isEqualTo("Lossy conversion from -129 to byte; at path $"); + assertThat(e).hasMessageThat().isEqualTo("Lossy conversion from -129 to byte; at path $"); } try { gson.fromJson("256", byte.class); fail(); } catch (JsonSyntaxException e) { - assertThat(e.getMessage()).isEqualTo("Lossy conversion from 256 to byte; at path $"); + assertThat(e).hasMessageThat().isEqualTo("Lossy conversion from 256 to byte; at path $"); } try { gson.fromJson("2147483648", byte.class); fail(); } catch (JsonSyntaxException e) { - assertThat(e.getMessage()).isEqualTo("java.lang.NumberFormatException: Expected an int but was 2147483648 at line 1 column 11 path $"); + assertThat(e).hasMessageThat().isEqualTo("java.lang.NumberFormatException: Expected an int but was 2147483648 at line 1 column 11 path $"); } } @@ -140,21 +140,21 @@ public void testShortDeserializationLossy() { gson.fromJson("-32769", short.class); fail(); } catch (JsonSyntaxException e) { - assertThat(e.getMessage()).isEqualTo("Lossy conversion from -32769 to short; at path $"); + assertThat(e).hasMessageThat().isEqualTo("Lossy conversion from -32769 to short; at path $"); } try { gson.fromJson("65536", short.class); fail(); } catch (JsonSyntaxException e) { - assertThat(e.getMessage()).isEqualTo("Lossy conversion from 65536 to short; at path $"); + assertThat(e).hasMessageThat().isEqualTo("Lossy conversion from 65536 to short; at path $"); } try { gson.fromJson("2147483648", short.class); fail(); } catch (JsonSyntaxException e) { - assertThat(e.getMessage()).isEqualTo("java.lang.NumberFormatException: Expected an int but was 2147483648 at line 1 column 11 path $"); + assertThat(e).hasMessageThat().isEqualTo("java.lang.NumberFormatException: Expected an int but was 2147483648 at line 1 column 11 path $"); } } @@ -1064,6 +1064,7 @@ public void testDeserializingBigIntegerAsBigDecimal() { @Test public void testStringsAsBooleans() { String json = "['true', 'false', 'TRUE', 'yes', '1']"; - assertThat( gson.>fromJson(json, new TypeToken>() {}.getType())).isEqualTo(Arrays.asList(true, false, true, false, false)); + List deserialized = gson.fromJson(json, new TypeToken>() {}); + assertThat(deserialized).isEqualTo(Arrays.asList(true, false, true, false, false)); } } diff --git a/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java b/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java index 756603607b..bd0c71983c 100644 --- a/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java +++ b/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java @@ -79,7 +79,7 @@ public void checkPermission(Permission perm) { gson.getAdapter(clazz); fail(); } catch (SecurityException e) { - assertThat(e.getMessage()).isEqualTo("Gson: no-member-access"); + assertThat(e).hasMessageThat().isEqualTo("Gson: no-member-access"); } final AtomicBoolean wasReadCalled = new AtomicBoolean(false); @@ -107,6 +107,20 @@ public Object read(JsonReader in) throws IOException { } } + private static JsonIOException assertInaccessibleException(String json, Class toDeserialize) { + Gson gson = new Gson(); + try { + gson.fromJson(json, toDeserialize); + throw new AssertionError("Missing exception; test has to be run with `--illegal-access=deny`"); + } catch (JsonSyntaxException e) { + throw new AssertionError("Unexpected exception; test has to be run with `--illegal-access=deny`", e); + } catch (JsonIOException expected) { + assertThat(expected).hasMessageThat().endsWith("\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#reflection-inaccessible"); + // Return exception for further assertions + return expected; + } + } + /** * Test serializing an instance of a non-accessible internal class, but where * Gson supports serializing one of its superinterfaces. @@ -126,14 +140,19 @@ public void testSerializeInternalImplementationObject() { // But deserialization should fail Class internalClass = Collections.emptyList().getClass(); - try { - gson.fromJson("[]", internalClass); - fail("Missing exception; test has to be run with `--illegal-access=deny`"); - } catch (JsonSyntaxException e) { - throw new AssertionError("Unexpected exception; test has to be run with `--illegal-access=deny`", e); - } catch (JsonIOException expected) { - assertThat(expected).hasMessageThat().startsWith("Failed making constructor 'java.util.Collections$EmptyList()' accessible;" - + " either increase its visibility or write a custom InstanceCreator or TypeAdapter for its declaring type: "); - } + JsonIOException exception = assertInaccessibleException("[]", internalClass); + // Don't check exact class name because it is a JDK implementation detail + assertThat(exception).hasMessageThat().startsWith("Failed making constructor '"); + assertThat(exception).hasMessageThat().contains("' accessible; either increase its visibility or" + + " write a custom InstanceCreator or TypeAdapter for its declaring type: "); + } + + @Test + public void testInaccessibleField() { + JsonIOException exception = assertInaccessibleException("{}", Throwable.class); + // Don't check exact field name because it is a JDK implementation detail + assertThat(exception).hasMessageThat().startsWith("Failed making field 'java.lang.Throwable#"); + assertThat(exception).hasMessageThat().contains("' accessible; either increase its visibility or" + + " write a custom TypeAdapter for its declaring type."); } } diff --git a/gson/src/test/java/com/google/gson/functional/StreamingTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/StreamingTypeAdaptersTest.java index fecc828f47..d259564eb3 100644 --- a/gson/src/test/java/com/google/gson/functional/StreamingTypeAdaptersTest.java +++ b/gson/src/test/java/com/google/gson/functional/StreamingTypeAdaptersTest.java @@ -17,7 +17,6 @@ package com.google.gson.functional; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.fail; import com.google.common.base.Splitter; @@ -151,7 +150,7 @@ public void testSerialize1dArray() { public void testDeserialize1dArray() throws IOException { TypeAdapter arrayAdapter = miniGson.getAdapter(new TypeToken() {}); double[] array = arrayAdapter.fromJson("[1.0,2.0,3.0]"); - assertWithMessage(Arrays.toString(array)).that(Arrays.equals(new double[]{1.0, 2.0, 3.0}, array)).isTrue(); + assertThat(array).isEqualTo(new double[]{1.0, 2.0, 3.0}); } @Test @@ -166,7 +165,7 @@ public void testDeserialize2dArray() throws IOException { TypeAdapter arrayAdapter = miniGson.getAdapter(new TypeToken() {}); double[][] array = arrayAdapter.fromJson("[[1.0,2.0],[3.0]]"); double[][] expected = { {1.0, 2.0 }, { 3.0 } }; - assertWithMessage(Arrays.toString(array)).that(Arrays.deepEquals(expected, array)).isTrue(); + assertThat(array).isEqualTo(expected); } @Test @@ -195,7 +194,10 @@ public void testNullSafe() { try { gson.fromJson(json, Truck.class); fail(); - } catch (JsonSyntaxException expected) {} + } catch (JsonSyntaxException expected) { + assertThat(expected).hasMessageThat().isEqualTo("java.lang.IllegalStateException: Expected a string but was NULL at line 1 column 33 path $.passengers[0]" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#adapter-not-null-safe"); + } gson = new GsonBuilder().registerTypeAdapter(Person.class, typeAdapter.nullSafe()).create(); assertThat(gson.toJson(truck, Truck.class)) .isEqualTo("{\"horsePower\":1.0,\"passengers\":[null,\"jesse,30\"]}"); @@ -216,7 +218,7 @@ public void testSerializeRecursive() { + "'left':{'label':'left','left':null,'right':null}," + "'right':{'label':'right','left':null,'right':null}}"); } - + @Test public void testFromJsonTree() { JsonObject truckObject = new JsonObject(); diff --git a/gson/src/test/java/com/google/gson/functional/TypeAdapterPrecedenceTest.java b/gson/src/test/java/com/google/gson/functional/TypeAdapterPrecedenceTest.java index e96175a3c9..328b6ede1e 100644 --- a/gson/src/test/java/com/google/gson/functional/TypeAdapterPrecedenceTest.java +++ b/gson/src/test/java/com/google/gson/functional/TypeAdapterPrecedenceTest.java @@ -75,7 +75,7 @@ public void testStreamingFollowedByNonstreaming() { .registerTypeAdapter(Foo.class, newDeserializer("deserializer")) .create(); assertThat(gson.toJson(new Foo("foo"))).isEqualTo("\"foo via serializer\""); - assertThat( gson.fromJson("foo", Foo.class).name).isEqualTo("foo via deserializer"); + assertThat(gson.fromJson("foo", Foo.class).name).isEqualTo("foo via deserializer"); } @Test diff --git a/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java b/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java index f79c76b7cc..b3b7ff53d0 100644 --- a/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java +++ b/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java @@ -99,7 +99,7 @@ public void testPutNullValue_Forbidden() { map.put("a", null); fail(); } catch (NullPointerException e) { - assertThat(e.getMessage()).isEqualTo("value == null"); + assertThat(e).hasMessageThat().isEqualTo("value == null"); } assertThat(map).hasSize(0); assertThat(map).doesNotContainKey("a"); @@ -132,7 +132,7 @@ public void testEntrySetValueNull_Forbidden() { entry.setValue(null); fail(); } catch (NullPointerException e) { - assertThat(e.getMessage()).isEqualTo("value == null"); + assertThat(e).hasMessageThat().isEqualTo("value == null"); } assertThat(entry.getValue()).isEqualTo("1"); assertThat(map.get("a")).isEqualTo("1"); diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java index 9974da13ce..4ee7656147 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java @@ -63,21 +63,21 @@ public void testStrictNansAndInfinities() throws IOException { reader.nextDouble(); fail(); } catch (MalformedJsonException e) { - assertThat(e.getMessage()).isEqualTo("JSON forbids NaN and infinities: NaN"); + assertThat(e).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: NaN"); } assertThat(reader.nextString()).isEqualTo("NaN"); try { reader.nextDouble(); fail(); } catch (MalformedJsonException e) { - assertThat(e.getMessage()).isEqualTo("JSON forbids NaN and infinities: -Infinity"); + assertThat(e).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: -Infinity"); } assertThat(reader.nextString()).isEqualTo("-Infinity"); try { reader.nextDouble(); fail(); } catch (MalformedJsonException e) { - assertThat(e.getMessage()).isEqualTo("JSON forbids NaN and infinities: Infinity"); + assertThat(e).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: Infinity"); } assertThat(reader.nextString()).isEqualTo("Infinity"); reader.endArray(); diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java index 5faa718015..cea6023c61 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java @@ -133,7 +133,7 @@ public JsonElement deepCopy() { reader.peek(); fail(); } catch (MalformedJsonException expected) { - assertThat(expected.getMessage()).isEqualTo("Custom JsonElement subclass " + CustomSubclass.class.getName() + " is not supported"); + assertThat(expected).hasMessageThat().isEqualTo("Custom JsonElement subclass " + CustomSubclass.class.getName() + " is not supported"); } } diff --git a/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java b/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java index a9b5ca1039..e864000950 100644 --- a/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java +++ b/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java @@ -34,7 +34,7 @@ /** * Tests to measure performance for Gson. All tests in this file will be disabled in code. To run - * them remove disabled_ prefix from the tests and run them. + * them remove the {@code @Ignore} annotation from the tests. * * @author Inderjeet Singh * @author Joel Leitch @@ -58,7 +58,7 @@ public void testDummy() { @Test @Ignore - public void disabled_testStringDeserialization() { + public void testStringDeserialization() { StringBuilder sb = new StringBuilder(8096); sb.append("Error Yippie"); @@ -117,7 +117,7 @@ private CollectionEntry() { */ @Test @Ignore - public void disabled_testLargeCollectionSerialization() { + public void testLargeCollectionSerialization() { int count = 1400000; List list = new ArrayList<>(count); for (int i = 0; i < count; ++i) { @@ -131,7 +131,7 @@ public void disabled_testLargeCollectionSerialization() { */ @Test @Ignore - public void disabled_testLargeCollectionDeserialization() { + public void testLargeCollectionDeserialization() { StringBuilder sb = new StringBuilder(); int count = 87000; boolean first = true; @@ -157,7 +157,7 @@ public void disabled_testLargeCollectionDeserialization() { // Last I tested, Gson was able to serialize upto 14MB byte array @Test @Ignore - public void disabled_testByteArraySerialization() { + public void testByteArraySerialization() { for (int size = 4145152; true; size += 1036288) { byte[] ba = new byte[size]; for (int i = 0; i < size; ++i) { @@ -174,7 +174,7 @@ public void disabled_testByteArraySerialization() { // Last I tested, Gson was able to deserialize a byte array of 11MB @Test @Ignore - public void disabled_testByteArrayDeserialization() { + public void testByteArrayDeserialization() { for (int numElements = 10639296; true; numElements += 16384) { StringBuilder sb = new StringBuilder(numElements*2); sb.append("["); @@ -205,7 +205,7 @@ public void disabled_testByteArrayDeserialization() { @Test @Ignore - public void disabled_testSerializeClasses() { + public void testSerializeClasses() { ClassWithList c = new ClassWithList("str"); for (int i = 0; i < COLLECTION_SIZE; ++i) { c.list.add(new ClassWithField("element-" + i)); @@ -222,7 +222,7 @@ public void disabled_testSerializeClasses() { @Test @Ignore - public void disabled_testDeserializeClasses() { + public void testDeserializeClasses() { String json = buildJsonForClassWithList(); ClassWithList[] target = new ClassWithList[NUM_ITERATIONS]; long t1 = System.currentTimeMillis(); @@ -236,7 +236,7 @@ public void disabled_testDeserializeClasses() { @Test @Ignore - public void disabled_testLargeObjectSerializationAndDeserialization() { + public void testLargeObjectSerializationAndDeserialization() { Map largeObject = new HashMap<>(); for (long l = 0; l < 100000; l++) { largeObject.put("field" + l, l); @@ -256,7 +256,7 @@ public void disabled_testLargeObjectSerializationAndDeserialization() { @Test @Ignore - public void disabled_testSerializeExposedClasses() { + public void testSerializeExposedClasses() { ClassWithListOfObjects c1 = new ClassWithListOfObjects("str"); for (int i1 = 0; i1 < COLLECTION_SIZE; ++i1) { c1.list.add(new ClassWithExposedField("element-" + i1)); @@ -274,7 +274,7 @@ public void disabled_testSerializeExposedClasses() { @Test @Ignore - public void disabled_testDeserializeExposedClasses() { + public void testDeserializeExposedClasses() { String json = buildJsonForClassWithList(); ClassWithListOfObjects[] target = new ClassWithListOfObjects[NUM_ITERATIONS]; long t1 = System.currentTimeMillis(); @@ -288,7 +288,7 @@ public void disabled_testDeserializeExposedClasses() { @Test @Ignore - public void disabled_testLargeGsonMapRoundTrip() throws Exception { + public void testLargeGsonMapRoundTrip() throws Exception { Map original = new HashMap<>(); for (long i = 0; i < 1000000; i++) { original.put(i, i + 1); diff --git a/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java b/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java index 5df759f00b..61c5dfc2b9 100644 --- a/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java +++ b/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java @@ -235,21 +235,21 @@ class SubSubTypeToken2 extends SubTypeToken {} new SubTypeToken() {}; fail(); } catch (IllegalStateException expected) { - assertThat(expected.getMessage()).isEqualTo("Must only create direct subclasses of TypeToken"); + assertThat(expected).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken"); } try { new SubSubTypeToken1(); fail(); } catch (IllegalStateException expected) { - assertThat(expected.getMessage()).isEqualTo("Must only create direct subclasses of TypeToken"); + assertThat(expected).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken"); } try { new SubSubTypeToken2(); fail(); } catch (IllegalStateException expected) { - assertThat(expected.getMessage()).isEqualTo("Must only create direct subclasses of TypeToken"); + assertThat(expected).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken"); } } diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java index 2009f16bd6..90406b6df0 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java @@ -16,6 +16,7 @@ package com.google.gson.stream; +import static com.google.common.truth.Truth.assertThat; import static com.google.gson.stream.JsonToken.BEGIN_ARRAY; import static com.google.gson.stream.JsonToken.BEGIN_OBJECT; import static com.google.gson.stream.JsonToken.BOOLEAN; @@ -25,7 +26,6 @@ import static com.google.gson.stream.JsonToken.NULL; import static com.google.gson.stream.JsonToken.NUMBER; import static com.google.gson.stream.JsonToken.STRING; -import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import java.io.EOFException; @@ -273,7 +273,9 @@ public void testInvalidJsonInput() throws IOException { try { reader.nextName(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Invalid escape sequence at line 2 column 8 path $." + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -288,16 +290,16 @@ public void testNulls() { } @Test - public void testEmptyString() { + public void testEmptyString() throws IOException { try { new JsonReader(reader("")).beginArray(); fail(); - } catch (IOException expected) { + } catch (EOFException expected) { } try { new JsonReader(reader("")).beginObject(); fail(); - } catch (IOException expected) { + } catch (EOFException expected) { } } @@ -357,6 +359,8 @@ public void testUnescapingInvalidCharacters() throws IOException { reader.nextString(); fail(); } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Malformed Unicode escape \\u000g at line 1 column 5 path $[0]" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -368,7 +372,9 @@ public void testUnescapingTruncatedCharacters() throws IOException { try { reader.nextString(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Unterminated escape sequence at line 1 column 5 path $[0]" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -380,7 +386,9 @@ public void testUnescapingTruncatedSequence() throws IOException { try { reader.nextString(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Unterminated escape sequence at line 1 column 4 path $[0]" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -446,6 +454,7 @@ public void testStrictNonFiniteDoubles() throws IOException { reader.nextDouble(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } } @@ -458,6 +467,8 @@ public void testStrictQuotedNonFiniteDoubles() throws IOException { reader.nextDouble(); fail(); } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: NaN at line 1 column 7 path $[0]" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -494,6 +505,7 @@ public void testStrictNonFiniteDoublesWithSkipValue() throws IOException { reader.skipValue(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } } @@ -532,8 +544,8 @@ public void testLongs() throws IOException { } @Test - @Ignore - public void disabled_testNumberWithOctalPrefix() throws IOException { + @Ignore("JsonReader advances after exception for invalid number was thrown; to be decided if that is acceptable") + public void testNumberWithOctalPrefix() throws IOException { String json = "[01]"; JsonReader reader = new JsonReader(reader(json)); reader.beginArray(); @@ -541,21 +553,25 @@ public void disabled_testNumberWithOctalPrefix() throws IOException { reader.peek(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } try { reader.nextInt(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "TODO"); } try { reader.nextLong(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "TODO"); } try { reader.nextDouble(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "TODO"); } assertThat(reader.nextString()).isEqualTo("01"); reader.endArray(); @@ -582,6 +598,7 @@ public void testPeekingUnquotedStringsPrefixedWithBooleans() throws IOException reader.nextBoolean(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "a boolean", "STRING", "line 1 column 2 path $[0]"); } assertThat(reader.nextString()).isEqualTo("truey"); reader.endArray(); @@ -723,7 +740,7 @@ public void testNegativeZero() throws Exception { */ @Test @Ignore - public void disabled_testPeekLargerThanLongMaxValue() throws IOException { + public void testPeekLargerThanLongMaxValue() throws IOException { JsonReader reader = new JsonReader(reader("[9223372036854775808]")); reader.setLenient(true); reader.beginArray(); @@ -741,7 +758,7 @@ public void disabled_testPeekLargerThanLongMaxValue() throws IOException { */ @Test @Ignore - public void disabled_testPeekLargerThanLongMinValue() throws IOException { + public void testPeekLargerThanLongMinValue() throws IOException { @SuppressWarnings("FloatingPointLiteralPrecision") double d = -9223372036854775809d; JsonReader reader = new JsonReader(reader("[-9223372036854775809]")); @@ -762,7 +779,7 @@ public void disabled_testPeekLargerThanLongMinValue() throws IOException { */ @Test @Ignore - public void disabled_testHighPrecisionLong() throws IOException { + public void testHighPrecisionLong() throws IOException { String json = "[9223372036854775806.000]"; JsonReader reader = new JsonReader(reader(json)); reader.beginArray(); @@ -817,7 +834,9 @@ public void testMissingValue() throws IOException { try { reader.nextString(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected value at line 1 column 6 path $.a" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -830,38 +849,41 @@ public void testPrematureEndOfInput() throws IOException { try { reader.nextName(); fail(); - } catch (IOException expected) { + } catch (EOFException expected) { } } @Test public void testPrematurelyClosed() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\":[]}")); + reader.beginObject(); + reader.close(); try { - JsonReader reader = new JsonReader(reader("{\"a\":[]}")); - reader.beginObject(); - reader.close(); reader.nextName(); fail(); } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("JsonReader is closed"); } + reader = new JsonReader(reader("{\"a\":[]}")); + reader.close(); try { - JsonReader reader = new JsonReader(reader("{\"a\":[]}")); - reader.close(); reader.beginObject(); fail(); } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("JsonReader is closed"); } + reader = new JsonReader(reader("{\"a\":true}")); + reader.beginObject(); + String unused1 = reader.nextName(); + JsonToken unused2 = reader.peek(); + reader.close(); try { - JsonReader reader = new JsonReader(reader("{\"a\":true}")); - reader.beginObject(); - String unused1 = reader.nextName(); - JsonToken unused2 = reader.peek(); - reader.close(); reader.nextBoolean(); fail(); } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("JsonReader is closed"); } } @@ -873,53 +895,63 @@ public void testNextFailuresDoNotAdvance() throws IOException { String unused = reader.nextString(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "a string", "NAME", "line 1 column 3 path $."); } assertThat(reader.nextName()).isEqualTo("a"); try { String unused = reader.nextName(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "a name", "BOOLEAN", "line 1 column 10 path $.a"); } try { reader.beginArray(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "BEGIN_ARRAY", "BOOLEAN", "line 1 column 10 path $.a"); } try { reader.endArray(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "END_ARRAY", "BOOLEAN", "line 1 column 10 path $.a"); } try { reader.beginObject(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "BEGIN_OBJECT", "BOOLEAN", "line 1 column 10 path $.a"); } try { reader.endObject(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "END_OBJECT", "BOOLEAN", "line 1 column 10 path $.a"); } assertThat(reader.nextBoolean()).isTrue(); try { reader.nextString(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "a string", "END_OBJECT", "line 1 column 11 path $.a"); } try { reader.nextName(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "a name", "END_OBJECT", "line 1 column 11 path $.a"); } try { reader.beginArray(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "BEGIN_ARRAY", "END_OBJECT", "line 1 column 11 path $.a"); } try { reader.endArray(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "END_ARRAY", "END_OBJECT", "line 1 column 11 path $.a"); } reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT); @@ -947,6 +979,7 @@ public void testStringNullIsNotNull() throws IOException { reader.nextNull(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "null", "STRING", "line 1 column 3 path $[0]"); } } @@ -958,6 +991,7 @@ public void testNullLiteralIsNotAString() throws IOException { reader.nextString(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "a string", "NULL", "line 1 column 6 path $[0]"); } } @@ -969,7 +1003,8 @@ public void testStrictNameValueSeparator() throws IOException { try { reader.nextBoolean(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 6 path $.a"); } reader = new JsonReader(reader("{\"a\"=>true}")); @@ -978,7 +1013,8 @@ public void testStrictNameValueSeparator() throws IOException { try { reader.nextBoolean(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 6 path $.a"); } } @@ -1005,7 +1041,8 @@ public void testStrictNameValueSeparatorWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 6 path $.a"); } reader = new JsonReader(reader("{\"a\"=>true}")); @@ -1014,7 +1051,8 @@ public void testStrictNameValueSeparatorWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 6 path $.a"); } } @@ -1045,7 +1083,8 @@ public void testStrictComments() throws IOException { try { reader.nextBoolean(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } reader = new JsonReader(reader("[# comment \n true]")); @@ -1053,7 +1092,8 @@ public void testStrictComments() throws IOException { try { reader.nextBoolean(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } reader = new JsonReader(reader("[/* comment */ true]")); @@ -1061,7 +1101,8 @@ public void testStrictComments() throws IOException { try { reader.nextBoolean(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } } @@ -1090,7 +1131,8 @@ public void testStrictCommentsWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } reader = new JsonReader(reader("[# comment \n true]")); @@ -1098,7 +1140,8 @@ public void testStrictCommentsWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } reader = new JsonReader(reader("[/* comment */ true]")); @@ -1106,7 +1149,8 @@ public void testStrictCommentsWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } } @@ -1117,7 +1161,8 @@ public void testStrictUnquotedNames() throws IOException { try { reader.nextName(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $."); } } @@ -1136,7 +1181,8 @@ public void testStrictUnquotedNamesWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $."); } } @@ -1147,7 +1193,8 @@ public void testStrictSingleQuotedNames() throws IOException { try { reader.nextName(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $."); } } @@ -1166,7 +1213,8 @@ public void testStrictSingleQuotedNamesWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $."); } } @@ -1178,6 +1226,7 @@ public void testStrictUnquotedStrings() throws IOException { reader.nextString(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } } @@ -1189,6 +1238,7 @@ public void testStrictUnquotedStringsWithSkipValue() throws IOException { reader.skipValue(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } } @@ -1207,7 +1257,8 @@ public void testStrictSingleQuotedStrings() throws IOException { try { reader.nextString(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } } @@ -1226,7 +1277,8 @@ public void testStrictSingleQuotedStringsWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } } @@ -1235,10 +1287,10 @@ public void testStrictSemicolonDelimitedArray() throws IOException { JsonReader reader = new JsonReader(reader("[true;true]")); reader.beginArray(); try { - boolean unused1 = reader.nextBoolean(); - boolean unused2 = reader.nextBoolean(); + boolean unused = reader.nextBoolean(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } } @@ -1256,10 +1308,10 @@ public void testStrictSemicolonDelimitedArrayWithSkipValue() throws IOException JsonReader reader = new JsonReader(reader("[true;true]")); reader.beginArray(); try { - reader.skipValue(); reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } } @@ -1269,10 +1321,10 @@ public void testStrictSemicolonDelimitedNameValuePair() throws IOException { reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try { - boolean unused1 = reader.nextBoolean(); - String unused2 = reader.nextName(); + boolean unused = reader.nextBoolean(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 6 path $.a"); } } @@ -1292,10 +1344,10 @@ public void testStrictSemicolonDelimitedNameValuePairWithSkipValue() throws IOEx reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try { - reader.skipValue(); reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 6 path $.a"); } } @@ -1307,7 +1359,8 @@ public void testStrictUnnecessaryArraySeparators() throws IOException { try { reader.nextNull(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 8 path $[1]"); } reader = new JsonReader(reader("[,true]")); @@ -1315,7 +1368,8 @@ public void testStrictUnnecessaryArraySeparators() throws IOException { try { reader.nextNull(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } reader = new JsonReader(reader("[true,]")); @@ -1324,7 +1378,8 @@ public void testStrictUnnecessaryArraySeparators() throws IOException { try { reader.nextNull(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 8 path $[1]"); } reader = new JsonReader(reader("[,]")); @@ -1332,7 +1387,8 @@ public void testStrictUnnecessaryArraySeparators() throws IOException { try { reader.nextNull(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } } @@ -1376,7 +1432,8 @@ public void testStrictUnnecessaryArraySeparatorsWithSkipValue() throws IOExcepti try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 8 path $[1]"); } reader = new JsonReader(reader("[,true]")); @@ -1384,7 +1441,8 @@ public void testStrictUnnecessaryArraySeparatorsWithSkipValue() throws IOExcepti try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } reader = new JsonReader(reader("[true,]")); @@ -1393,7 +1451,8 @@ public void testStrictUnnecessaryArraySeparatorsWithSkipValue() throws IOExcepti try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 8 path $[1]"); } reader = new JsonReader(reader("[,]")); @@ -1401,7 +1460,8 @@ public void testStrictUnnecessaryArraySeparatorsWithSkipValue() throws IOExcepti try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } } @@ -1413,7 +1473,8 @@ public void testStrictMultipleTopLevelValues() throws IOException { try { reader.peek(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 5 path $"); } } @@ -1437,7 +1498,8 @@ public void testStrictMultipleTopLevelValuesWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 5 path $"); } } @@ -1477,22 +1539,24 @@ public void testTopLevelValueTypeWithSkipValue() throws IOException { } @Test - public void testStrictNonExecutePrefix() { + public void testStrictNonExecutePrefix() throws IOException { JsonReader reader = new JsonReader(reader(")]}'\n []")); try { reader.beginArray(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 1 path $"); } } @Test - public void testStrictNonExecutePrefixWithSkipValue() { + public void testStrictNonExecutePrefixWithSkipValue() throws IOException { JsonReader reader = new JsonReader(reader(")]}'\n []")); try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 1 path $"); } } @@ -1515,14 +1579,16 @@ public void testLenientNonExecutePrefixWithLeadingWhitespace() throws IOExceptio } @Test - public void testLenientPartialNonExecutePrefix() { + public void testLenientPartialNonExecutePrefix() throws IOException { JsonReader reader = new JsonReader(reader(")]}' []")); reader.setLenient(true); + assertThat(reader.nextString()).isEqualTo(")"); try { - assertThat(reader.nextString()).isEqualTo(")"); reader.nextString(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Unexpected value at line 1 column 3 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1540,7 +1606,8 @@ public void testBomForbiddenAsOtherCharacterInDocument() throws IOException { try { reader.endArray(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } } @@ -1606,8 +1673,8 @@ private void testFailWithPosition(String message, String json) throws IOExceptio try { JsonToken unused2 = reader1.peek(); fail(); - } catch (IOException expected) { - assertThat(expected.getMessage()).isEqualTo(message); + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo(message + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } // Also validate that it works when skipping. @@ -1618,8 +1685,8 @@ private void testFailWithPosition(String message, String json) throws IOExceptio try { JsonToken unused3 = reader2.peek(); fail(); - } catch (IOException expected) { - assertThat(expected.getMessage()).isEqualTo(message); + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo(message + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1636,8 +1703,10 @@ public void testFailWithPositionDeepPath() throws IOException { try { JsonToken unused5 = reader.peek(); fail(); - } catch (IOException expected) { - assertThat(expected.getMessage()).isEqualTo("Expected value at line 1 column 14 path $[1].a[2]"); + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo( + "Expected value at line 1 column 14 path $[1].a[2]" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1646,9 +1715,10 @@ public void testStrictVeryLongNumber() throws IOException { JsonReader reader = new JsonReader(reader("[0." + repeat('9', 8192) + "]")); reader.beginArray(); try { - assertThat(reader.nextDouble()).isEqualTo(1d); + reader.nextDouble(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } } @@ -1721,6 +1791,8 @@ public void testStringEndingInSlash() throws IOException { reader.peek(); fail(); } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected value at line 1 column 1 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1732,6 +1804,8 @@ public void testDocumentWithCommentEndingInSlash() throws IOException { reader.peek(); fail(); } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected value at line 1 column 10 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1743,6 +1817,8 @@ public void testStringWithLeadingSlash() throws IOException { reader.peek(); fail(); } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected value at line 1 column 1 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1757,6 +1833,8 @@ public void testUnterminatedObject() throws IOException { reader.peek(); fail(); } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Unterminated object at line 1 column 16 path $.a" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1883,7 +1961,9 @@ public void testStrictExtraCommasInMaps() throws IOException { try { reader.peek(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected name at line 1 column 11 path $.a" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1897,7 +1977,9 @@ public void testLenientExtraCommasInMaps() throws IOException { try { reader.peek(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected name at line 1 column 11 path $.a" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1909,45 +1991,45 @@ private String repeat(char c, int count) { @Test public void testMalformedDocuments() throws IOException { - assertDocument("{]", BEGIN_OBJECT, IOException.class); - assertDocument("{,", BEGIN_OBJECT, IOException.class); - assertDocument("{{", BEGIN_OBJECT, IOException.class); - assertDocument("{[", BEGIN_OBJECT, IOException.class); - assertDocument("{:", BEGIN_OBJECT, IOException.class); - assertDocument("{\"name\",", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{\"name\",", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{\"name\":}", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{\"name\"::", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{\"name\":,", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{\"name\"=}", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{\"name\"=>}", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{\"name\"=>\"string\":", BEGIN_OBJECT, NAME, STRING, IOException.class); - assertDocument("{\"name\"=>\"string\"=", BEGIN_OBJECT, NAME, STRING, IOException.class); - assertDocument("{\"name\"=>\"string\"=>", BEGIN_OBJECT, NAME, STRING, IOException.class); - assertDocument("{\"name\"=>\"string\",", BEGIN_OBJECT, NAME, STRING, IOException.class); + assertDocument("{]", BEGIN_OBJECT, MalformedJsonException.class); + assertDocument("{,", BEGIN_OBJECT, MalformedJsonException.class); + assertDocument("{{", BEGIN_OBJECT, MalformedJsonException.class); + assertDocument("{[", BEGIN_OBJECT, MalformedJsonException.class); + assertDocument("{:", BEGIN_OBJECT, MalformedJsonException.class); + assertDocument("{\"name\",", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{\"name\",", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{\"name\":}", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{\"name\"::", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{\"name\":,", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{\"name\"=}", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{\"name\"=>}", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{\"name\"=>\"string\":", BEGIN_OBJECT, NAME, STRING, MalformedJsonException.class); + assertDocument("{\"name\"=>\"string\"=", BEGIN_OBJECT, NAME, STRING, MalformedJsonException.class); + assertDocument("{\"name\"=>\"string\"=>", BEGIN_OBJECT, NAME, STRING, MalformedJsonException.class); + assertDocument("{\"name\"=>\"string\",", BEGIN_OBJECT, NAME, STRING, EOFException.class); assertDocument("{\"name\"=>\"string\",\"name\"", BEGIN_OBJECT, NAME, STRING, NAME); - assertDocument("[}", BEGIN_ARRAY, IOException.class); + assertDocument("[}", BEGIN_ARRAY, MalformedJsonException.class); assertDocument("[,]", BEGIN_ARRAY, NULL, NULL, END_ARRAY); - assertDocument("{", BEGIN_OBJECT, IOException.class); - assertDocument("{\"name\"", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{\"name\",", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{'name'", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{'name',", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{name", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("[", BEGIN_ARRAY, IOException.class); - assertDocument("[string", BEGIN_ARRAY, STRING, IOException.class); - assertDocument("[\"string\"", BEGIN_ARRAY, STRING, IOException.class); - assertDocument("['string'", BEGIN_ARRAY, STRING, IOException.class); - assertDocument("[123", BEGIN_ARRAY, NUMBER, IOException.class); - assertDocument("[123,", BEGIN_ARRAY, NUMBER, IOException.class); - assertDocument("{\"name\":123", BEGIN_OBJECT, NAME, NUMBER, IOException.class); - assertDocument("{\"name\":123,", BEGIN_OBJECT, NAME, NUMBER, IOException.class); - assertDocument("{\"name\":\"string\"", BEGIN_OBJECT, NAME, STRING, IOException.class); - assertDocument("{\"name\":\"string\",", BEGIN_OBJECT, NAME, STRING, IOException.class); - assertDocument("{\"name\":'string'", BEGIN_OBJECT, NAME, STRING, IOException.class); - assertDocument("{\"name\":'string',", BEGIN_OBJECT, NAME, STRING, IOException.class); - assertDocument("{\"name\":false", BEGIN_OBJECT, NAME, BOOLEAN, IOException.class); - assertDocument("{\"name\":false,,", BEGIN_OBJECT, NAME, BOOLEAN, IOException.class); + assertDocument("{", BEGIN_OBJECT, EOFException.class); + assertDocument("{\"name\"", BEGIN_OBJECT, NAME, EOFException.class); + assertDocument("{\"name\",", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{'name'", BEGIN_OBJECT, NAME, EOFException.class); + assertDocument("{'name',", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{name", BEGIN_OBJECT, NAME, EOFException.class); + assertDocument("[", BEGIN_ARRAY, EOFException.class); + assertDocument("[string", BEGIN_ARRAY, STRING, EOFException.class); + assertDocument("[\"string\"", BEGIN_ARRAY, STRING, EOFException.class); + assertDocument("['string'", BEGIN_ARRAY, STRING, EOFException.class); + assertDocument("[123", BEGIN_ARRAY, NUMBER, EOFException.class); + assertDocument("[123,", BEGIN_ARRAY, NUMBER, EOFException.class); + assertDocument("{\"name\":123", BEGIN_OBJECT, NAME, NUMBER, EOFException.class); + assertDocument("{\"name\":123,", BEGIN_OBJECT, NAME, NUMBER, EOFException.class); + assertDocument("{\"name\":\"string\"", BEGIN_OBJECT, NAME, STRING, EOFException.class); + assertDocument("{\"name\":\"string\",", BEGIN_OBJECT, NAME, STRING, EOFException.class); + assertDocument("{\"name\":'string'", BEGIN_OBJECT, NAME, STRING, EOFException.class); + assertDocument("{\"name\":'string',", BEGIN_OBJECT, NAME, STRING, EOFException.class); + assertDocument("{\"name\":false", BEGIN_OBJECT, NAME, BOOLEAN, EOFException.class); + assertDocument("{\"name\":false,,", BEGIN_OBJECT, NAME, BOOLEAN, MalformedJsonException.class); } /** @@ -1964,6 +2046,8 @@ public void testUnterminatedStringFailure() throws IOException { reader.nextString(); fail(); } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Unterminated string at line 1 column 9 path $[0]" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1983,6 +2067,17 @@ public void testReadAcrossBuffers() throws IOException { assertThat(token).isEqualTo(JsonToken.NUMBER); } + private static void assertStrictError(MalformedJsonException exception, String expectedLocation) { + assertThat(exception).hasMessageThat().isEqualTo("Use JsonReader.setLenient(true) to accept malformed JSON at " + expectedLocation + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); + } + + private static void assertUnexpectedStructureError(IllegalStateException exception, String expectedToken, String actualToken, String expectedLocation) { + String troubleshootingId = actualToken.equals("NULL") ? "adapter-not-null-safe" : "unexpected-json-structure"; + assertThat(exception).hasMessageThat().isEqualTo("Expected " + expectedToken + " but was " + actualToken + " at " + expectedLocation + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#" + troubleshootingId); + } + private void assertDocument(String document, Object... expectations) throws IOException { JsonReader reader = new JsonReader(reader(document)); reader.setLenient(true); @@ -2005,15 +2100,15 @@ private void assertDocument(String document, Object... expectations) throws IOEx assertThat(reader.nextInt()).isEqualTo(123); } else if (expectation == NULL) { reader.nextNull(); - } else if (expectation == IOException.class) { + } else if (expectation instanceof Class && Exception.class.isAssignableFrom((Class) expectation)) { try { reader.peek(); fail(); - } catch (IOException expected) { - // OK: Should fail + } catch (Exception expected) { + assertThat(expected.getClass()).isEqualTo((Class) expectation); } } else { - throw new AssertionError(); + throw new AssertionError("Unsupported expectation value: " + expectation); } } } diff --git a/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java b/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java index 70470a166b..2ee120f38e 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java @@ -472,7 +472,7 @@ public void testMalformedNumbers() throws IOException { jsonWriter.value(new LazilyParsedNumber(malformedNumber)); fail("Should have failed writing malformed number: " + malformedNumber); } catch (IllegalArgumentException e) { - assertThat(e.getMessage()).isEqualTo("String created by class com.google.gson.internal.LazilyParsedNumber is not a valid JSON number: " + malformedNumber); + assertThat(e).hasMessageThat().isEqualTo("String created by class com.google.gson.internal.LazilyParsedNumber is not a valid JSON number: " + malformedNumber); } } } From 55cf9fa941e93030244ecc9092f65183c16fc745 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 07:27:14 -0700 Subject: [PATCH 07/95] Bump jackson-databind from 2.14.2 to 2.15.0 (#2377) Bumps [jackson-databind](https://github.com/FasterXML/jackson) from 2.14.2 to 2.15.0. - [Release notes](https://github.com/FasterXML/jackson/releases) - [Commits](https://github.com/FasterXML/jackson/commits) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- metrics/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metrics/pom.xml b/metrics/pom.xml index b5b708c636..b2fc7b5e6d 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -48,7 +48,7 @@ com.fasterxml.jackson.core jackson-databind - 2.14.2 + 2.15.0 com.google.caliper From 8333ccac62eb586f437305d66f4b6438a3529c52 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 May 2023 06:23:28 -0700 Subject: [PATCH 08/95] Bump moditect-maven-plugin from 1.0.0.RC3 to 1.0.0.Final (#2381) Bumps [moditect-maven-plugin](https://github.com/moditect/moditect) from 1.0.0.RC3 to 1.0.0.Final. - [Release notes](https://github.com/moditect/moditect/releases) - [Commits](https://github.com/moditect/moditect/compare/1.0.0.RC3...1.0.0.Final) --- updated-dependencies: - dependency-name: org.moditect:moditect-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gson/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/pom.xml b/gson/pom.xml index afd6ac7183..581e19d561 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -246,7 +246,7 @@ org.moditect moditect-maven-plugin - 1.0.0.RC3 + 1.0.0.Final add-module-info From 5fffd5aca1094d045e978bd143032500934395f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 12:11:43 -0700 Subject: [PATCH 09/95] Bump maven-gpg-plugin from 3.0.1 to 3.1.0 (#2384) Bumps [maven-gpg-plugin](https://github.com/apache/maven-gpg-plugin) from 3.0.1 to 3.1.0. - [Commits](https://github.com/apache/maven-gpg-plugin/compare/maven-gpg-plugin-3.0.1...maven-gpg-plugin-3.1.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-gpg-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a57ca468d1..5c5a789402 100644 --- a/pom.xml +++ b/pom.xml @@ -187,7 +187,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.0.1 + 3.1.0 org.apache.maven.plugins From 1339b506298cb854877f5d39bea1affc8162146c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 12:17:29 -0700 Subject: [PATCH 10/95] Bump maven-surefire-plugin from 3.0.0 to 3.1.0 (#2383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.0.0 to 3.1.0. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.0.0...surefire-3.1.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Éamonn McManus --- gson/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/pom.xml b/gson/pom.xml index 581e19d561..2930ac46ef 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -136,7 +136,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0 + 3.1.0 -XDcompilePolicy=simple - -Xplugin:ErrorProne -XepExcludedPaths:.*/generated-test-sources/protobuf/.* + -Xplugin:ErrorProne + -XepExcludedPaths:.*/generated-test-sources/protobuf/.* + -Xep:HidingField:OFF + -Xep:NotJavadoc:OFF + -Xlint:all,-options @@ -141,7 +145,7 @@ com.google.errorprone error_prone_core - 2.18.0 + 2.19.0 From eae63f92fb42e603bddd22f238a2a82a61140ce2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 May 2023 09:43:22 -0700 Subject: [PATCH 13/95] Bump error_prone_annotations from 2.19.0 to 2.19.1 (#2389) Bumps [error_prone_annotations](https://github.com/google/error-prone) from 2.19.0 to 2.19.1. - [Release notes](https://github.com/google/error-prone/releases) - [Commits](https://github.com/google/error-prone/compare/v2.19.0...v2.19.1) --- updated-dependencies: - dependency-name: com.google.errorprone:error_prone_annotations dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gson/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/pom.xml b/gson/pom.xml index c21bfeb80c..abbe573dfd 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -46,7 +46,7 @@ com.google.errorprone error_prone_annotations - 2.19.0 + 2.19.1 From 3c4b0721005dbc5e0d7515b95a321715d48b3291 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 May 2023 09:43:50 -0700 Subject: [PATCH 14/95] Bump error_prone_core from 2.19.0 to 2.19.1 (#2388) Bumps [error_prone_core](https://github.com/google/error-prone) from 2.19.0 to 2.19.1. - [Release notes](https://github.com/google/error-prone/releases) - [Commits](https://github.com/google/error-prone/compare/v2.19.0...v2.19.1) --- updated-dependencies: - dependency-name: com.google.errorprone:error_prone_core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0607d2dc1c..c236153da7 100644 --- a/pom.xml +++ b/pom.xml @@ -145,7 +145,7 @@ com.google.errorprone error_prone_core - 2.19.0 + 2.19.1
From a9bd30b57796b4490d9f3eb4b59edea49fb327a3 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Thu, 11 May 2023 22:18:09 +0200 Subject: [PATCH 15/95] Enable Error Prone HidingField bug pattern again (#2390) Bug which required disabling this bug pattern was fixed in 2.19.1 and Error Prone dependency was updated to that version in commit 3c4b0721005dbc5e0d7515b95a321715d48b3291 --- pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/pom.xml b/pom.xml index c236153da7..a3ca02886a 100644 --- a/pom.xml +++ b/pom.xml @@ -134,7 +134,6 @@ -XDcompilePolicy=simple -Xplugin:ErrorProne -XepExcludedPaths:.*/generated-test-sources/protobuf/.* - -Xep:HidingField:OFF -Xep:NotJavadoc:OFF true diff --git a/pom.xml b/pom.xml index 26e8022abe..55b17e8068 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,7 @@ gson + shrinker-test extras metrics proto @@ -83,7 +84,12 @@ junit junit 4.13.2 - test + + + + com.google.truth + truth + 1.1.3 diff --git a/proto/pom.xml b/proto/pom.xml index 9225ff2239..313bb10613 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -62,7 +62,6 @@ com.google.truth truth - 1.1.3 test diff --git a/shrinker-test/README.md b/shrinker-test/README.md new file mode 100644 index 0000000000..f9b674d143 --- /dev/null +++ b/shrinker-test/README.md @@ -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. diff --git a/shrinker-test/pom.xml b/shrinker-test/pom.xml new file mode 100644 index 0000000000..bd6f789ac6 --- /dev/null +++ b/shrinker-test/pom.xml @@ -0,0 +1,218 @@ + + + + 4.0.0 + + + com.google.code.gson + gson-parent + 2.10.2-SNAPSHOT + + + shrinker-test + + + 8 + + + + + + google + https://maven.google.com + + + + + + com.google.code.gson + gson + ${project.parent.version} + + + + junit + junit + test + + + com.google.truth + truth + test + + + + + + + + com.github.siom79.japicmp + japicmp-maven-plugin + + + true + + + + org.apache.maven.plugins + maven-deploy-plugin + + + true + + + + + + + + + com.github.wvengen + proguard-maven-plugin + 2.6.0 + + + package + + proguard + + + + + true + ${project.basedir}/proguard.pro + + + + + + ${java.home}/jmods/java.base.jmod + + ${java.home}/jmods/java.sql.jmod + + ${java.home}/jmods/java.compiler.jmod + + + true + proguard-output.jar + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.4.1 + + + package + + shade + + + false + + false + + + *:* + + + META-INF/MANIFEST.MF + + + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + r8 + package + + java + + + + false + false + + true + + + com.android.tools + r8 + + + + com.android.tools.r8.R8 + + --release + + --classfile + --lib${java.home} + --pg-conf${project.basedir}/r8.pro + + --pg-map-output${project.build.directory}/r8_map.txt + --output${project.build.directory}/r8-output.jar + ${project.build.directory}/${project.build.finalName}.jar + + + + + + + + com.android.tools + r8 + 8.0.40 + + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.1.0 + + + + integration-test + verify + + + + + + + diff --git a/shrinker-test/proguard.pro b/shrinker-test/proguard.pro new file mode 100644 index 0000000000..0cb35048c9 --- /dev/null +++ b/shrinker-test/proguard.pro @@ -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 ; +} diff --git a/shrinker-test/r8.pro b/shrinker-test/r8.pro new file mode 100644 index 0000000000..a415aa1614 --- /dev/null +++ b/shrinker-test/r8.pro @@ -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 { + (); +} +-keepclassmembers class com.example.GenericClasses$GenericClass { + (); +} +-keepclassmembers class com.example.GenericClasses$UsingGenericClass { + (); +} +-keepclassmembers class com.example.GenericClasses$GenericUsingGenericClass { + (); +} + +# 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 ; +} + +# Keep enum constants which are not explicitly used in code +-keep class com.example.EnumClass { + ** SECOND; +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithAdapter.java b/shrinker-test/src/main/java/com/example/ClassWithAdapter.java new file mode 100644 index 0000000000..aa7f08da6b --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithAdapter.java @@ -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 { + @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 + "]"; + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithDefaultConstructor.java b/shrinker-test/src/main/java/com/example/ClassWithDefaultConstructor.java new file mode 100644 index 0000000000..6296237f66 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithDefaultConstructor.java @@ -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; + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithExposeAnnotation.java b/shrinker-test/src/main/java/com/example/ClassWithExposeAnnotation.java new file mode 100644 index 0000000000..30a61fa921 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithExposeAnnotation.java @@ -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; +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java b/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java new file mode 100644 index 0000000000..238ee1818e --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java @@ -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 { + @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 TypeAdapter create(Gson gson, TypeToken type) { + @SuppressWarnings("unchecked") // the code below is not type safe, but does not matter for this test + TypeAdapter r = (TypeAdapter) new TypeAdapter() { + @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 { + @Override + public JsonElement serialize(DummyClass src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive("serializer-" + src.s); + } + } + + static class Deserializer implements JsonDeserializer { + @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; + } + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithNamedFields.java b/shrinker-test/src/main/java/com/example/ClassWithNamedFields.java new file mode 100644 index 0000000000..0a68da9c25 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithNamedFields.java @@ -0,0 +1,10 @@ +package com.example; + +public class ClassWithNamedFields { + public int myField; + public short notAccessedField = -1; + + public ClassWithNamedFields(int i) { + myField = i; + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithSerializedName.java b/shrinker-test/src/main/java/com/example/ClassWithSerializedName.java new file mode 100644 index 0000000000..ce982215ca --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithSerializedName.java @@ -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; + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithVersionAnnotations.java b/shrinker-test/src/main/java/com/example/ClassWithVersionAnnotations.java new file mode 100644 index 0000000000..9c554f7e9b --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithVersionAnnotations.java @@ -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; +} diff --git a/shrinker-test/src/main/java/com/example/DefaultConstructorMain.java b/shrinker-test/src/main/java/com/example/DefaultConstructorMain.java new file mode 100644 index 0000000000..e570866bec --- /dev/null +++ b/shrinker-test/src/main/java/com/example/DefaultConstructorMain.java @@ -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; + } +} diff --git a/shrinker-test/src/main/java/com/example/EnumClass.java b/shrinker-test/src/main/java/com/example/EnumClass.java new file mode 100644 index 0000000000..36688887bb --- /dev/null +++ b/shrinker-test/src/main/java/com/example/EnumClass.java @@ -0,0 +1,6 @@ +package com.example; + +public enum EnumClass { + FIRST, + SECOND +} diff --git a/shrinker-test/src/main/java/com/example/EnumClassWithSerializedName.java b/shrinker-test/src/main/java/com/example/EnumClassWithSerializedName.java new file mode 100644 index 0000000000..a127a8be13 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/EnumClassWithSerializedName.java @@ -0,0 +1,10 @@ +package com.example; + +import com.google.gson.annotations.SerializedName; + +public enum EnumClassWithSerializedName { + @SerializedName("one") + FIRST, + @SerializedName("two") + SECOND +} diff --git a/shrinker-test/src/main/java/com/example/GenericClasses.java b/shrinker-test/src/main/java/com/example/GenericClasses.java new file mode 100644 index 0000000000..cd91149be4 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/GenericClasses.java @@ -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 { + @SerializedName("t") + T t; + + @Override + public String toString() { + return "{t=" + t + "}"; + } + } + + static class UsingGenericClass { + @SerializedName("g") + GenericClass g; + + @Override + public String toString() { + return "{g=" + g + "}"; + } + } + + static class GenericUsingGenericClass { + @SerializedName("g") + GenericClass 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 { + @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(); + } + } + } +} diff --git a/shrinker-test/src/main/java/com/example/Main.java b/shrinker-test/src/main/java/com/example/Main.java new file mode 100644 index 0000000000..55bbb6377d --- /dev/null +++ b/shrinker-test/src/main/java/com/example/Main.java @@ -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()}. + * + *

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 outputConsumer) { + // Create the TypeToken instances on demand because creation of them can fail when + // generic signatures were erased + testTypeTokenWriteRead(outputConsumer, "anonymous", () -> new TypeToken>() {}); + 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 outputConsumer, String description, Supplier> 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 fromJson(Gson gson, String json, Class c) { + return gson.fromJson(json, same(c)); + } + + private static void testNamedFields(BiConsumer 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 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 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 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 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 outputConsumer) { + Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + TestExecutor.run(outputConsumer, "Write: @Expose", () -> toJson(gson, new ClassWithExposeAnnotation())); + } + + private static void testVersionAnnotations(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setVersion(1).create(); + TestExecutor.run(outputConsumer, "Write: Version annotations", () -> toJson(gson, new ClassWithVersionAnnotations())); + } + + private static void testJsonAdapterAnnotation(BiConsumer 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 outputConsumer) { + Gson gson = new Gson(); + TestExecutor.run(outputConsumer, "Read: Generic TypeToken", () -> gson.fromJson("{\"t\": 1}", new TypeToken>() {}).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>() {}).toString()); + } +} diff --git a/shrinker-test/src/main/java/com/example/TestExecutor.java b/shrinker-test/src/main/java/com/example/TestExecutor.java new file mode 100644 index 0000000000..6ea9b9b927 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/TestExecutor.java @@ -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 outputConsumer, String name, Supplier 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 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")); + } +} diff --git a/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java b/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java new file mode 100644 index 0000000000..ddf6f34f19 --- /dev/null +++ b/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java @@ -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 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) (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." + ); + } + }); + } +} From a8a928ee51452c3f8925fe07d64603abca8bf3d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 May 2023 15:42:06 -0700 Subject: [PATCH 20/95] Bump guava-testlib from 31.1-jre to 32.0.0-jre (#2400) Bumps [guava-testlib](https://github.com/google/guava) from 31.1-jre to 32.0.0-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava-testlib dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gson/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/pom.xml b/gson/pom.xml index 6e911c74de..f5c0b11feb 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -62,7 +62,7 @@ com.google.guava guava-testlib - 31.1-jre + 32.0.0-jre test From d3e17587fe8dbf1c6afd1acbad4878f19ff64c02 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 May 2023 15:42:32 -0700 Subject: [PATCH 21/95] Bump guava from 31.1-jre to 32.0.0-jre (#2399) Bumps [guava](https://github.com/google/guava) from 31.1-jre to 32.0.0-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- proto/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto/pom.xml b/proto/pom.xml index 313bb10613..ac04212ab7 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -50,7 +50,7 @@ com.google.guava guava - 31.1-jre + 32.0.0-jre From 481ac9b82cd5713f43018fafc16218d4045dc4c4 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Wed, 31 May 2023 01:32:22 +0200 Subject: [PATCH 22/95] Use non-`null` `FormattingStyle`; configure space after separator (#2345) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use non-`null` `FormattingStyle`; configure space after separator * Improve Javadoc and tests * Rename to plural separator*s* * Add explicit tests for default formatting styles --------- Co-authored-by: Éamonn McManus --- .../java/com/google/gson/FormattingStyle.java | 67 +++++-- gson/src/main/java/com/google/gson/Gson.java | 6 +- .../java/com/google/gson/GsonBuilder.java | 12 +- .../com/google/gson/stream/JsonWriter.java | 68 ++++--- .../test/java/com/google/gson/GsonTest.java | 4 +- .../gson/functional/FormattingStyleTest.java | 169 +++++++++++------- .../google/gson/stream/JsonWriterTest.java | 29 ++- 7 files changed, 240 insertions(+), 115 deletions(-) diff --git a/gson/src/main/java/com/google/gson/FormattingStyle.java b/gson/src/main/java/com/google/gson/FormattingStyle.java index ed9f86dd1f..ff031cd4bc 100644 --- a/gson/src/main/java/com/google/gson/FormattingStyle.java +++ b/gson/src/main/java/com/google/gson/FormattingStyle.java @@ -22,10 +22,15 @@ /** * A class used to control what the serialization output looks like. * - *

It currently defines the kind of newline to use, and the indent, but - * might add more in the future.

+ *

It currently has the following configuration methods, but more methods + * might be added in the future: + *

    + *
  • {@link #withNewline(String)} + *
  • {@link #withIndent(String)} + *
  • {@link #withSpaceAfterSeparators(boolean)} + *
* - * @see GsonBuilder#setPrettyPrinting(FormattingStyle) + * @see GsonBuilder#setFormattingStyle(FormattingStyle) * @see JsonWriter#setFormattingStyle(FormattingStyle) * @see Wikipedia Newline article * @@ -34,15 +39,30 @@ public class FormattingStyle { private final String newline; private final String indent; + private final boolean spaceAfterSeparators; /** - * The default pretty printing formatting style using {@code "\n"} as - * newline and two spaces as indent. + * The default compact formatting style: + *
    + *
  • no newline + *
  • no indent + *
  • no space after {@code ','} and {@code ':'} + *
*/ - public static final FormattingStyle DEFAULT = - new FormattingStyle("\n", " "); + public static final FormattingStyle COMPACT = new FormattingStyle("", "", false); - private FormattingStyle(String newline, String indent) { + /** + * The default pretty printing formatting style: + *
    + *
  • {@code "\n"} as newline + *
  • two spaces as indent + *
  • a space between {@code ':'} and the subsequent value + *
+ */ + public static final FormattingStyle PRETTY = + new FormattingStyle("\n", " ", true); + + private FormattingStyle(String newline, String indent, boolean spaceAfterSeparators) { Objects.requireNonNull(newline, "newline == null"); Objects.requireNonNull(indent, "indent == null"); if (!newline.matches("[\r\n]*")) { @@ -55,6 +75,7 @@ private FormattingStyle(String newline, String indent) { } this.newline = newline; this.indent = indent; + this.spaceAfterSeparators = spaceAfterSeparators; } /** @@ -70,7 +91,7 @@ private FormattingStyle(String newline, String indent) { * @return a newly created {@link FormattingStyle} */ public FormattingStyle withNewline(String newline) { - return new FormattingStyle(newline, this.indent); + return new FormattingStyle(newline, this.indent, this.spaceAfterSeparators); } /** @@ -82,11 +103,26 @@ public FormattingStyle withNewline(String newline) { * @return a newly created {@link FormattingStyle} */ public FormattingStyle withIndent(String indent) { - return new FormattingStyle(this.newline, indent); + return new FormattingStyle(this.newline, indent, this.spaceAfterSeparators); + } + + /** + * Creates a {@link FormattingStyle} which either uses a space after + * the separators {@code ','} and {@code ':'} in the JSON output, or not. + * + *

This setting has no effect on the {@linkplain #withNewline(String) configured newline}. + * If a non-empty newline is configured, it will always be added after + * {@code ','} and no space is added after the {@code ','} in that case.

+ * + * @param spaceAfterSeparators whether to output a space after {@code ','} and {@code ':'}. + * @return a newly created {@link FormattingStyle} + */ + public FormattingStyle withSpaceAfterSeparators(boolean spaceAfterSeparators) { + return new FormattingStyle(this.newline, this.indent, spaceAfterSeparators); } /** - * The string value that will be used as a newline. + * Returns the string value that will be used as a newline. * * @return the newline value. */ @@ -95,11 +131,18 @@ public String getNewline() { } /** - * The string value that will be used as indent. + * Returns the string value that will be used as indent. * * @return the indent value. */ public String getIndent() { return this.indent; } + + /** + * Returns whether a space will be used after {@code ','} and {@code ':'}. + */ + public boolean usesSpaceAfterSeparators() { + return this.spaceAfterSeparators; + } } diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index ec5f69f63e..ba7bf4b4ae 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -141,7 +141,7 @@ public final class Gson { static final boolean DEFAULT_JSON_NON_EXECUTABLE = false; static final boolean DEFAULT_LENIENT = false; - static final FormattingStyle DEFAULT_FORMATTING_STYLE = null; + static final FormattingStyle DEFAULT_FORMATTING_STYLE = FormattingStyle.COMPACT; static final boolean DEFAULT_ESCAPE_HTML = true; static final boolean DEFAULT_SERIALIZE_NULLS = false; static final boolean DEFAULT_COMPLEX_MAP_KEYS = false; @@ -205,7 +205,7 @@ public final class Gson { * means that all the unneeded white-space is removed. You can change this behavior with * {@link GsonBuilder#setPrettyPrinting()}. *
  • When the JSON generated contains more than one line, the kind of newline and indent to - * use can be configured with {@link GsonBuilder#setPrettyPrinting(FormattingStyle)}.
  • + * use can be configured with {@link GsonBuilder#setFormattingStyle(FormattingStyle)}. *
  • The generated JSON omits all the fields that are null. Note that nulls in arrays are * kept as is since an array is an ordered list. Moreover, if a field is not null, but its * generated JSON is empty, the field is kept. You can configure Gson to serialize null values @@ -894,7 +894,7 @@ public void toJson(JsonElement jsonElement, Appendable writer) throws JsonIOExce *
  • {@link GsonBuilder#serializeNulls()}
  • *
  • {@link GsonBuilder#setLenient()}
  • *
  • {@link GsonBuilder#setPrettyPrinting()}
  • - *
  • {@link GsonBuilder#setPrettyPrinting(FormattingStyle)}
  • + *
  • {@link GsonBuilder#setFormattingStyle(FormattingStyle)}
  • * */ public JsonWriter newJsonWriter(Writer writer) throws IOException { diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java index d1508cfa04..e3d4818233 100644 --- a/gson/src/main/java/com/google/gson/GsonBuilder.java +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java @@ -497,29 +497,27 @@ public GsonBuilder addDeserializationExclusionStrategy(ExclusionStrategy strateg * Configures Gson to output JSON that fits in a page for pretty printing. This option only * affects JSON serialization. * - *

    This is a convenience method which simply calls {@link #setPrettyPrinting(FormattingStyle)} - * with {@link FormattingStyle#DEFAULT}. + *

    This is a convenience method which simply calls {@link #setFormattingStyle(FormattingStyle)} + * with {@link FormattingStyle#PRETTY}. * * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern */ @CanIgnoreReturnValue public GsonBuilder setPrettyPrinting() { - return setPrettyPrinting(FormattingStyle.DEFAULT); + return setFormattingStyle(FormattingStyle.PRETTY); } /** * Configures Gson to output JSON that uses a certain kind of formatting style (for example newline and indent). * This option only affects JSON serialization. By default Gson produces compact JSON output without any formatting. * - *

    Has no effect if the serialized format is a single line.

    - * * @param formattingStyle the formatting style to use. * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern * @since $next-version$ */ @CanIgnoreReturnValue - public GsonBuilder setPrettyPrinting(FormattingStyle formattingStyle) { - this.formattingStyle = formattingStyle; + public GsonBuilder setFormattingStyle(FormattingStyle formattingStyle) { + this.formattingStyle = Objects.requireNonNull(formattingStyle); return this; } diff --git a/gson/src/main/java/com/google/gson/stream/JsonWriter.java b/gson/src/main/java/com/google/gson/stream/JsonWriter.java index 4a2424e687..2eb56ed58b 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonWriter.java +++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java @@ -25,6 +25,7 @@ import static com.google.gson.stream.JsonScope.NONEMPTY_OBJECT; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.gson.FormattingStyle; import java.io.Closeable; import java.io.Flushable; import java.io.IOException; @@ -37,8 +38,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.regex.Pattern; -import com.google.gson.FormattingStyle; - /** * Writes a JSON (RFC 7159) * encoded value to a stream, one token at a time. The stream includes both @@ -182,15 +181,12 @@ public class JsonWriter implements Closeable, Flushable { push(EMPTY_DOCUMENT); } - /** - * The settings used for pretty printing, or null for no pretty printing. - */ private FormattingStyle formattingStyle; - - /** - * The name/value separator; either ":" or ": ". - */ - private String separator = ":"; + // These fields cache data derived from the formatting style, to avoid having to + // re-evaluate it every time something is written + private String formattedColon; + private String formattedComma; + private boolean usesEmptyNewlineAndIndent; private boolean lenient; @@ -207,6 +203,7 @@ public class JsonWriter implements Closeable, Flushable { */ public JsonWriter(Writer out) { this.out = Objects.requireNonNull(out, "out == null"); + setFormattingStyle(FormattingStyle.COMPACT); } /** @@ -215,36 +212,49 @@ public JsonWriter(Writer out) { * will be compact. Otherwise the encoded document will be more * human-readable. * + *

    This is a convenience method which overwrites any previously + * {@linkplain #setFormattingStyle(FormattingStyle) set formatting style} with + * either {@link FormattingStyle#COMPACT} if the given indent string is + * empty, or {@link FormattingStyle#PRETTY} with the given indent if + * not empty. + * * @param indent a string containing only whitespace. */ public final void setIndent(String indent) { if (indent.isEmpty()) { - setFormattingStyle(null); + setFormattingStyle(FormattingStyle.COMPACT); } else { - setFormattingStyle(FormattingStyle.DEFAULT.withIndent(indent)); + setFormattingStyle(FormattingStyle.PRETTY.withIndent(indent)); } } /** - * Sets the pretty printing style to be used in the encoded document. - * No pretty printing is done if the given style is {@code null}. + * Sets the formatting style to be used in the encoded document. * - *

    Sets the various attributes to be used in the encoded document. - * For example the indentation string to be repeated for each level of indentation. - * Or the newline style, to accommodate various OS styles.

    + *

    The formatting style specifies for example the indentation string to be + * repeated for each level of indentation, or the newline style, to accommodate + * various OS styles.

    * - *

    Has no effect if the serialized format is a single line.

    - * - * @param formattingStyle the style used for pretty printing, no pretty printing if {@code null}. + * @param formattingStyle the formatting style to use, must not be {@code null}. * @since $next-version$ */ public final void setFormattingStyle(FormattingStyle formattingStyle) { - this.formattingStyle = formattingStyle; - if (formattingStyle == null) { - this.separator = ":"; + this.formattingStyle = Objects.requireNonNull(formattingStyle); + + this.formattedComma = ","; + if (this.formattingStyle.usesSpaceAfterSeparators()) { + this.formattedColon = ": "; + + // Only add space if no newline is written + if (this.formattingStyle.getNewline().isEmpty()) { + this.formattedComma = ", "; + } } else { - this.separator = ": "; + this.formattedColon = ":"; } + + this.usesEmptyNewlineAndIndent = this.formattingStyle.getNewline().isEmpty() + && this.formattingStyle.getIndent().isEmpty(); } /** @@ -419,7 +429,7 @@ private void replaceTop(int topOfStack) { /** * Encodes the property name. * - * @param name the name of the forthcoming value. May not be null. + * @param name the name of the forthcoming value. May not be {@code null}. * @return this writer. */ @CanIgnoreReturnValue @@ -693,7 +703,7 @@ private void string(String value) throws IOException { } private void newline() throws IOException { - if (formattingStyle == null) { + if (usesEmptyNewlineAndIndent) { return; } @@ -710,7 +720,7 @@ private void newline() throws IOException { private void beforeName() throws IOException { int context = peek(); if (context == NONEMPTY_OBJECT) { // first in object - out.write(','); + out.write(formattedComma); } else if (context != EMPTY_OBJECT) { // not in an object! throw new IllegalStateException("Nesting problem."); } @@ -742,12 +752,12 @@ private void beforeValue() throws IOException { break; case NONEMPTY_ARRAY: // another in array - out.append(','); + out.append(formattedComma); newline(); break; case DANGLING_NAME: // value for name - out.append(separator); + out.append(formattedColon); replaceTop(NONEMPTY_OBJECT); break; diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java index c1e9e9d785..fcdc7cbc54 100644 --- a/gson/src/test/java/com/google/gson/GsonTest.java +++ b/gson/src/test/java/com/google/gson/GsonTest.java @@ -63,7 +63,7 @@ public final class GsonTest { public void testOverridesDefaultExcluder() { Gson gson = new Gson(CUSTOM_EXCLUDER, CUSTOM_FIELD_NAMING_STRATEGY, new HashMap>(), true, false, true, false, - FormattingStyle.DEFAULT, true, false, true, + FormattingStyle.PRETTY, true, false, true, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT, DateFormat.DEFAULT, new ArrayList(), new ArrayList(), new ArrayList(), @@ -80,7 +80,7 @@ public void testOverridesDefaultExcluder() { public void testClonedTypeAdapterFactoryListsAreIndependent() { Gson original = new Gson(CUSTOM_EXCLUDER, CUSTOM_FIELD_NAMING_STRATEGY, new HashMap>(), true, false, true, false, - FormattingStyle.DEFAULT, true, false, true, + FormattingStyle.PRETTY, true, false, true, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT, DateFormat.DEFAULT, new ArrayList(), new ArrayList(), new ArrayList(), diff --git a/gson/src/test/java/com/google/gson/functional/FormattingStyleTest.java b/gson/src/test/java/com/google/gson/functional/FormattingStyleTest.java index 170e0ff29d..920f820847 100644 --- a/gson/src/test/java/com/google/gson/functional/FormattingStyleTest.java +++ b/gson/src/test/java/com/google/gson/functional/FormattingStyleTest.java @@ -21,6 +21,11 @@ import com.google.gson.FormattingStyle; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -33,94 +38,140 @@ @RunWith(JUnit4.class) public class FormattingStyleTest { - private static final String[] INPUT = {"v1", "v2"}; - private static final String EXPECTED = "[\"v1\",\"v2\"]"; - private static final String EXPECTED_OS = buildExpected(System.lineSeparator(), " "); - private static final String EXPECTED_CR = buildExpected("\r", " "); - private static final String EXPECTED_LF = buildExpected("\n", " "); - private static final String EXPECTED_CRLF = buildExpected("\r\n", " "); + // Create new input object every time to protect against tests accidentally modifying input + private static Map> createInput() { + Map> map = new LinkedHashMap<>(); + map.put("a", Arrays.asList(1, 2)); + return map; + } + + private static String buildExpected(String newline, String indent, boolean spaceAfterSeparators) { + String expected = "{\"a\":[1,2]}"; + String commaSpace = spaceAfterSeparators && newline.isEmpty() ? " " : ""; + return expected.replace("", newline).replace("", indent) + .replace("", spaceAfterSeparators ? " " : "") + .replace("", commaSpace); + } // Various valid strings that can be used for newline and indent private static final String[] TEST_NEWLINES = { "", "\r", "\n", "\r\n", "\n\r\r\n", System.lineSeparator() }; private static final String[] TEST_INDENTS = { - "", " ", " ", " ", "\t", " \t \t" + "", " ", " ", "\t", " \t \t" }; @Test public void testDefault() { Gson gson = new GsonBuilder().setPrettyPrinting().create(); - String json = gson.toJson(INPUT); - // Make sure the default uses LF, like before. - assertThat(json).isEqualTo(EXPECTED_LF); + String json = gson.toJson(createInput()); + assertThat(json).isEqualTo(buildExpected("\n", " ", true)); } @Test - public void testNewlineCrLf() { - FormattingStyle style = FormattingStyle.DEFAULT.withNewline("\r\n"); - Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); - String json = gson.toJson(INPUT); - assertThat(json).isEqualTo(EXPECTED_CRLF); + public void testVariousCombinationsParse() { + // Mixing various indent and newline styles in the same string, to be parsed. + String jsonStringMix = "{\r\t'a':\r\n[ 1,2\t]\n}"; + TypeToken>> inputType = new TypeToken>>() {}; + + Map> actualParsed; + // Test all that all combinations of newline can be parsed and generate the same INPUT. + for (String indent : TEST_INDENTS) { + for (String newline : TEST_NEWLINES) { + FormattingStyle style = FormattingStyle.PRETTY.withNewline(newline).withIndent(indent); + Gson gson = new GsonBuilder().setFormattingStyle(style).create(); + + String toParse = buildExpected(newline, indent, true); + actualParsed = gson.fromJson(toParse, inputType); + assertThat(actualParsed).isEqualTo(createInput()); + + // Parse the mixed string with the gson parsers configured with various newline / indents. + actualParsed = gson.fromJson(jsonStringMix, inputType); + assertThat(actualParsed).isEqualTo(createInput()); + } + } + } + + private static String toJson(Object obj, FormattingStyle style) { + return new GsonBuilder().setFormattingStyle(style).create().toJson(obj); } @Test - public void testNewlineLf() { - FormattingStyle style = FormattingStyle.DEFAULT.withNewline("\n"); - Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); - String json = gson.toJson(INPUT); - assertThat(json).isEqualTo(EXPECTED_LF); + public void testFormatCompact() { + String json = toJson(createInput(), FormattingStyle.COMPACT); + String expectedJson = buildExpected("", "", false); + assertThat(json).isEqualTo(expectedJson); + // Sanity check to verify that `buildExpected` works correctly + assertThat(json).isEqualTo("{\"a\":[1,2]}"); } @Test - public void testNewlineCr() { - FormattingStyle style = FormattingStyle.DEFAULT.withNewline("\r"); - Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); - String json = gson.toJson(INPUT); - assertThat(json).isEqualTo(EXPECTED_CR); + public void testFormatPretty() { + String json = toJson(createInput(), FormattingStyle.PRETTY); + String expectedJson = buildExpected("\n", " ", true); + assertThat(json).isEqualTo(expectedJson); + // Sanity check to verify that `buildExpected` works correctly + assertThat(json).isEqualTo( + "{\n" + + " \"a\": [\n" + + " 1,\n" + + " 2\n" + + " ]\n" + + "}"); } @Test - public void testNewlineOs() { - FormattingStyle style = FormattingStyle.DEFAULT.withNewline(System.lineSeparator()); - Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); - String json = gson.toJson(INPUT); - assertThat(json).isEqualTo(EXPECTED_OS); + public void testFormatPrettySingleLine() { + FormattingStyle style = FormattingStyle.COMPACT.withSpaceAfterSeparators(true); + String json = toJson(createInput(), style); + String expectedJson = buildExpected("", "", true); + assertThat(json).isEqualTo(expectedJson); + // Sanity check to verify that `buildExpected` works correctly + assertThat(json).isEqualTo("{\"a\": [1, 2]}"); } @Test - public void testVariousCombinationsToString() { - for (String indent : TEST_INDENTS) { - for (String newline : TEST_NEWLINES) { - FormattingStyle style = FormattingStyle.DEFAULT.withNewline(newline).withIndent(indent); - Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); - String json = gson.toJson(INPUT); - assertThat(json).isEqualTo(buildExpected(newline, indent)); + public void testFormat() { + for (String newline : TEST_NEWLINES) { + for (String indent : TEST_INDENTS) { + for (boolean spaceAfterSeparators : new boolean[] {true, false}) { + FormattingStyle style = FormattingStyle.COMPACT.withNewline(newline) + .withIndent(indent).withSpaceAfterSeparators(spaceAfterSeparators); + + String json = toJson(createInput(), style); + String expectedJson = buildExpected(newline, indent, spaceAfterSeparators); + assertThat(json).isEqualTo(expectedJson); + } } } } + /** + * Should be able to convert {@link FormattingStyle#COMPACT} to {@link FormattingStyle#PRETTY} + * using the {@code withX} methods. + */ @Test - public void testVariousCombinationsParse() { - // Mixing various indent and newline styles in the same string, to be parsed. - String jsonStringMix = "[\r\t'v1',\r\n 'v2'\n]"; + public void testCompactToPretty() { + FormattingStyle style = FormattingStyle.COMPACT.withNewline("\n").withIndent(" ") + .withSpaceAfterSeparators(true); - String[] actualParsed; - // Test all that all combinations of newline can be parsed and generate the same INPUT. - for (String indent : TEST_INDENTS) { - for (String newline : TEST_NEWLINES) { - FormattingStyle style = FormattingStyle.DEFAULT.withNewline(newline).withIndent(indent); - Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); + String json = toJson(createInput(), style); + String expectedJson = toJson(createInput(), FormattingStyle.PRETTY); + assertThat(json).isEqualTo(expectedJson); + } - String toParse = buildExpected(newline, indent); - actualParsed = gson.fromJson(toParse, INPUT.getClass()); - assertThat(actualParsed).isEqualTo(INPUT); + /** + * Should be able to convert {@link FormattingStyle#PRETTY} to {@link FormattingStyle#COMPACT} + * using the {@code withX} methods. + */ + @Test + public void testPrettyToCompact() { + FormattingStyle style = FormattingStyle.PRETTY.withNewline("").withIndent("") + .withSpaceAfterSeparators(false); - // Parse the mixed string with the gson parsers configured with various newline / indents. - actualParsed = gson.fromJson(jsonStringMix, INPUT.getClass()); - assertThat(actualParsed).isEqualTo(INPUT); - } - } + String json = toJson(createInput(), style); + String expectedJson = toJson(createInput(), FormattingStyle.COMPACT); + assertThat(json).isEqualTo(expectedJson); } @Test @@ -128,7 +179,7 @@ public void testStyleValidations() { try { // TBD if we want to accept \u2028 and \u2029. For now we don't because JSON specification // does not consider them to be newlines - FormattingStyle.DEFAULT.withNewline("\u2028"); + FormattingStyle.PRETTY.withNewline("\u2028"); fail("Gson should not accept anything but \\r and \\n for newline"); } catch (IllegalArgumentException expected) { assertThat(expected).hasMessageThat() @@ -136,7 +187,7 @@ public void testStyleValidations() { } try { - FormattingStyle.DEFAULT.withNewline("NL"); + FormattingStyle.PRETTY.withNewline("NL"); fail("Gson should not accept anything but \\r and \\n for newline"); } catch (IllegalArgumentException expected) { assertThat(expected).hasMessageThat() @@ -144,15 +195,11 @@ public void testStyleValidations() { } try { - FormattingStyle.DEFAULT.withIndent("\f"); + FormattingStyle.PRETTY.withIndent("\f"); fail("Gson should not accept anything but space and tab for indent"); } catch (IllegalArgumentException expected) { assertThat(expected).hasMessageThat() .isEqualTo("Only combinations of spaces and tabs are allowed in indent."); } } - - private static String buildExpected(String newline, String indent) { - return EXPECTED.replace("", newline).replace("", indent); - } } diff --git a/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java b/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java index 2ee120f38e..2b9fb133ac 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java @@ -852,7 +852,9 @@ public void testSetGetFormattingStyle() throws IOException { StringWriter stringWriter = new StringWriter(); JsonWriter jsonWriter = new JsonWriter(stringWriter); - jsonWriter.setFormattingStyle(FormattingStyle.DEFAULT.withIndent(" \t ").withNewline(lineSeparator)); + // Default should be FormattingStyle.COMPACT + assertThat(jsonWriter.getFormattingStyle()).isSameInstanceAs(FormattingStyle.COMPACT); + jsonWriter.setFormattingStyle(FormattingStyle.PRETTY.withIndent(" \t ").withNewline(lineSeparator)); jsonWriter.beginArray(); jsonWriter.value(true); @@ -871,4 +873,29 @@ public void testSetGetFormattingStyle() throws IOException { assertThat(jsonWriter.getFormattingStyle().getNewline()).isEqualTo(lineSeparator); } + + @Test + public void testIndentOverwritesFormattingStyle() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.setFormattingStyle(FormattingStyle.COMPACT); + // Should overwrite formatting style + jsonWriter.setIndent(" "); + + jsonWriter.beginObject(); + jsonWriter.name("a"); + jsonWriter.beginArray(); + jsonWriter.value(1); + jsonWriter.value(2); + jsonWriter.endArray(); + jsonWriter.endObject(); + + String expected = "{\n" + + " \"a\": [\n" + + " 1,\n" + + " 2\n" + + " ]\n" + + "}"; + assertThat(stringWriter.toString()).isEqualTo(expected); + } } From 6292eafcdfb2e3985e90a004a64195bc74edb92d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 09:45:08 -0700 Subject: [PATCH 23/95] Bump truth from 1.1.3 to 1.1.4 (#2404) Bumps [truth](https://github.com/google/truth) from 1.1.3 to 1.1.4. - [Release notes](https://github.com/google/truth/releases) - [Commits](https://github.com/google/truth/commits/v1.1.4) --- updated-dependencies: - dependency-name: com.google.truth:truth dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 55b17e8068..b14289c7bd 100644 --- a/pom.xml +++ b/pom.xml @@ -89,7 +89,7 @@ com.google.truth truth - 1.1.3 + 1.1.4 From 2d7cc2e9f4e12163d4405a601fa8e814a66c9725 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 09:45:36 -0700 Subject: [PATCH 24/95] Bump jackson-databind from 2.15.1 to 2.15.2 (#2403) Bumps [jackson-databind](https://github.com/FasterXML/jackson) from 2.15.1 to 2.15.2. - [Commits](https://github.com/FasterXML/jackson/commits) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- metrics/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metrics/pom.xml b/metrics/pom.xml index 71dbccc375..82f2c8ea24 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -48,7 +48,7 @@ com.fasterxml.jackson.core jackson-databind - 2.15.1 + 2.15.2 com.google.caliper From cf50a5aaf152a34b3fb8bfd34ed12a07f1b8ed2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 12:55:48 -0700 Subject: [PATCH 25/95] Bump maven-release-plugin from 3.0.0 to 3.0.1 (#2409) Bumps [maven-release-plugin](https://github.com/apache/maven-release) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/apache/maven-release/releases) - [Commits](https://github.com/apache/maven-release/compare/maven-release-3.0.0...maven-release-3.0.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-release-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b14289c7bd..768cee6a86 100644 --- a/pom.xml +++ b/pom.xml @@ -201,7 +201,7 @@ org.apache.maven.plugins maven-release-plugin - 3.0.0 + 3.0.1 true From 144626ddd9e17fa18fcd2b3904ac8227cbbed8f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 08:16:59 -0700 Subject: [PATCH 26/95] Bump maven-failsafe-plugin from 3.1.0 to 3.1.2 (#2412) Bumps [maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.1.0 to 3.1.2. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.1.0...surefire-3.1.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- shrinker-test/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shrinker-test/pom.xml b/shrinker-test/pom.xml index bd6f789ac6..64418cda32 100644 --- a/shrinker-test/pom.xml +++ b/shrinker-test/pom.xml @@ -203,7 +203,7 @@ org.apache.maven.plugins maven-failsafe-plugin - 3.1.0 + 3.1.2 From e7f85dff8a17928fd3f0451e59f76ee72764430c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 08:17:46 -0700 Subject: [PATCH 27/95] Bump maven-surefire-plugin from 3.1.0 to 3.1.2 (#2411) Bumps [maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.1.0 to 3.1.2. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.1.0...surefire-3.1.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gson/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/pom.xml b/gson/pom.xml index f5c0b11feb..da8c049499 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -135,7 +135,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.1.0 + 3.1.2 + https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#r8-full-mode --> com.android.tools.r8.R8 --release diff --git a/shrinker-test/r8.pro b/shrinker-test/r8.pro index a415aa1614..690fe339b5 100644 --- a/shrinker-test/r8.pro +++ b/shrinker-test/r8.pro @@ -2,7 +2,7 @@ -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 +### See https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#r8-full-mode # Keep the no-args constructor of deserialized classes -keepclassmembers class com.example.ClassWithDefaultConstructor { diff --git a/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java b/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java index ddf6f34f19..9c32f1aa57 100644 --- a/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java +++ b/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java @@ -194,7 +194,7 @@ public void testDefaultConstructor() throws Exception { 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" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class" ); } }); From 44217b9100461ebf61f643279b486764899c77d9 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Mon, 12 Jun 2023 17:46:38 +0200 Subject: [PATCH 29/95] Rename branch references in GitHub workflow to 'main' (#2418) * Rename branch references in GitHub workflow to 'main' * Revert accidental branch reference renaming --- .github/workflows/codeql-analysis.yml | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 01d95bdf6f..76822e60f4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -4,9 +4,9 @@ name: "CodeQL" on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] schedule: # Run every Monday at 16:10 - cron: '10 16 * * 1' diff --git a/README.md b/README.md index e9588f5dfc..04eb1c94e8 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ JDK 11 or newer is required for building, JDK 17 is recommended. ### Contributing -See the [contributing guide](https://github.com/google/.github/blob/main/CONTRIBUTING.md). +See the [contributing guide](https://github.com/google/.github/blob/master/CONTRIBUTING.md). Please perform a quick search to check if there are already existing issues or pull requests related to your contribution. Keep in mind that Gson is in maintenance mode. If you want to add a new feature, please first search for existing GitHub issues, or create a new one to discuss the feature and get feedback. From 621a6a2d9a45a71887346d3001cccdf70340b9ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Jun 2023 14:03:15 -0700 Subject: [PATCH 30/95] Bump guava-testlib from 32.0.0-jre to 32.0.1-jre (#2416) Bumps [guava-testlib](https://github.com/google/guava) from 32.0.0-jre to 32.0.1-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava-testlib dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gson/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/pom.xml b/gson/pom.xml index da8c049499..9e995a2fde 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -62,7 +62,7 @@ com.google.guava guava-testlib - 32.0.0-jre + 32.0.1-jre test From 8c523b8d2c2efe22a670fc63e5369692342f0632 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Jun 2023 14:03:38 -0700 Subject: [PATCH 31/95] Bump guava from 32.0.0-jre to 32.0.1-jre (#2415) Bumps [guava](https://github.com/google/guava) from 32.0.0-jre to 32.0.1-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- proto/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto/pom.xml b/proto/pom.xml index ac04212ab7..fbf87b17b0 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -50,7 +50,7 @@ com.google.guava guava - 32.0.0-jre + 32.0.1-jre From fd8c8ac0821f13b3b7a76bab13b602812c9c614a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:40:32 -0700 Subject: [PATCH 32/95] Bump truth from 1.1.4 to 1.1.5 (#2424) Bumps [truth](https://github.com/google/truth) from 1.1.4 to 1.1.5. - [Release notes](https://github.com/google/truth/releases) - [Commits](https://github.com/google/truth/compare/v1.1.4...v1.1.5) --- updated-dependencies: - dependency-name: com.google.truth:truth dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 768cee6a86..dab4affb89 100644 --- a/pom.xml +++ b/pom.xml @@ -89,7 +89,7 @@ com.google.truth truth - 1.1.4 + 1.1.5 From 5fa61b25b40571872d9e331c1bd1a9ef8c816e7b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:44:18 -0700 Subject: [PATCH 33/95] Bump maven-shade-plugin from 3.4.1 to 3.5.0 (#2423) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [maven-shade-plugin](https://github.com/apache/maven-shade-plugin) from 3.4.1 to 3.5.0. - [Release notes](https://github.com/apache/maven-shade-plugin/releases) - [Commits](https://github.com/apache/maven-shade-plugin/compare/maven-shade-plugin-3.4.1...maven-shade-plugin-3.5.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-shade-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Éamonn McManus --- shrinker-test/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shrinker-test/pom.xml b/shrinker-test/pom.xml index dc4b01c91b..7c6a4bacc0 100644 --- a/shrinker-test/pom.xml +++ b/shrinker-test/pom.xml @@ -122,7 +122,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.4.1 + 3.5.0 package From 500ec501a753b33dc0ed78b1cf4cbc95473f4e41 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 09:16:18 -0700 Subject: [PATCH 34/95] Bump error_prone_annotations from 2.19.1 to 2.20.0 (#2421) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [error_prone_annotations](https://github.com/google/error-prone) from 2.19.1 to 2.20.0. - [Release notes](https://github.com/google/error-prone/releases) - [Commits](https://github.com/google/error-prone/compare/v2.19.1...v2.20.0) --- updated-dependencies: - dependency-name: com.google.errorprone:error_prone_annotations dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Éamonn McManus --- gson/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/pom.xml b/gson/pom.xml index 9e995a2fde..4cb7757d85 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -46,7 +46,7 @@ com.google.errorprone error_prone_annotations - 2.19.1 + 2.20.0 From 9cf0f8d3022fdfcd480dcd7648b2e5209e775b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 19 Jun 2023 14:22:28 -0700 Subject: [PATCH 35/95] Update to latest Error Prone and fix some newly-flagged problems. (#2426) --- .../gson/internal/NonNullElementWrapperList.java | 1 + .../gson/functional/DefaultTypeAdaptersTest.java | 1 + .../google/gson/functional/ExposeFieldsTest.java | 13 ++++++++----- pom.xml | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/gson/src/main/java/com/google/gson/internal/NonNullElementWrapperList.java b/gson/src/main/java/com/google/gson/internal/NonNullElementWrapperList.java index d7429503bf..57f2d96ed2 100644 --- a/gson/src/main/java/com/google/gson/internal/NonNullElementWrapperList.java +++ b/gson/src/main/java/com/google/gson/internal/NonNullElementWrapperList.java @@ -33,6 +33,7 @@ public class NonNullElementWrapperList extends AbstractList implements Ran // Explicitly specify ArrayList as type to guarantee that delegate implements RandomAccess private final ArrayList delegate; + @SuppressWarnings("NonApiType") public NonNullElementWrapperList(ArrayList delegate) { this.delegate = Objects.requireNonNull(delegate); } diff --git a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java index 83ca15c1d8..e70c175b3c 100644 --- a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java +++ b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java @@ -691,6 +691,7 @@ public void testTreeSetDeserialization() { assertThat(treeSet).contains("Value1"); } + @SuppressWarnings("UnnecessaryStringBuilder") // TODO: b/287969247 - remove when EP bug fixed @Test public void testStringBuilderSerialization() { StringBuilder sb = new StringBuilder("abc"); diff --git a/gson/src/test/java/com/google/gson/functional/ExposeFieldsTest.java b/gson/src/test/java/com/google/gson/functional/ExposeFieldsTest.java index 3b1119b6ff..81f4704ec3 100644 --- a/gson/src/test/java/com/google/gson/functional/ExposeFieldsTest.java +++ b/gson/src/test/java/com/google/gson/functional/ExposeFieldsTest.java @@ -60,11 +60,14 @@ public void testArrayWithOneNullExposeFieldObjectSerialization() { ClassWithExposedFields[] objects = { object1, object2, object3 }; String json = gson.toJson(objects); - String expected = new StringBuilder() - .append('[').append(object1.getExpectedJson()).append(',') - .append(object2.getExpectedJson()).append(',') - .append(object3.getExpectedJson()).append(']') - .toString(); + String expected = + '[' + + object1.getExpectedJson() + + ',' + + object2.getExpectedJson() + + ',' + + object3.getExpectedJson() + + ']'; assertThat(json).isEqualTo(expected); } diff --git a/pom.xml b/pom.xml index dab4affb89..34df6c4cc2 100644 --- a/pom.xml +++ b/pom.xml @@ -150,7 +150,7 @@ com.google.errorprone error_prone_core - 2.19.1 + 2.20.0 From a589ef20087b4b0f1ec3048d3ceaef1eedccd09d Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 24 Jun 2023 00:24:23 +0200 Subject: [PATCH 36/95] Improve creation of `ParameterizedType` (#2375) - Reject non-generic raw types for TypeToken.getParameterized - Fix ParameterizedTypeImpl erroneously requiring owner type for types without owner --- .../com/google/gson/internal/$Gson$Types.java | 33 ++-- .../com/google/gson/reflect/TypeToken.java | 22 ++- .../google/gson/internal/GsonTypesTest.java | 32 +++- .../google/gson/reflect/TypeTokenTest.java | 164 ++++++++---------- 4 files changed, 132 insertions(+), 119 deletions(-) diff --git a/gson/src/main/java/com/google/gson/internal/$Gson$Types.java b/gson/src/main/java/com/google/gson/internal/$Gson$Types.java index 4a925aa49b..470e67dfd9 100644 --- a/gson/src/main/java/com/google/gson/internal/$Gson$Types.java +++ b/gson/src/main/java/com/google/gson/internal/$Gson$Types.java @@ -33,8 +33,8 @@ import java.util.HashMap; import java.util.Map; import java.util.NoSuchElementException; -import java.util.Properties; import java.util.Objects; +import java.util.Properties; /** * Static methods for working with types. @@ -138,9 +138,8 @@ public static Class getRawType(Type type) { } else if (type instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) type; - // I'm not exactly sure why getRawType() returns Type instead of Class. - // Neal isn't either but suspects some pathological case related - // to nested classes exists. + // getRawType() returns Type instead of Class; that seems to be an API mistake, + // see https://bugs.openjdk.org/browse/JDK-8250659 Type rawType = parameterizedType.getRawType(); checkArgument(rawType instanceof Class); return (Class) rawType; @@ -481,19 +480,33 @@ static void checkNotPrimitive(Type type) { checkArgument(!(type instanceof Class) || !((Class) type).isPrimitive()); } + /** + * Whether an {@linkplain ParameterizedType#getOwnerType() owner type} must be specified when + * constructing a {@link ParameterizedType} for {@code rawType}. + * + *

    Note that this method might not require an owner type for all cases where Java reflection + * would create parameterized types with owner type. + */ + public static boolean requiresOwnerType(Type rawType) { + if (rawType instanceof Class) { + Class rawTypeAsClass = (Class) rawType; + return !Modifier.isStatic(rawTypeAsClass.getModifiers()) + && rawTypeAsClass.getDeclaringClass() != null; + } + return false; + } + private static final class ParameterizedTypeImpl implements ParameterizedType, Serializable { private final Type ownerType; private final Type rawType; private final Type[] typeArguments; public ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) { + // TODO: Should this enforce that rawType is a Class? See JDK implementation of + // the ParameterizedType interface and https://bugs.openjdk.org/browse/JDK-8250659 requireNonNull(rawType); - // require an owner type if the raw type needs it - if (rawType instanceof Class) { - Class rawTypeAsClass = (Class) rawType; - boolean isStaticOrTopLevelClass = Modifier.isStatic(rawTypeAsClass.getModifiers()) - || rawTypeAsClass.getEnclosingClass() == null; - checkArgument(ownerType != null || isStaticOrTopLevelClass); + if (ownerType == null && requiresOwnerType(rawType)) { + throw new IllegalArgumentException("Must specify owner type for " + rawType); } this.ownerType = ownerType == null ? null : canonicalize(ownerType); diff --git a/gson/src/main/java/com/google/gson/reflect/TypeToken.java b/gson/src/main/java/com/google/gson/reflect/TypeToken.java index f4a9c0d943..4a695666c8 100644 --- a/gson/src/main/java/com/google/gson/reflect/TypeToken.java +++ b/gson/src/main/java/com/google/gson/reflect/TypeToken.java @@ -338,8 +338,8 @@ public static TypeToken get(Class type) { * and care must be taken to pass in the correct number of type arguments. * * @throws IllegalArgumentException - * If {@code rawType} is not of type {@code Class}, or if the type arguments are invalid for - * the raw type + * If {@code rawType} is not of type {@code Class}, if it is not a generic type, or if the + * type arguments are invalid for the raw type */ public static TypeToken getParameterized(Type rawType, Type... typeArguments) { Objects.requireNonNull(rawType); @@ -354,6 +354,18 @@ public static TypeToken getParameterized(Type rawType, Type... typeArguments) Class rawClass = (Class) rawType; TypeVariable[] typeVariables = rawClass.getTypeParameters(); + // Note: Does not check if owner type of rawType is generic because this factory method + // does not support specifying owner type + if (typeVariables.length == 0) { + throw new IllegalArgumentException(rawClass.getName() + " is not a generic type"); + } + + // Check for this here to avoid misleading exception thrown by ParameterizedTypeImpl + if ($Gson$Types.requiresOwnerType(rawType)) { + throw new IllegalArgumentException("Raw type " + rawClass.getName() + " is not supported because" + + " it requires specifying an owner type"); + } + int expectedArgsCount = typeVariables.length; int actualArgsCount = typeArguments.length; if (actualArgsCount != expectedArgsCount) { @@ -362,7 +374,7 @@ public static TypeToken getParameterized(Type rawType, Type... typeArguments) } for (int i = 0; i < expectedArgsCount; i++) { - Type typeArgument = typeArguments[i]; + Type typeArgument = Objects.requireNonNull(typeArguments[i], "Type argument must not be null"); Class rawTypeArgument = $Gson$Types.getRawType(typeArgument); TypeVariable typeVariable = typeVariables[i]; @@ -370,8 +382,8 @@ public static TypeToken getParameterized(Type rawType, Type... typeArguments) Class rawBound = $Gson$Types.getRawType(bound); if (!rawBound.isAssignableFrom(rawTypeArgument)) { - throw new IllegalArgumentException("Type argument " + typeArgument + " does not satisfy bounds " - + "for type variable " + typeVariable + " declared by " + rawType); + throw new IllegalArgumentException("Type argument " + typeArgument + " does not satisfy bounds" + + " for type variable " + typeVariable + " declared by " + rawType); } } } diff --git a/gson/src/test/java/com/google/gson/internal/GsonTypesTest.java b/gson/src/test/java/com/google/gson/internal/GsonTypesTest.java index af824975c5..0c5adde541 100644 --- a/gson/src/test/java/com/google/gson/internal/GsonTypesTest.java +++ b/gson/src/test/java/com/google/gson/internal/GsonTypesTest.java @@ -17,7 +17,7 @@ package com.google.gson.internal; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; @@ -29,20 +29,33 @@ public final class GsonTypesTest { @Test public void testNewParameterizedTypeWithoutOwner() throws Exception { // List. List is a top-level class - Type type = $Gson$Types.newParameterizedTypeWithOwner(null, List.class, A.class); - assertThat(getFirstTypeArgument(type)).isEqualTo(A.class); + ParameterizedType type = $Gson$Types.newParameterizedTypeWithOwner(null, List.class, A.class); + assertThat(type.getOwnerType()).isNull(); + assertThat(type.getRawType()).isEqualTo(List.class); + assertThat(type.getActualTypeArguments()).asList().containsExactly(A.class); // A. A is a static inner class. type = $Gson$Types.newParameterizedTypeWithOwner(null, A.class, B.class); assertThat(getFirstTypeArgument(type)).isEqualTo(B.class); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + // NonStaticInner is not allowed without owner + () -> $Gson$Types.newParameterizedTypeWithOwner(null, NonStaticInner.class, A.class)); + assertThat(e).hasMessageThat().isEqualTo("Must specify owner type for " + NonStaticInner.class); + + type = $Gson$Types.newParameterizedTypeWithOwner(GsonTypesTest.class, NonStaticInner.class, A.class); + assertThat(type.getOwnerType()).isEqualTo(GsonTypesTest.class); + assertThat(type.getRawType()).isEqualTo(NonStaticInner.class); + assertThat(type.getActualTypeArguments()).asList().containsExactly(A.class); + final class D { } - try { - // D is not allowed since D is not a static inner class - $Gson$Types.newParameterizedTypeWithOwner(null, D.class, A.class); - fail(); - } catch (IllegalArgumentException expected) {} + + // D is allowed since D has no owner type + type = $Gson$Types.newParameterizedTypeWithOwner(null, D.class, A.class); + assertThat(type.getOwnerType()).isNull(); + assertThat(type.getRawType()).isEqualTo(D.class); + assertThat(type.getActualTypeArguments()).asList().containsExactly(A.class); // A is allowed. type = $Gson$Types.newParameterizedTypeWithOwner(null, A.class, D.class); @@ -63,6 +76,9 @@ private static final class B { } private static final class C { } + @SuppressWarnings({"ClassCanBeStatic", "UnusedTypeParameter"}) + private final class NonStaticInner { + } /** * Given a parameterized type A<B,C>, returns B. If the specified type is not diff --git a/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java b/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java index f727a87a6e..d38f05b1ec 100644 --- a/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java +++ b/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java @@ -17,9 +17,10 @@ package com.google.gson.reflect; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; @@ -103,11 +104,13 @@ public void testArrayFactory() { Type listOfString = new TypeToken>() {}.getType(); assertThat(TypeToken.getArray(listOfString)).isEqualTo(expectedListOfStringArray); - try { - TypeToken.getArray(null); - fail(); - } catch (NullPointerException e) { - } + TypeToken expectedIntArray = new TypeToken() {}; + assertThat(TypeToken.getArray(int.class)).isEqualTo(expectedIntArray); + + assertThrows(NullPointerException.class, () -> TypeToken.getArray(null)); + } + + static class NestedGeneric { } @Test @@ -131,84 +134,70 @@ public void testParameterizedFactory() { TypeToken expectedSatisfyingTwoBounds = new TypeToken>() {}; assertThat(TypeToken.getParameterized(GenericWithMultiBound.class, ClassSatisfyingBounds.class)).isEqualTo(expectedSatisfyingTwoBounds); + + TypeToken nestedTypeToken = TypeToken.getParameterized(NestedGeneric.class, Integer.class); + ParameterizedType nestedParameterizedType = (ParameterizedType) nestedTypeToken.getType(); + // TODO: This seems to differ from how Java reflection behaves; when using TypeToken>, + // then NestedGeneric does have an owner type + assertThat(nestedParameterizedType.getOwnerType()).isNull(); + assertThat(nestedParameterizedType.getRawType()).isEqualTo(NestedGeneric.class); + assertThat(nestedParameterizedType.getActualTypeArguments()).asList().containsExactly(Integer.class); + + class LocalGenericClass {} + TypeToken expectedLocalType = new TypeToken>() {}; + assertThat(TypeToken.getParameterized(LocalGenericClass.class, Integer.class)).isEqualTo(expectedLocalType); } @Test public void testParameterizedFactory_Invalid() { - try { - TypeToken.getParameterized(null, new Type[0]); - fail(); - } catch (NullPointerException e) { - } + assertThrows(NullPointerException.class, () -> TypeToken.getParameterized(null, new Type[0])); + assertThrows(NullPointerException.class, () -> TypeToken.getParameterized(List.class, new Type[] { null })); GenericArrayType arrayType = (GenericArrayType) TypeToken.getArray(String.class).getType(); - try { - TypeToken.getParameterized(arrayType, new Type[0]); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessageThat().isEqualTo("rawType must be of type Class, but was java.lang.String[]"); - } + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> TypeToken.getParameterized(arrayType, new Type[0])); + assertThat(e).hasMessageThat().isEqualTo("rawType must be of type Class, but was java.lang.String[]"); - try { - TypeToken.getParameterized(String.class, String.class); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessageThat().isEqualTo("java.lang.String requires 0 type arguments, but got 1"); - } + e = assertThrows(IllegalArgumentException.class, () -> TypeToken.getParameterized(String.class, Number.class)); + assertThat(e).hasMessageThat().isEqualTo("java.lang.String is not a generic type"); - try { - TypeToken.getParameterized(List.class, new Type[0]); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessageThat().isEqualTo("java.util.List requires 1 type arguments, but got 0"); - } + e = assertThrows(IllegalArgumentException.class, () -> TypeToken.getParameterized(List.class, new Type[0])); + assertThat(e).hasMessageThat().isEqualTo("java.util.List requires 1 type arguments, but got 0"); - try { - TypeToken.getParameterized(List.class, String.class, String.class); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessageThat().isEqualTo("java.util.List requires 1 type arguments, but got 2"); - } + e = assertThrows(IllegalArgumentException.class, () -> TypeToken.getParameterized(List.class, String.class, String.class)); + assertThat(e).hasMessageThat().isEqualTo("java.util.List requires 1 type arguments, but got 2"); - try { - TypeToken.getParameterized(GenericWithBound.class, String.class); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessageThat().isEqualTo("Type argument class java.lang.String does not satisfy bounds " - + "for type variable T declared by " + GenericWithBound.class); - } + // Primitive types must not be used as type argument + e = assertThrows(IllegalArgumentException.class, () -> TypeToken.getParameterized(List.class, int.class)); + assertThat(e).hasMessageThat().isEqualTo("Type argument int does not satisfy bounds" + + " for type variable E declared by " + List.class); - try { - TypeToken.getParameterized(GenericWithBound.class, Object.class); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessageThat().isEqualTo("Type argument class java.lang.Object does not satisfy bounds " - + "for type variable T declared by " + GenericWithBound.class); - } + e = assertThrows(IllegalArgumentException.class, () -> TypeToken.getParameterized(GenericWithBound.class, String.class)); + assertThat(e).hasMessageThat().isEqualTo("Type argument class java.lang.String does not satisfy bounds" + + " for type variable T declared by " + GenericWithBound.class); - try { - TypeToken.getParameterized(GenericWithMultiBound.class, Number.class); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessageThat().isEqualTo("Type argument class java.lang.Number does not satisfy bounds " - + "for type variable T declared by " + GenericWithMultiBound.class); - } + e = assertThrows(IllegalArgumentException.class, () -> TypeToken.getParameterized(GenericWithBound.class, Object.class)); + assertThat(e).hasMessageThat().isEqualTo("Type argument class java.lang.Object does not satisfy bounds" + + " for type variable T declared by " + GenericWithBound.class); - try { - TypeToken.getParameterized(GenericWithMultiBound.class, CharSequence.class); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessageThat().isEqualTo("Type argument interface java.lang.CharSequence does not satisfy bounds " - + "for type variable T declared by " + GenericWithMultiBound.class); - } + e = assertThrows(IllegalArgumentException.class, () -> TypeToken.getParameterized(GenericWithMultiBound.class, Number.class)); + assertThat(e).hasMessageThat().isEqualTo("Type argument class java.lang.Number does not satisfy bounds" + + " for type variable T declared by " + GenericWithMultiBound.class); + + e = assertThrows(IllegalArgumentException.class, () -> TypeToken.getParameterized(GenericWithMultiBound.class, CharSequence.class)); + assertThat(e).hasMessageThat().isEqualTo("Type argument interface java.lang.CharSequence does not satisfy bounds" + + " for type variable T declared by " + GenericWithMultiBound.class); + + e = assertThrows(IllegalArgumentException.class, () -> TypeToken.getParameterized(GenericWithMultiBound.class, Object.class)); + assertThat(e).hasMessageThat().isEqualTo("Type argument class java.lang.Object does not satisfy bounds" + + " for type variable T declared by " + GenericWithMultiBound.class); - try { - TypeToken.getParameterized(GenericWithMultiBound.class, Object.class); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessageThat().isEqualTo("Type argument class java.lang.Object does not satisfy bounds " - + "for type variable T declared by " + GenericWithMultiBound.class); + class Outer { + class NonStaticInner {} } + + e = assertThrows(IllegalArgumentException.class, () -> TypeToken.getParameterized(Outer.NonStaticInner.class, Object.class)); + assertThat(e).hasMessageThat().isEqualTo("Raw type " + Outer.NonStaticInner.class.getName() + + " is not supported because it requires specifying an owner type"); } private static class CustomTypeToken extends TypeToken { @@ -231,40 +220,23 @@ class SubTypeToken extends TypeToken {} class SubSubTypeToken1 extends SubTypeToken {} class SubSubTypeToken2 extends SubTypeToken {} - try { - new SubTypeToken() {}; - fail(); - } catch (IllegalStateException expected) { - assertThat(expected).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken"); - } + IllegalStateException e = assertThrows(IllegalStateException.class, () -> new SubTypeToken() {}); + assertThat(e).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken"); - try { - new SubSubTypeToken1(); - fail(); - } catch (IllegalStateException expected) { - assertThat(expected).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken"); - } + e = assertThrows(IllegalStateException.class, () -> new SubSubTypeToken1()); + assertThat(e).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken"); - try { - new SubSubTypeToken2(); - fail(); - } catch (IllegalStateException expected) { - assertThat(expected).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken"); - } + e = assertThrows(IllegalStateException.class, () -> new SubSubTypeToken2()); + assertThat(e).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken"); } @SuppressWarnings("rawtypes") @Test public void testTypeTokenRaw() { - try { - 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." - + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#type-token-raw" - ); - } + IllegalStateException e = assertThrows(IllegalStateException.class, () -> new TypeToken() {}); + assertThat(e).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/main/Troubleshooting.md#type-token-raw"); } } From 6d9c3566b71900c54644a9f71ce028696ee5d4bd Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Thu, 6 Jul 2023 19:04:36 +0200 Subject: [PATCH 37/95] Document minimum Android API level and add workflow to check compatibility (#2431) --- .../workflows/check-android-compatibility.yml | 29 +++++++++++++++++++ .github/workflows/check-api-compatibility.yml | 2 ++ README.md | 7 +++++ gson/pom.xml | 1 + metrics/pom.xml | 8 +++++ pom.xml | 27 +++++++++++++++++ shrinker-test/pom.xml | 8 +++++ 7 files changed, 82 insertions(+) create mode 100644 .github/workflows/check-android-compatibility.yml diff --git a/.github/workflows/check-android-compatibility.yml b/.github/workflows/check-android-compatibility.yml new file mode 100644 index 0000000000..e71956fa2c --- /dev/null +++ b/.github/workflows/check-android-compatibility.yml @@ -0,0 +1,29 @@ +# For security reasons this is a separate GitHub workflow, see https://github.com/google/gson/issues/2429#issuecomment-1622522842 +# Once https://github.com/mojohaus/animal-sniffer/issues/252 or https://github.com/mojohaus/animal-sniffer/pull/253 +# are resolved, can consider adjusting pom.xml to include this as part of normal Maven build + +name: Check Android compatibility + +on: [push, pull_request] + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + check-android-compatibility: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '11' + cache: 'maven' + + - name: Check Android compatibility + run: | + # Run 'test' phase because plugin normally expects to be executed after tests have been compiled + mvn --batch-mode --no-transfer-progress test animal-sniffer:check@check-android-compatibility -DskipTests diff --git a/.github/workflows/check-api-compatibility.yml b/.github/workflows/check-api-compatibility.yml index a446576408..79a793f564 100644 --- a/.github/workflows/check-api-compatibility.yml +++ b/.github/workflows/check-api-compatibility.yml @@ -1,3 +1,5 @@ +# This workflow makes sure that a pull request does not make any incompatible changes +# to the public API of Gson name: Check API compatibility on: pull_request diff --git a/README.md b/README.md index 04eb1c94e8..7418a27af7 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,13 @@ When this module is present, Gson can use the `Unsafe` class to create instances However, care should be taken when relying on this. `Unsafe` is not available in all environments and its usage has some pitfalls, see [`GsonBuilder.disableJdkUnsafe()`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#disableJdkUnsafe()). +#### Minimum Android API level + +- Gson 2.11.0 and newer: API level 21 +- Gson 2.10.1 and older: API level 19 + +Older Gson versions may also support lower API levels, however this has not been verified. + ### Documentation * [API Javadoc](https://www.javadoc.io/doc/com.google.code.gson/gson): Documentation for the current release * [User guide](UserGuide.md): This guide contains examples on how to use Gson in your code diff --git a/gson/pom.xml b/gson/pom.xml index 4cb7757d85..0e120d3e44 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -276,6 +276,7 @@ + JDK17 diff --git a/metrics/pom.xml b/metrics/pom.xml index 82f2c8ea24..82098745e0 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -68,6 +68,14 @@ true + + org.codehaus.mojo + animal-sniffer-maven-plugin + + + true + + org.apache.maven.plugins maven-deploy-plugin diff --git a/pom.xml b/pom.xml index 34df6c4cc2..9ba8f03989 100644 --- a/pom.xml +++ b/pom.xml @@ -315,6 +315,33 @@ + + + + org.codehaus.mojo + animal-sniffer-maven-plugin + 1.23 + + + check-android-compatibility + + check + + + + + net.sf.androidscents.signature + android-api-level-21 + 5.0.1_r2 + + + + + diff --git a/shrinker-test/pom.xml b/shrinker-test/pom.xml index 7c6a4bacc0..e72d26e60d 100644 --- a/shrinker-test/pom.xml +++ b/shrinker-test/pom.xml @@ -69,6 +69,14 @@ true + + org.codehaus.mojo + animal-sniffer-maven-plugin + + + true + + org.apache.maven.plugins maven-deploy-plugin From ecb9f8c8adab5446225461b7f8aea16c79e2e104 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Mon, 24 Jul 2023 16:34:02 +0200 Subject: [PATCH 38/95] Adjust ProGuard default rules and shrinking tests (#2420) * Adjust ProGuard default rules and shrinking tests * Adjust comment * Add shrinking test for class without no-args constructor; improve docs * Improve Unsafe mention in Troubleshooting Guide * Improve comment for `-if class *` --- Troubleshooting.md | 8 +++++- examples/android-proguard-example/README.md | 7 +++-- .../gson/internal/ConstructorConstructor.java | 17 +++++------ .../main/resources/META-INF/proguard/gson.pro | 28 +++++++++++++------ .../gson/functional/Java17RecordTest.java | 27 +++++++++--------- .../internal/ConstructorConstructorTest.java | 22 +++++++-------- shrinker-test/pom.xml | 2 ++ shrinker-test/proguard.pro | 19 +++++++++++++ shrinker-test/r8.pro | 18 ++---------- .../ClassWithJsonAdapterAnnotation.java | 1 + .../com/example/DefaultConstructorMain.java | 20 ++++++++++++- .../java/com/google/gson/it/ShrinkingIT.java | 20 +++++++++++++ 12 files changed, 129 insertions(+), 60 deletions(-) diff --git a/Troubleshooting.md b/Troubleshooting.md index 57e781cbb9..184f19166e 100644 --- a/Troubleshooting.md +++ b/Troubleshooting.md @@ -313,6 +313,8 @@ Note: For newer Gson versions these rules might be applied automatically; make s **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). +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. + **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: @@ -324,6 +326,10 @@ Note: For newer Gson versions these rules might be applied automatically; make s } ``` +You can also use `(...);` to keep all constructors of that class, but then you might actually rely on `sun.misc.Unsafe` on both JDK and Android to create classes without no-args constructor, see [`GsonBuilder.disableJdkUnsafe()`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#disableJdkUnsafe()) for more information. + 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. +For Android you can alternatively use the [`@Keep` annotation](https://developer.android.com/studio/write/annotations#keep) on the class or constructor you want to keep. That might be easier than having to maintain a custom R8 configuration. + +Note that the latest Gson versions (> 2.10.1) specify a default R8 configuration. If your class is a top-level class or is `static`, has a no-args constructor and its fields are annotated with Gson's [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html), you might not have to perform any additional R8 configuration. diff --git a/examples/android-proguard-example/README.md b/examples/android-proguard-example/README.md index 942477d71a..902960fdfa 100644 --- a/examples/android-proguard-example/README.md +++ b/examples/android-proguard-example/README.md @@ -12,6 +12,9 @@ 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, +Note that the latest Gson versions (> 2.10.1) 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. +the Gson version you are using. In general if your classes are top-level classes or are `static`, have a no-args constructor and their fields are annotated with Gson's [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html), you might not have to perform any additional ProGuard or R8 configuration. + +An alternative to writing custom keep rules for your classes in the ProGuard configuration can be to use +Android's [`@Keep` annotation](https://developer.android.com/studio/write/annotations#keep). diff --git a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java index 7d2dc9b622..0f488a9bb9 100644 --- a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java +++ b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java @@ -76,14 +76,15 @@ static String checkInstantiable(Class c) { if (Modifier.isAbstract(modifiers)) { // 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(); + /* + * Note: Ideally should only show this R8-specific message when it is clear that R8 was + * used (e.g. when `c.getDeclaredConstructors().length == 0`), but on Android where this + * issue with R8 occurs most, R8 seems to keep some constructors for some reason while + * still making the class abstract + */ + 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 null; } diff --git a/gson/src/main/resources/META-INF/proguard/gson.pro b/gson/src/main/resources/META-INF/proguard/gson.pro index c9f235e95b..59d3bb441d 100644 --- a/gson/src/main/resources/META-INF/proguard/gson.pro +++ b/gson/src/main/resources/META-INF/proguard/gson.pro @@ -13,7 +13,7 @@ # Keep Gson annotations # Note: Cannot perform finer selection here to only cover Gson annotations, see also https://stackoverflow.com/q/47515093 --keepattributes *Annotation* +-keepattributes RuntimeVisibleAnnotations,AnnotationDefault ### The following rules are needed for R8 in "full mode" which only adheres to `-keepattribtues` if @@ -24,10 +24,10 @@ -keep class com.google.gson.reflect.TypeToken { *; } # Keep any (anonymous) classes extending TypeToken --keep class * extends com.google.gson.reflect.TypeToken +-keep,allowobfuscation class * extends com.google.gson.reflect.TypeToken # Keep classes with @JsonAdapter annotation --keep @com.google.gson.annotations.JsonAdapter class * +-keep,allowobfuscation,allowoptimization @com.google.gson.annotations.JsonAdapter class * # Keep fields with @SerializedName annotation, but allow obfuscation of their names -keepclassmembers,allowobfuscation class * { @@ -35,7 +35,9 @@ } # Keep fields with any other Gson annotation --keepclassmembers class * { +# Also allow obfuscation, assuming that users will additionally use @SerializedName or +# other means to preserve the field names +-keepclassmembers,allowobfuscation class * { @com.google.gson.annotations.Expose ; @com.google.gson.annotations.JsonAdapter ; @com.google.gson.annotations.Since ; @@ -44,15 +46,25 @@ # 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 { +-keepclassmembers class * extends com.google.gson.TypeAdapter { (); } --keep class * implements com.google.gson.TypeAdapterFactory { +-keepclassmembers class * implements com.google.gson.TypeAdapterFactory { (); } --keep class * implements com.google.gson.JsonSerializer { +-keepclassmembers class * implements com.google.gson.JsonSerializer { (); } --keep class * implements com.google.gson.JsonDeserializer { +-keepclassmembers class * implements com.google.gson.JsonDeserializer { (); } + +# If a class is used in some way by the application, and has fields annotated with @SerializedName +# and a no-args constructor, keep those fields and the constructor +# Based on https://issuetracker.google.com/issues/150189783#comment11 +# See also https://github.com/google/gson/pull/2420#discussion_r1241813541 for a more detailed explanation +-if class * +-keepclasseswithmembers,allowobfuscation,allowoptimization class <1> { + (); + @com.google.gson.annotations.SerializedName ; +} diff --git a/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java b/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java index 0b9a6c078c..bc9e8f06b6 100644 --- a/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java +++ b/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java @@ -89,7 +89,7 @@ public int i() { var exception = assertThrows(JsonIOException.class, () -> gson.getAdapter(LocalRecord.class)); assertThat(exception).hasMessageThat() - .isEqualTo("@SerializedName on method '" + LocalRecord.class.getName() + "#i()' is not supported"); + .isEqualTo("@SerializedName on method '" + LocalRecord.class.getName() + "#i()' is not supported"); } @Test @@ -154,7 +154,7 @@ record LocalRecord(String s) { // TODO: Adjust this once Gson throws more specific exception type catch (RuntimeException e) { assertThat(e).hasMessageThat() - .isEqualTo("Failed to invoke constructor '" + LocalRecord.class.getName() + "(String)' with args [value]"); + .isEqualTo("Failed to invoke constructor '" + LocalRecord.class.getName() + "(String)' with args [value]"); assertThat(e).hasCauseThat().isSameInstanceAs(LocalRecord.thrownException); } } @@ -227,7 +227,7 @@ public void testPrimitiveJsonNullValue() { String s = "{'aString': 's', 'aByte': null, 'aShort': 0}"; var e = assertThrows(JsonParseException.class, () -> gson.fromJson(s, RecordWithPrimitives.class)); assertThat(e).hasMessageThat() - .isEqualTo("null is not allowed as value for record component 'aByte' of primitive type; at path $.aByte"); + .isEqualTo("null is not allowed as value for record component 'aByte' of primitive type; at path $.aByte"); } /** @@ -384,8 +384,8 @@ record Blocked(int b) {} var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new Blocked(1))); assertThat(exception).hasMessageThat() - .isEqualTo("ReflectionAccessFilter does not permit using reflection for class " + Blocked.class.getName() + - ". Register a TypeAdapter for this type or adjust the access filter."); + .isEqualTo("ReflectionAccessFilter does not permit using reflection for class " + Blocked.class.getName() + + ". Register a TypeAdapter for this type or adjust the access filter."); } @Test @@ -396,15 +396,15 @@ public void testReflectionFilterBlockInaccessible() { var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new PrivateRecord(1))); assertThat(exception).hasMessageThat() - .isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and" - + " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring" - + " type, adjust the access filter or increase the visibility of the element and its declaring type."); + .isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and" + + " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring" + + " type, adjust the access filter or increase the visibility of the element and its declaring type."); exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", PrivateRecord.class)); assertThat(exception).hasMessageThat() - .isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and" - + " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring" - + " type, adjust the access filter or increase the visibility of the element and its declaring type."); + .isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and" + + " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring" + + " type, adjust the access filter or increase the visibility of the element and its declaring type."); assertThat(gson.toJson(new PublicRecord(1))).isEqualTo("{\"i\":1}"); assertThat(gson.fromJson("{\"i\":2}", PublicRecord.class)).isEqualTo(new PublicRecord(2)); @@ -427,7 +427,8 @@ record LocalRecord(int i) {} var exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", Record.class)); assertThat(exception).hasMessageThat() - .isEqualTo("Abstract classes can't be instantiated! Register an InstanceCreator or a TypeAdapter for" - + " this type. Class name: java.lang.Record"); + .isEqualTo("Abstract classes can't be instantiated! Adjust the R8 configuration or register an InstanceCreator" + + " or a TypeAdapter for this type. Class name: java.lang.Record" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class"); } } diff --git a/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java b/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java index 602ba074e9..e582ad08ac 100644 --- a/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java +++ b/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java @@ -19,17 +19,14 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import com.google.gson.InstanceCreator; -import com.google.gson.ReflectionAccessFilter; import com.google.gson.reflect.TypeToken; -import java.lang.reflect.Type; import java.util.Collections; import org.junit.Test; public class ConstructorConstructorTest { private ConstructorConstructor constructorConstructor = new ConstructorConstructor( - Collections.>emptyMap(), true, - Collections.emptyList() + Collections.emptyMap(), true, + Collections.emptyList() ); private abstract static class AbstractClass { @@ -39,7 +36,7 @@ public AbstractClass() { } private interface Interface { } /** - * Verify that ConstructorConstructor does not try to invoke no-arg constructor + * Verify that ConstructorConstructor does not try to invoke no-args constructor * of abstract class. */ @Test @@ -49,9 +46,10 @@ public void testGet_AbstractClassNoArgConstructor() { constructor.construct(); fail("Expected exception"); } catch (RuntimeException exception) { - assertThat(exception).hasMessageThat().isEqualTo("Abstract classes can't be instantiated! " - + "Register an InstanceCreator or a TypeAdapter for this type. " - + "Class name: com.google.gson.internal.ConstructorConstructorTest$AbstractClass"); + assertThat(exception).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.google.gson.internal.ConstructorConstructorTest$AbstractClass" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class"); } } @@ -62,9 +60,9 @@ public void testGet_Interface() { constructor.construct(); fail("Expected exception"); } catch (RuntimeException exception) { - assertThat(exception).hasMessageThat().isEqualTo("Interfaces can't be instantiated! " - + "Register an InstanceCreator or a TypeAdapter for this type. " - + "Interface name: com.google.gson.internal.ConstructorConstructorTest$Interface"); + assertThat(exception).hasMessageThat().isEqualTo("Interfaces can't be instantiated!" + + " Register an InstanceCreator or a TypeAdapter for this type." + + " Interface name: com.google.gson.internal.ConstructorConstructorTest$Interface"); } } } diff --git a/shrinker-test/pom.xml b/shrinker-test/pom.xml index e72d26e60d..65de7c7f12 100644 --- a/shrinker-test/pom.xml +++ b/shrinker-test/pom.xml @@ -199,6 +199,8 @@ + com.android.tools r8 8.0.40 diff --git a/shrinker-test/proguard.pro b/shrinker-test/proguard.pro index 0cb35048c9..3e8a812041 100644 --- a/shrinker-test/proguard.pro +++ b/shrinker-test/proguard.pro @@ -18,6 +18,7 @@ -keep class com.example.DefaultConstructorMain { public static java.lang.String runTest(); public static java.lang.String runTestNoJdkUnsafe(); + public static java.lang.String runTestNoDefaultConstructor(); } @@ -27,3 +28,21 @@ -keepclassmembers class com.example.ClassWithNamedFields { !transient ; } + +-keepclassmembernames class com.example.ClassWithExposeAnnotation { + ; +} +-keepclassmembernames class com.example.ClassWithJsonAdapterAnnotation { + ** f; +} +-keepclassmembernames class com.example.ClassWithVersionAnnotations { + ; +} + + +-keepclassmembernames class com.example.DefaultConstructorMain$TestClass { + ; +} +-keepclassmembernames class com.example.DefaultConstructorMain$TestClassNotAbstract { + ; +} diff --git a/shrinker-test/r8.pro b/shrinker-test/r8.pro index 690fe339b5..392f0f0d9c 100644 --- a/shrinker-test/r8.pro +++ b/shrinker-test/r8.pro @@ -4,20 +4,6 @@ ### 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/main/compatibility-faq.md#r8-full-mode -# Keep the no-args constructor of deserialized classes --keepclassmembers class com.example.ClassWithDefaultConstructor { - (); -} --keepclassmembers class com.example.GenericClasses$GenericClass { - (); -} --keepclassmembers class com.example.GenericClasses$UsingGenericClass { - (); -} --keepclassmembers class com.example.GenericClasses$GenericUsingGenericClass { - (); -} - # 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 @@ -25,12 +11,14 @@ # Don't obfuscate class name, to check it in exception message -keep,allowshrinking,allowoptimization class com.example.DefaultConstructorMain$TestClass +-keep,allowshrinking,allowoptimization class com.example.DefaultConstructorMain$TestClassWithoutDefaultConstructor + # 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 ; } # Keep enum constants which are not explicitly used in code --keep class com.example.EnumClass { +-keepclassmembers class com.example.EnumClass { ** SECOND; } diff --git a/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java b/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java index 238ee1818e..0cb05a25a6 100644 --- a/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java +++ b/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java @@ -23,6 +23,7 @@ */ public class ClassWithJsonAdapterAnnotation { // For this field don't use @SerializedName and ignore it for deserialization + // Has custom ProGuard rule to keep the field name @JsonAdapter(value = Adapter.class, nullSafe = false) DummyClass f; diff --git a/shrinker-test/src/main/java/com/example/DefaultConstructorMain.java b/shrinker-test/src/main/java/com/example/DefaultConstructorMain.java index e570866bec..4f0152f7d1 100644 --- a/shrinker-test/src/main/java/com/example/DefaultConstructorMain.java +++ b/shrinker-test/src/main/java/com/example/DefaultConstructorMain.java @@ -8,14 +8,24 @@ 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 { + public String s; + } + + // Current Gson ProGuard rules only keep default constructor (and only then prevent R8 from + // making class abstract); other constructors are ignored to suggest to user adding default + // constructor instead of implicitly relying on JDK Unsafe + static class TestClassWithoutDefaultConstructor { @SerializedName("s") public String s; + + public TestClassWithoutDefaultConstructor(String s) { + this.s = s; + } } /** @@ -34,4 +44,12 @@ public static String runTestNoJdkUnsafe() { TestClassNotAbstract deserialized = gson.fromJson("{\"s\": \"value\"}", same(TestClassNotAbstract.class)); return deserialized.s; } + + /** + * Main entrypoint, called by {@code ShrinkingIT.testNoDefaultConstructor()}. + */ + public static String runTestNoDefaultConstructor() { + TestClassWithoutDefaultConstructor deserialized = new Gson().fromJson("{\"s\":\"value\"}", same(TestClassWithoutDefaultConstructor.class)); + return deserialized.s; + } } diff --git a/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java b/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java index 9c32f1aa57..3304570197 100644 --- a/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java +++ b/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java @@ -220,4 +220,24 @@ public void testDefaultConstructorNoJdkUnsafe() throws Exception { } }); } + + @Test + public void testNoDefaultConstructor() throws Exception { + runTest("com.example.DefaultConstructorMain", c -> { + Method m = c.getMethod("runTestNoDefaultConstructor"); + + 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$TestClassWithoutDefaultConstructor" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class" + ); + } + }); + } } From 3d33a8621f70c03269dbf8a15c18a2a1ba68f9f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 07:55:18 -0700 Subject: [PATCH 39/95] Bump com.google.guava:guava from 32.0.1-jre to 32.1.1-jre (#2444) Bumps [com.google.guava:guava](https://github.com/google/guava) from 32.0.1-jre to 32.1.1-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- proto/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto/pom.xml b/proto/pom.xml index fbf87b17b0..dfd2024bf6 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -50,7 +50,7 @@ com.google.guava guava - 32.0.1-jre + 32.1.1-jre From 79ae239a499d72c599f9b4d27166aad61086c3eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 07:55:59 -0700 Subject: [PATCH 40/95] Bump com.google.guava:guava-testlib from 32.0.1-jre to 32.1.1-jre (#2443) Bumps [com.google.guava:guava-testlib](https://github.com/google/guava) from 32.0.1-jre to 32.1.1-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava-testlib dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gson/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/pom.xml b/gson/pom.xml index 0e120d3e44..9a48ef4ded 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -62,7 +62,7 @@ com.google.guava guava-testlib - 32.0.1-jre + 32.1.1-jre test From a38b757bc4fddfd57ab762fbf192dd2b3fc6be22 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Tue, 25 Jul 2023 22:00:26 +0200 Subject: [PATCH 41/95] Support non-generic type for `TypeToken.getParameterized` for legacy reasons (#2447) This partially restores the behavior before a589ef20087b4b0f1ec3048d3ceaef1eedccd09d, except that back then for a non-generic type a bogus `TypeToken(ParameterizedType)` was created, whereas now a `TypeToken(Class)` is created instead. --- .../com/google/gson/reflect/TypeToken.java | 28 ++++++++++--------- .../google/gson/reflect/TypeTokenTest.java | 5 +++- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/gson/src/main/java/com/google/gson/reflect/TypeToken.java b/gson/src/main/java/com/google/gson/reflect/TypeToken.java index 4a695666c8..3921a70b64 100644 --- a/gson/src/main/java/com/google/gson/reflect/TypeToken.java +++ b/gson/src/main/java/com/google/gson/reflect/TypeToken.java @@ -337,9 +337,12 @@ public static TypeToken get(Class type) { * As seen here the result is a {@code TypeToken}; this method cannot provide any type safety, * and care must be taken to pass in the correct number of type arguments. * + *

    If {@code rawType} is a non-generic class and no type arguments are provided, this method + * simply delegates to {@link #get(Class)} and creates a {@code TypeToken(Class)}. + * * @throws IllegalArgumentException - * If {@code rawType} is not of type {@code Class}, if it is not a generic type, or if the - * type arguments are invalid for the raw type + * If {@code rawType} is not of type {@code Class}, or if the type arguments are invalid for + * the raw type */ public static TypeToken getParameterized(Type rawType, Type... typeArguments) { Objects.requireNonNull(rawType); @@ -354,10 +357,16 @@ public static TypeToken getParameterized(Type rawType, Type... typeArguments) Class rawClass = (Class) rawType; TypeVariable[] typeVariables = rawClass.getTypeParameters(); - // Note: Does not check if owner type of rawType is generic because this factory method - // does not support specifying owner type - if (typeVariables.length == 0) { - throw new IllegalArgumentException(rawClass.getName() + " is not a generic type"); + int expectedArgsCount = typeVariables.length; + int actualArgsCount = typeArguments.length; + if (actualArgsCount != expectedArgsCount) { + throw new IllegalArgumentException(rawClass.getName() + " requires " + expectedArgsCount + + " type arguments, but got " + actualArgsCount); + } + + // For legacy reasons create a TypeToken(Class) if the type is not generic + if (typeArguments.length == 0) { + return get(rawClass); } // Check for this here to avoid misleading exception thrown by ParameterizedTypeImpl @@ -366,13 +375,6 @@ public static TypeToken getParameterized(Type rawType, Type... typeArguments) + " it requires specifying an owner type"); } - int expectedArgsCount = typeVariables.length; - int actualArgsCount = typeArguments.length; - if (actualArgsCount != expectedArgsCount) { - throw new IllegalArgumentException(rawClass.getName() + " requires " + expectedArgsCount + - " type arguments, but got " + actualArgsCount); - } - for (int i = 0; i < expectedArgsCount; i++) { Type typeArgument = Objects.requireNonNull(typeArguments[i], "Type argument must not be null"); Class rawTypeArgument = $Gson$Types.getRawType(typeArgument); diff --git a/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java b/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java index d38f05b1ec..4c1ccf0d6b 100644 --- a/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java +++ b/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java @@ -146,6 +146,9 @@ public void testParameterizedFactory() { class LocalGenericClass {} TypeToken expectedLocalType = new TypeToken>() {}; assertThat(TypeToken.getParameterized(LocalGenericClass.class, Integer.class)).isEqualTo(expectedLocalType); + + // For legacy reasons, if requesting parameterized type for non-generic class, create a `TypeToken(Class)` + assertThat(TypeToken.getParameterized(String.class)).isEqualTo(TypeToken.get(String.class)); } @Test @@ -158,7 +161,7 @@ public void testParameterizedFactory_Invalid() { assertThat(e).hasMessageThat().isEqualTo("rawType must be of type Class, but was java.lang.String[]"); e = assertThrows(IllegalArgumentException.class, () -> TypeToken.getParameterized(String.class, Number.class)); - assertThat(e).hasMessageThat().isEqualTo("java.lang.String is not a generic type"); + assertThat(e).hasMessageThat().isEqualTo("java.lang.String requires 0 type arguments, but got 1"); e = assertThrows(IllegalArgumentException.class, () -> TypeToken.getParameterized(List.class, new Type[0])); assertThat(e).hasMessageThat().isEqualTo("java.util.List requires 1 type arguments, but got 0"); From 5a87d806550f8389fb26327bcf97751c6851d7c7 Mon Sep 17 00:00:00 2001 From: elevne <97422844+elevne@users.noreply.github.com> Date: Thu, 27 Jul 2023 00:47:28 +0900 Subject: [PATCH 42/95] Fixed Typo in GsonBuilder.java (#2449) --- gson/src/main/java/com/google/gson/GsonBuilder.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java index e3d4818233..c72c411f07 100644 --- a/gson/src/main/java/com/google/gson/GsonBuilder.java +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java @@ -57,8 +57,7 @@ * use {@code new Gson()}. {@code GsonBuilder} is best used by creating it, and then invoking its * various configuration methods, and finally calling create.

    * - *

    The following is an example shows how to use the {@code GsonBuilder} to construct a Gson - * instance: + *

    The following example shows how to use the {@code GsonBuilder} to construct a Gson instance: * *

      * Gson gson = new GsonBuilder()
    @@ -120,7 +119,7 @@ public GsonBuilder() {
        * Constructs a GsonBuilder instance from a Gson instance. The newly constructed GsonBuilder
        * has the same configuration as the previously built Gson instance.
        *
    -   * @param gson the gson instance whose configuration should by applied to a new GsonBuilder.
    +   * @param gson the gson instance whose configuration should be applied to a new GsonBuilder.
        */
       GsonBuilder(Gson gson) {
         this.excluder = gson.excluder;
    @@ -274,7 +273,7 @@ public GsonBuilder serializeNulls() {
        * {"x":2,"y":3}}.
        *
        * 

    Given the assumption above, a {@code Map} will be - * serialize as an array of arrays (can be viewed as an entry set of pairs). + * serialized as an array of arrays (can be viewed as an entry set of pairs). * *

    Below is an example of serializing complex types as JSON arrays: *

     {@code
    @@ -574,7 +573,7 @@ public GsonBuilder setDateFormat(String pattern) {
       }
     
       /**
    -   * Configures Gson to to serialize {@code Date} objects according to the style value provided.
    +   * Configures Gson to serialize {@code Date} objects according to the style value provided.
        * You can call this method or {@link #setDateFormat(String)} multiple times, but only the last
        * invocation will be used to decide the serialization format.
        *
    @@ -595,7 +594,7 @@ public GsonBuilder setDateFormat(int style) {
       }
     
       /**
    -   * Configures Gson to to serialize {@code Date} objects according to the style value provided.
    +   * Configures Gson to serialize {@code Date} objects according to the style value provided.
        * You can call this method or {@link #setDateFormat(String)} multiple times, but only the last
        * invocation will be used to decide the serialization format.
        *
    
    From 5055b6246314e04f54f7d7e2c46b7d22786a9089 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?=C3=89amonn=20McManus?= 
    Date: Wed, 26 Jul 2023 12:24:04 -0700
    Subject: [PATCH 43/95] Make date-formatting tests less fragile with regular
     expressions. (#2450)
    
    * Make date-formatting tests less fragile with regular expressions.
    
    This is not great. We should really ensure that formatted dates are the same
    regardless of JDK version. There is code that attempts to do that but it is not
    really effective. So for now we fudge around the differences by using regular
    expressions to paper over the differences.
    
    * Temporarily add test-debugging code.
    
    * Another attempt at debugging a test failure.
    
    * Fix pattern in assertion.
    ---
     .../functional/DefaultTypeAdaptersTest.java   | 12 ++------
     .../google/gson/functional/ObjectTest.java    |  7 ++---
     .../bind/DefaultDateTypeAdapterTest.java      | 28 +++++++++----------
     .../gson/internal/sql/SqlTypesGsonTest.java   |  9 +++---
     4 files changed, 22 insertions(+), 34 deletions(-)
    
    diff --git a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java
    index e70c175b3c..e858062dd1 100644
    --- a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java
    +++ b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java
    @@ -384,11 +384,7 @@ public void testBitSetDeserialization() {
       public void testDefaultDateSerialization() {
         Date now = new Date(1315806903103L);
         String json = gson.toJson(now);
    -    if (JavaVersion.isJava9OrLater()) {
    -      assertThat(json).isEqualTo("\"Sep 11, 2011, 10:55:03 PM\"");
    -    } else {
    -      assertThat(json).isEqualTo("\"Sep 11, 2011 10:55:03 PM\"");
    -    }
    +    assertThat(json).matches("\"Sep 11, 2011,? 10:55:03\\hPM\"");
       }
     
       @Test
    @@ -420,11 +416,7 @@ public void testDefaultDateSerializationUsingBuilder() {
         Gson gson = new GsonBuilder().create();
         Date now = new Date(1315806903103L);
         String json = gson.toJson(now);
    -    if (JavaVersion.isJava9OrLater()) {
    -      assertThat(json).isEqualTo("\"Sep 11, 2011, 10:55:03 PM\"");
    -    } else {
    -      assertThat(json).isEqualTo("\"Sep 11, 2011 10:55:03 PM\"");
    -    }
    +    assertThat(json).matches("\"Sep 11, 2011,? 10:55:03\\hPM\"");
       }
     
       @Test
    diff --git a/gson/src/test/java/com/google/gson/functional/ObjectTest.java b/gson/src/test/java/com/google/gson/functional/ObjectTest.java
    index b0de9da48d..e0efd94dee 100644
    --- a/gson/src/test/java/com/google/gson/functional/ObjectTest.java
    +++ b/gson/src/test/java/com/google/gson/functional/ObjectTest.java
    @@ -570,11 +570,8 @@ static final class Product {
       public void testDateAsMapObjectField() {
         HasObjectMap a = new HasObjectMap();
         a.map.put("date", new Date(0));
    -    if (JavaVersion.isJava9OrLater()) {
    -      assertThat(gson.toJson(a)).isEqualTo("{\"map\":{\"date\":\"Dec 31, 1969, 4:00:00 PM\"}}");
    -    } else {
    -      assertThat(gson.toJson(a)).isEqualTo("{\"map\":{\"date\":\"Dec 31, 1969 4:00:00 PM\"}}");
    -    }
    +    assertThat(gson.toJson(a))
    +        .matches("\\{\"map\":\\{\"date\":\"Dec 31, 1969,? 4:00:00\\hPM\"\\}\\}");
       }
     
       static class HasObjectMap {
    diff --git a/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java b/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java
    index 2034e38fc6..835bc9dd49 100644
    --- a/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java
    +++ b/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java
    @@ -58,21 +58,22 @@ private void assertFormattingAlwaysEmitsUsLocale(Locale locale) {
         Locale defaultLocale = Locale.getDefault();
         Locale.setDefault(locale);
         try {
    -      String afterYearSep = JavaVersion.isJava9OrLater() ? ", " : " ";
    -      String afterYearLongSep = JavaVersion.isJava9OrLater() ? " at " : " ";
    -      String utcFull = JavaVersion.isJava9OrLater() ? "Coordinated Universal Time" : "UTC";
    -      assertFormatted(String.format("Jan 1, 1970%s12:00:00 AM", afterYearSep),
    -          DateType.DATE.createDefaultsAdapterFactory());
    +      // The patterns here attempt to accommodate minor date-time formatting differences between JDK
    +      // versions. Ideally Gson would serialize in a way that is independent of the JDK version.
    +      // Note: \h means "horizontal space", because some JDK versions use Narrow No Break Space
    +      // (U+202F) before the AM or PM indication.
    +      String utcFull = "(Coordinated Universal Time|UTC)";
    +      assertFormatted("Jan 1, 1970,? 12:00:00\\hAM", DateType.DATE.createDefaultsAdapterFactory());
           assertFormatted("1/1/70", DateType.DATE.createAdapterFactory(DateFormat.SHORT));
           assertFormatted("Jan 1, 1970", DateType.DATE.createAdapterFactory(DateFormat.MEDIUM));
           assertFormatted("January 1, 1970", DateType.DATE.createAdapterFactory(DateFormat.LONG));
    -      assertFormatted(String.format("1/1/70%s12:00 AM", afterYearSep),
    +      assertFormatted("1/1/70,? 12:00\\hAM",
               DateType.DATE.createAdapterFactory(DateFormat.SHORT, DateFormat.SHORT));
    -      assertFormatted(String.format("Jan 1, 1970%s12:00:00 AM", afterYearSep),
    +      assertFormatted("Jan 1, 1970,? 12:00:00\\hAM",
               DateType.DATE.createAdapterFactory(DateFormat.MEDIUM, DateFormat.MEDIUM));
    -      assertFormatted(String.format("January 1, 1970%s12:00:00 AM UTC", afterYearLongSep),
    +      assertFormatted("January 1, 1970(,| at)? 12:00:00\\hAM UTC",
               DateType.DATE.createAdapterFactory(DateFormat.LONG, DateFormat.LONG));
    -      assertFormatted(String.format("Thursday, January 1, 1970%s12:00:00 AM %s", afterYearLongSep, utcFull),
    +      assertFormatted("Thursday, January 1, 1970(,| at)? 12:00:00\\hAM " + utcFull,
               DateType.DATE.createAdapterFactory(DateFormat.FULL, DateFormat.FULL));
         } finally {
           TimeZone.setDefault(defaultTimeZone);
    @@ -150,9 +151,7 @@ public void testFormatUsesDefaultTimezone() throws Exception {
         Locale defaultLocale = Locale.getDefault();
         Locale.setDefault(Locale.US);
         try {
    -      String afterYearSep = JavaVersion.isJava9OrLater() ? ", " : " ";
    -      assertFormatted(String.format("Dec 31, 1969%s4:00:00 PM", afterYearSep),
    -          DateType.DATE.createDefaultsAdapterFactory());
    +      assertFormatted("Dec 31, 1969,? 4:00:00\\hPM", DateType.DATE.createDefaultsAdapterFactory());
           assertParsed("Dec 31, 1969 4:00:00 PM", DateType.DATE.createDefaultsAdapterFactory());
         } finally {
           TimeZone.setDefault(defaultTimeZone);
    @@ -222,9 +221,10 @@ private static TypeAdapter dateAdapter(TypeAdapterFactory adapterFactory)
         return adapter;
       }
     
    -  private static void assertFormatted(String formatted, TypeAdapterFactory adapterFactory) {
    +  private static void assertFormatted(String formattedPattern, TypeAdapterFactory adapterFactory) {
         TypeAdapter adapter = dateAdapter(adapterFactory);
    -    assertThat(adapter.toJson(new Date(0))).isEqualTo(toLiteral(formatted));
    +    String json = adapter.toJson(new Date(0));
    +    assertThat(json).matches(toLiteral(formattedPattern));
       }
     
       @SuppressWarnings("UndefinedEquals")
    diff --git a/gson/src/test/java/com/google/gson/internal/sql/SqlTypesGsonTest.java b/gson/src/test/java/com/google/gson/internal/sql/SqlTypesGsonTest.java
    index b04c56ab34..8233275c37 100644
    --- a/gson/src/test/java/com/google/gson/internal/sql/SqlTypesGsonTest.java
    +++ b/gson/src/test/java/com/google/gson/internal/sql/SqlTypesGsonTest.java
    @@ -113,11 +113,10 @@ public void testDefaultSqlTimeDeserialization() {
       public void testDefaultSqlTimestampSerialization() {
         Timestamp now = new java.sql.Timestamp(1259875082000L);
         String json = gson.toJson(now);
    -    if (JavaVersion.isJava9OrLater()) {
    -      assertThat(json).isEqualTo("\"Dec 3, 2009, 1:18:02 PM\"");
    -    } else {
    -      assertThat(json).isEqualTo("\"Dec 3, 2009 1:18:02 PM\"");
    -    }
    +    // The exact format of the serialized date-time string depends on the JDK version. The pattern
    +    // here allows for an optional comma after the date, and what might be U+202F (Narrow No-Break
    +    // Space) before "PM".
    +    assertThat(json).matches("\"Dec 3, 2009,? 1:18:02\\hPM\"");
       }
     
       @Test
    
    From 3d241ca0a6435cbf1fa1cdaed2af8480b99fecde Mon Sep 17 00:00:00 2001
    From: Wonil 
    Date: Sun, 30 Jul 2023 06:18:45 +0900
    Subject: [PATCH 44/95] Modification in test cases (#2454)
    
    * Fixed Typo in GsonBuilder.java
    
    * Suggestions on Test cases
    
    * Modified test cases using assertThrows method (JUnit)
    
    * Update gson/src/test/java/com/google/gson/JsonArrayAsListTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/GsonTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/JsonArrayAsListTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/JsonStreamParserTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/JsonStreamParserTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/JsonStreamParserTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/ToNumberPolicyTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/TypeAdapterTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/TypeAdapterTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/ToNumberPolicyTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/ToNumberPolicyTest.java
    
    Co-authored-by: Marcono1234 
    
    ---------
    
    Co-authored-by: Marcono1234 
    ---
     .../test/java/com/google/gson/GsonTest.java   |  26 +---
     .../com/google/gson/JsonArrayAsListTest.java  |  94 +++----------
     .../java/com/google/gson/JsonParserTest.java  |  13 +-
     .../com/google/gson/JsonStreamParserTest.java |  43 ++----
     .../com/google/gson/ToNumberPolicyTest.java   | 131 +++++++-----------
     .../java/com/google/gson/TypeAdapterTest.java |  18 +--
     6 files changed, 95 insertions(+), 230 deletions(-)
    
    diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java
    index fcdc7cbc54..dfa1e3072a 100644
    --- a/gson/src/test/java/com/google/gson/GsonTest.java
    +++ b/gson/src/test/java/com/google/gson/GsonTest.java
    @@ -17,7 +17,7 @@
     package com.google.gson;
     
     import static com.google.common.truth.Truth.assertThat;
    -import static org.junit.Assert.fail;
    +import static org.junit.Assert.assertThrows;
     
     import com.google.gson.Gson.FutureTypeAdapter;
     import com.google.gson.internal.Excluder;
    @@ -104,12 +104,8 @@ private static final class TestTypeAdapter extends TypeAdapter {
       @Test
       public void testGetAdapter_Null() {
         Gson gson = new Gson();
    -    try {
    -      gson.getAdapter((TypeToken) null);
    -      fail();
    -    } catch (NullPointerException e) {
    -      assertThat(e).hasMessageThat().isEqualTo("type must not be null");
    -    }
    +    NullPointerException e = assertThrows(NullPointerException.class, () -> gson.getAdapter((TypeToken) null));
    +    assertThat(e).hasMessageThat().isEqualTo("type must not be null");
       }
     
       @Test
    @@ -282,13 +278,9 @@ public void testNewJsonWriter_Default() throws IOException {
         jsonWriter.value(true);
         jsonWriter.endObject();
     
    -    try {
    -      // Additional top-level value
    -      jsonWriter.value(1);
    -      fail();
    -    } catch (IllegalStateException expected) {
    -      assertThat(expected).hasMessageThat().isEqualTo("JSON must have only one top-level value.");
    -    }
    +    // Additional top-level value
    +    IllegalStateException e = assertThrows(IllegalStateException.class, () -> jsonWriter.value(1));
    +    assertThat(e).hasMessageThat().isEqualTo("JSON must have only one top-level value.");
     
         jsonWriter.close();
         assertThat(writer.toString()).isEqualTo("{\"\\u003ctest2\":true}");
    @@ -323,11 +315,7 @@ public void testNewJsonWriter_Custom() throws IOException {
       public void testNewJsonReader_Default() throws IOException {
         String json = "test"; // String without quotes
         JsonReader jsonReader = new Gson().newJsonReader(new StringReader(json));
    -    try {
    -      jsonReader.nextString();
    -      fail();
    -    } catch (MalformedJsonException expected) {
    -    }
    +    assertThrows(MalformedJsonException.class, jsonReader::nextString);
         jsonReader.close();
       }
     
    diff --git a/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java b/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java
    index 4998737931..99ffaef76d 100644
    --- a/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java
    +++ b/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java
    @@ -17,7 +17,7 @@
     package com.google.gson;
     
     import static com.google.common.truth.Truth.assertThat;
    -import static org.junit.Assert.fail;
    +import static org.junit.Assert.assertThrows;
     
     import com.google.gson.common.MoreAsserts;
     import java.util.Arrays;
    @@ -37,17 +37,8 @@ public void testGet() {
         List list = a.asList();
         assertThat(list.get(0)).isEqualTo(new JsonPrimitive(1));
     
    -    try {
    -      list.get(-1);
    -      fail();
    -    } catch (IndexOutOfBoundsException e) {
    -    }
    -
    -    try {
    -      list.get(2);
    -      fail();
    -    } catch (IndexOutOfBoundsException e) {
    -    }
    +    assertThrows(IndexOutOfBoundsException.class, () -> list.get(-1));
    +    assertThrows(IndexOutOfBoundsException.class, () -> list.get(2));
     
         a.add((JsonElement) null);
         assertThat(list.get(1)).isEqualTo(JsonNull.INSTANCE);
    @@ -75,24 +66,11 @@ public void testSet() {
         assertThat(list.get(0)).isEqualTo(new JsonPrimitive(2));
         assertThat(a.get(0)).isEqualTo(new JsonPrimitive(2));
     
    -    try {
    -      list.set(-1, new JsonPrimitive(1));
    -      fail();
    -    } catch (IndexOutOfBoundsException e) {
    -    }
    -
    -    try {
    -      list.set(2, new JsonPrimitive(1));
    -      fail();
    -    } catch (IndexOutOfBoundsException e) {
    -    }
    -
    -    try {
    -      list.set(0, null);
    -      fail();
    -    } catch (NullPointerException e) {
    -      assertThat(e).hasMessageThat().isEqualTo("Element must be non-null");
    -    }
    +    assertThrows(IndexOutOfBoundsException.class, () -> list.set(-1, new JsonPrimitive(1)));
    +    assertThrows(IndexOutOfBoundsException.class, () -> list.set(2, new JsonPrimitive(1)));
    +
    +    NullPointerException e = assertThrows(NullPointerException.class, () -> list.set(0, null));
    +    assertThat(e).hasMessageThat().isEqualTo("Element must be non-null");
       }
     
       @Test
    @@ -115,30 +93,14 @@ public void testAdd() {
         );
         assertThat(list).isEqualTo(expectedList);
     
    -    try {
    -      list.set(-1, new JsonPrimitive(1));
    -      fail();
    -    } catch (IndexOutOfBoundsException e) {
    -    }
    -
    -    try {
    -      list.set(list.size(), new JsonPrimitive(1));
    -      fail();
    -    } catch (IndexOutOfBoundsException e) {
    -    }
    -
    -    try {
    -      list.add(0, null);
    -      fail();
    -    } catch (NullPointerException e) {
    -      assertThat(e).hasMessageThat().isEqualTo("Element must be non-null");
    -    }
    -    try {
    -      list.add(null);
    -      fail();
    -    } catch (NullPointerException e) {
    -      assertThat(e).hasMessageThat().isEqualTo("Element must be non-null");
    -    }
    +    assertThrows(IndexOutOfBoundsException.class, () -> list.set(-1, new JsonPrimitive(1)));
    +    assertThrows(IndexOutOfBoundsException.class, () -> list.set(list.size(), new JsonPrimitive(1)));
    +
    +    NullPointerException e = assertThrows(NullPointerException.class, () -> list.add(0, null));
    +    assertThat(e).hasMessageThat().isEqualTo("Element must be non-null");
    +
    +    e = assertThrows(NullPointerException.class, () -> list.add(null));
    +    assertThat(e).hasMessageThat().isEqualTo("Element must be non-null");
       }
     
       @Test
    @@ -157,18 +119,11 @@ public void testAddAll() {
         assertThat(list).isEqualTo(expectedList);
         assertThat(list).isEqualTo(expectedList);
     
    -    try {
    -      list.addAll(0, Collections.singletonList(null));
    -      fail();
    -    } catch (NullPointerException e) {
    -      assertThat(e).hasMessageThat().isEqualTo("Element must be non-null");
    -    }
    -    try {
    -      list.addAll(Collections.singletonList(null));
    -      fail();
    -    } catch (NullPointerException e) {
    -      assertThat(e).hasMessageThat().isEqualTo("Element must be non-null");
    -    }
    +    NullPointerException e = assertThrows(NullPointerException.class, () -> list.addAll(0, Collections.singletonList(null)));
    +    assertThat(e).hasMessageThat().isEqualTo("Element must be non-null");
    +
    +    e = assertThrows(NullPointerException.class, () -> list.addAll(Collections.singletonList(null)));
    +    assertThat(e).hasMessageThat().isEqualTo("Element must be non-null");
       }
     
       @Test
    @@ -180,11 +135,8 @@ public void testRemoveIndex() {
         assertThat(list.remove(0)).isEqualTo(new JsonPrimitive(1));
         assertThat(list).hasSize(0);
         assertThat(a).hasSize(0);
    -    try {
    -      list.remove(0);
    -      fail();
    -    } catch (IndexOutOfBoundsException e) {
    -    }
    +
    +    assertThrows(IndexOutOfBoundsException.class, () -> list.remove(0));
       }
     
       @Test
    diff --git a/gson/src/test/java/com/google/gson/JsonParserTest.java b/gson/src/test/java/com/google/gson/JsonParserTest.java
    index 587ba35dcd..8130625227 100644
    --- a/gson/src/test/java/com/google/gson/JsonParserTest.java
    +++ b/gson/src/test/java/com/google/gson/JsonParserTest.java
    @@ -17,7 +17,7 @@
     package com.google.gson;
     
     import static com.google.common.truth.Truth.assertThat;
    -import static org.junit.Assert.fail;
    +import static org.junit.Assert.assertThrows;
     
     import com.google.gson.common.TestTypes.BagOfPrimitives;
     import com.google.gson.internal.Streams;
    @@ -37,10 +37,7 @@ public class JsonParserTest {
     
       @Test
       public void testParseInvalidJson() {
    -    try {
    -      JsonParser.parseString("[[]");
    -      fail();
    -    } catch (JsonSyntaxException expected) { }
    +    assertThrows(JsonSyntaxException.class, () -> JsonParser.parseString("[[]"));
       }
     
       @Test
    @@ -81,11 +78,7 @@ public void testParseUnquotedSingleWordStringFails() {
     
       @Test
       public void testParseUnquotedMultiWordStringFails() {
    -    String unquotedSentence = "Test is a test..blah blah";
    -    try {
    -      JsonParser.parseString(unquotedSentence);
    -      fail();
    -    } catch (JsonSyntaxException expected) { }
    +    assertThrows(JsonSyntaxException.class, () -> JsonParser.parseString("Test is a test..blah blah"));
       }
     
       @Test
    diff --git a/gson/src/test/java/com/google/gson/JsonStreamParserTest.java b/gson/src/test/java/com/google/gson/JsonStreamParserTest.java
    index 2fec1f1767..807b93f89d 100644
    --- a/gson/src/test/java/com/google/gson/JsonStreamParserTest.java
    +++ b/gson/src/test/java/com/google/gson/JsonStreamParserTest.java
    @@ -16,7 +16,7 @@
     package com.google.gson;
     
     import static com.google.common.truth.Truth.assertThat;
    -import static org.junit.Assert.fail;
    +import static org.junit.Assert.assertThrows;
     
     import java.io.EOFException;
     import java.util.NoSuchElementException;
    @@ -72,57 +72,34 @@ public void testNoSideEffectForHasNext() {
       public void testCallingNextBeyondAvailableInput() {
         JsonElement unused1 = parser.next();
         JsonElement unused2 = parser.next();
    -    try {
    -      parser.next();
    -      fail("Parser should not go beyond available input");
    -    } catch (NoSuchElementException expected) {
    -    }
    +    // Parser should not go beyond available input
    +    assertThrows(NoSuchElementException.class, parser::next);
       }
     
       @Test
       public void testEmptyInput() {
         JsonStreamParser parser = new JsonStreamParser("");
    -    try {
    -      parser.next();
    -      fail();
    -    } catch (JsonIOException e) {
    -      assertThat(e.getCause()).isInstanceOf(EOFException.class);
    -    }
    +    JsonIOException e = assertThrows(JsonIOException.class, parser::next);
    +    assertThat(e).hasCauseThat().isInstanceOf(EOFException.class);
     
         parser = new JsonStreamParser("");
    -    try {
    -      parser.hasNext();
    -      fail();
    -    } catch (JsonIOException e) {
    -      assertThat(e.getCause()).isInstanceOf(EOFException.class);
    -    }
    +    e = assertThrows(JsonIOException.class, parser::hasNext);
    +    assertThat(e).hasCauseThat().isInstanceOf(EOFException.class);
       }
     
       @Test
       public void testIncompleteInput() {
         JsonStreamParser parser = new JsonStreamParser("[");
         assertThat(parser.hasNext()).isTrue();
    -    try {
    -      parser.next();
    -      fail();
    -    } catch (JsonSyntaxException e) {
    -    }
    +    assertThrows(JsonSyntaxException.class, parser::next);
       }
     
       @Test
       public void testMalformedInput() {
         JsonStreamParser parser = new JsonStreamParser(":");
    -    try {
    -      parser.hasNext();
    -      fail();
    -    } catch (JsonSyntaxException e) {
    -    }
    +    assertThrows(JsonSyntaxException.class, parser::hasNext);
     
         parser = new JsonStreamParser(":");
    -    try {
    -      parser.next();
    -      fail();
    -    } catch (JsonSyntaxException e) {
    -    }
    +    assertThrows(JsonSyntaxException.class, parser::next);
       }
     }
    diff --git a/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java b/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java
    index 665b48bf46..4d6c7c41a4 100644
    --- a/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java
    +++ b/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java
    @@ -17,7 +17,7 @@
     package com.google.gson;
     
     import static com.google.common.truth.Truth.assertThat;
    -import static org.junit.Assert.fail;
    +import static org.junit.Assert.assertThrows;
     
     import com.google.gson.internal.LazilyParsedNumber;
     import com.google.gson.stream.JsonReader;
    @@ -33,19 +33,14 @@ public void testDouble() throws IOException {
         ToNumberStrategy strategy = ToNumberPolicy.DOUBLE;
         assertThat(strategy.readNumber(fromString("10.1"))).isEqualTo(10.1);
         assertThat(strategy.readNumber(fromString("3.141592653589793238462643383279"))).isEqualTo(3.141592653589793D);
    -    try {
    -      strategy.readNumber(fromString("1e400"));
    -      fail();
    -    } catch (MalformedJsonException expected) {
    -      assertThat(expected).hasMessageThat().isEqualTo(
    -          "JSON forbids NaN and infinities: Infinity at line 1 column 6 path $"
    -          + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
    -    }
    -    try {
    -      strategy.readNumber(fromString("\"not-a-number\""));
    -      fail();
    -    } catch (NumberFormatException expected) {
    -    }
    +
    +    MalformedJsonException e = assertThrows(MalformedJsonException.class, () -> strategy.readNumber(fromString("1e400")));
    +    assertThat(e).hasMessageThat().isEqualTo(
    +        "JSON forbids NaN and infinities: Infinity at line 1 column 6 path $"
    +        + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json"
    +    );
    +
    +    assertThrows(NumberFormatException.class, () -> strategy.readNumber(fromString("\"not-a-number\"")));
       }
     
       @Test
    @@ -62,46 +57,31 @@ public void testLongOrDouble() throws IOException {
         assertThat(strategy.readNumber(fromString("10"))).isEqualTo(10L);
         assertThat(strategy.readNumber(fromString("10.1"))).isEqualTo(10.1);
         assertThat(strategy.readNumber(fromString("3.141592653589793238462643383279"))).isEqualTo(3.141592653589793D);
    -    try {
    -      strategy.readNumber(fromString("1e400"));
    -      fail();
    -    } catch (MalformedJsonException expected) {
    -      assertThat(expected).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: Infinity; at path $");
    -    }
    -    try {
    -      strategy.readNumber(fromString("\"not-a-number\""));
    -      fail();
    -    } catch (JsonParseException expected) {
    -      assertThat(expected).hasMessageThat().isEqualTo("Cannot parse not-a-number; at path $");
    -    }
    +
    +    Exception e = assertThrows(MalformedJsonException.class, () -> strategy.readNumber(fromString("1e400")));
    +    assertThat(e).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: Infinity; at path $");
    +
    +    e = assertThrows(JsonParseException.class, () -> strategy.readNumber(fromString("\"not-a-number\"")));
    +    assertThat(e).hasMessageThat().isEqualTo("Cannot parse not-a-number; at path $");
     
         assertThat(strategy.readNumber(fromStringLenient("NaN"))).isEqualTo(Double.NaN);
         assertThat(strategy.readNumber(fromStringLenient("Infinity"))).isEqualTo(Double.POSITIVE_INFINITY);
         assertThat(strategy.readNumber(fromStringLenient("-Infinity"))).isEqualTo(Double.NEGATIVE_INFINITY);
    -    try {
    -      strategy.readNumber(fromString("NaN"));
    -      fail();
    -    } catch (MalformedJsonException expected) {
    -      assertThat(expected).hasMessageThat().isEqualTo(
    -          "Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $"
    -          + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
    -    }
    -    try {
    -      strategy.readNumber(fromString("Infinity"));
    -      fail();
    -    } catch (MalformedJsonException expected) {
    -      assertThat(expected).hasMessageThat().isEqualTo(
    -          "Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $"
    -          + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
    -    }
    -    try {
    -      strategy.readNumber(fromString("-Infinity"));
    -      fail();
    -    } catch (MalformedJsonException expected) {
    -      assertThat(expected).hasMessageThat().isEqualTo(
    -          "Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $"
    -          + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
    -    }
    +
    +    e = assertThrows(MalformedJsonException.class, () -> strategy.readNumber(fromString("NaN")));
    +    assertThat(e).hasMessageThat().isEqualTo(
    +        "Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $"
    +        + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
    +
    +    e = assertThrows(MalformedJsonException.class, () -> strategy.readNumber(fromString("Infinity")));
    +    assertThat(e).hasMessageThat().isEqualTo(
    +        "Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $"
    +        + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
    +
    +    e = assertThrows(MalformedJsonException.class, () -> strategy.readNumber(fromString("-Infinity")));
    +    assertThat(e).hasMessageThat().isEqualTo(
    +        "Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $"
    +        + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
       }
     
       @Test
    @@ -111,44 +91,27 @@ public void testBigDecimal() throws IOException {
         assertThat(strategy.readNumber(fromString("3.141592653589793238462643383279"))).isEqualTo(new BigDecimal("3.141592653589793238462643383279"));
         assertThat(strategy.readNumber(fromString("1e400"))).isEqualTo(new BigDecimal("1e400"));
     
    -    try {
    -      strategy.readNumber(fromString("\"not-a-number\""));
    -      fail();
    -    } catch (JsonParseException expected) {
    -      assertThat(expected).hasMessageThat().isEqualTo("Cannot parse not-a-number; at path $");
    -    }
    +    JsonParseException e = assertThrows(JsonParseException.class, () -> strategy.readNumber(fromString("\"not-a-number\"")));
    +    assertThat(e).hasMessageThat().isEqualTo("Cannot parse not-a-number; at path $");
       }
     
       @Test
       public void testNullsAreNeverExpected() throws IOException {
    -    try {
    -      ToNumberPolicy.DOUBLE.readNumber(fromString("null"));
    -      fail();
    -    } catch (IllegalStateException expected) {
    -      assertThat(expected).hasMessageThat().isEqualTo("Expected a double but was NULL at line 1 column 5 path $"
    -          + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe");
    -    }
    -    try {
    -      ToNumberPolicy.LAZILY_PARSED_NUMBER.readNumber(fromString("null"));
    -      fail();
    -    } catch (IllegalStateException expected) {
    -      assertThat(expected).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $"
    -          + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe");
    -    }
    -    try {
    -      ToNumberPolicy.LONG_OR_DOUBLE.readNumber(fromString("null"));
    -      fail();
    -    } catch (IllegalStateException expected) {
    -      assertThat(expected).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $"
    -          + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe");
    -    }
    -    try {
    -      ToNumberPolicy.BIG_DECIMAL.readNumber(fromString("null"));
    -      fail();
    -    } catch (IllegalStateException expected) {
    -      assertThat(expected).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $"
    -          + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe");
    -    }
    +    IllegalStateException e = assertThrows(IllegalStateException.class, () -> ToNumberPolicy.DOUBLE.readNumber(fromString("null")));
    +    assertThat(e).hasMessageThat().isEqualTo("Expected a double but was NULL at line 1 column 5 path $"
    +        + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe");
    +
    +    e = assertThrows(IllegalStateException.class, () -> ToNumberPolicy.LAZILY_PARSED_NUMBER.readNumber(fromString("null")));
    +    assertThat(e).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $"
    +        + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe");
    +
    +    e = assertThrows(IllegalStateException.class, () -> ToNumberPolicy.LONG_OR_DOUBLE.readNumber(fromString("null")));
    +    assertThat(e).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $"
    +        + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe");
    +
    +    e = assertThrows(IllegalStateException.class, () -> ToNumberPolicy.BIG_DECIMAL.readNumber(fromString("null")));
    +    assertThat(e).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $"
    +        + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe");
       }
     
       private static JsonReader fromString(String json) {
    diff --git a/gson/src/test/java/com/google/gson/TypeAdapterTest.java b/gson/src/test/java/com/google/gson/TypeAdapterTest.java
    index 18af4c4bfa..ea3042eb08 100644
    --- a/gson/src/test/java/com/google/gson/TypeAdapterTest.java
    +++ b/gson/src/test/java/com/google/gson/TypeAdapterTest.java
    @@ -17,7 +17,7 @@
     package com.google.gson;
     
     import static com.google.common.truth.Truth.assertThat;
    -import static org.junit.Assert.fail;
    +import static org.junit.Assert.assertThrows;
     
     import com.google.gson.stream.JsonReader;
     import com.google.gson.stream.JsonWriter;
    @@ -59,19 +59,11 @@ public void testToJson_ThrowingIOException() {
           }
         };
     
    -    try {
    -      adapter.toJson(1);
    -      fail();
    -    } catch (JsonIOException e) {
    -      assertThat(e.getCause()).isEqualTo(exception);
    -    }
    +    JsonIOException e = assertThrows(JsonIOException.class, () -> adapter.toJson(1));
    +    assertThat(e).hasCauseThat().isEqualTo(exception);
     
    -    try {
    -      adapter.toJsonTree(1);
    -      fail();
    -    } catch (JsonIOException e) {
    -      assertThat(e.getCause()).isEqualTo(exception);
    -    }
    +    e = assertThrows(JsonIOException.class, () -> adapter.toJsonTree(1));
    +    assertThat(e).hasCauseThat().isEqualTo(exception);
       }
     
       private static final TypeAdapter adapter = new TypeAdapter() {
    
    From b3dce2dc17086c72bd1f8ccf508874849f9631bb Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?=C3=89amonn=20McManus?= 
    Date: Sun, 30 Jul 2023 09:20:32 -0700
    Subject: [PATCH 45/95] Strict mode for JSON parsing, contributed by
     @marten-voorberg. (#2437)
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    * Strict mode for JSON parsing (#2323)
    
    * Feat #6: Add strict flag to Gson and GsonBuilder
    
    * Test #2: Add failing tests for capitalized keywords
    
    * Feat #2: JsonReader does not read (partially) capitalized keywords if strict mode is used
    
    * Feat #3: Added implementation and tests for JSONReader not accepting specific escape sequence representing in strict mode
    
    * Test #3: Simplify test cases by removing unnecessary array
    
    * Feat #3: Improve error by including the illegal character
    
    * Feat #5: JsonReader does not allow unespaced control flow characters in strict mode
    
    * Test #5: Test unespaced control flow characters in strict mode
    
    * Feat #4: Disallow espaced newline character in strict mode
    
    * Test #4: Add tests for (dis)allowing newline character depensding on strictness
    
    * Test #5: Test case for unescaped control char in non-strict mode
    
    * Test #2: Simplify test cases
    
    * Feat #13: Change leniency API to Strictness enum in JsonReader, Gson, and GsonBuilder
    
    * Feat #15: Change JsonWriter API to also use Strictness
    
    * Test #15: Test Strictness in JsonWriter API
    
    * Doc #15: Add and update documentation for Strictness in JsonWriter API
    
    * refactor #12: Fixed typos and empty catch brackets in tests
    
    * refactor #12: Resolved importing wildcards, made some lines adhere to Google java style
    
    * #5 Add test case for unescaped control characters
    
    * Feat #5: add new lines to make JsonReader able to detect unescaped control characters (U+0000 through U+001F) and throw exceptions.
    
    * Feat #5: add new lines to make JsonReader able to detect unescaped control characters (U+0000 through U+001F) and throw exceptions.
    
    * Test #11: Added two tests for testing implementation of control character handling in strict mode and moved the implementation to nextQuotedValue
    
    * Test #11: Added two tests for testing implementation of control character handling in strict mode and moved the implementation to nextQuotedValue
    
    ---------
    
    Co-authored-by: LMC117 <2295699210@qq.com>
    Co-authored-by: Marten Voorberg 
    
    * Doc #17: Add and change javadoc of public methods
    
    * Doc #17: Update JavaDoc in JsonReader and Strictness
    
    * Doc #17: Update JavaDoc in Gson and GsonBuilder
    
    * Test #34: Add tests for setting strictness through GsonBuilder
    
    * Fix: Add Fix broken test
    
    * Fix: Invalid JavaDoc in Gson.java
    
    * Doc #17: update outdated javadoc
    
    * #37: Resolve more PR feedback
    
    * Fix #37: Resolve various PR comments
    
    * Fix #37: Resolve various PR comments
    
    * Refactor #35: Refactor JsonReader#peekKeyword to reduce the amount of strictness checks (#39)
    
    * Doc #40: Update JavaDoc based on PR feedback
    
    * Doc #40: Update old RFC in GsonBuilder documentation
    
    * Doc #40: Fix formatting error in JavaDoc
    
    * Doc #40: Add tests for setting strictness and lenient to JsonReaderTest
    
    * Test #43: Changed tests to make use of assertThrows
    
    * test #43: Changed tests to make use of assertThrows as per feedback
    
    * Test #43: Update JsonWriterTest#testStrictnessNull to use assertThrows
    
    * Test #43: Update JsonWriterTest#testStrictnessNull to use assertThrows
    
    * test #43: Resolve PR recommendations
    
    * Test #43: Mini change to TC
    
    * Test #43: Mini change to TC
    
    ---------
    
    Co-authored-by: Marten Voorberg 
    
    * doc #46: Resolved comments in main PR
    
    * Feat #45: Change Gson.fromJson and Gson.toJson to be strict when the provided writer/reader is strict
    
    * Fix #45: Small type
    
    * Update gson/src/test/java/com/google/gson/stream/JsonReaderTest.java
    
    Co-authored-by: Marcono1234 
    
    * Fix #45: Resolve various comments by Marcono1234
    
    * Update gson/src/main/java/com/google/gson/GsonBuilder.java
    
    Co-authored-by: Marcono1234 
    
    * Fix #45: Resolve various comments by Marcono1234
    
    * Fix #45: Resolve various comments by eamonmcmanus
    
    * Strictness mode follow-up
    
    * Update Troubleshooting.md and Gson default lenient mode documentation
    
    * Always use GSON strictness when set.
    
    * Rename Strictness.DEFAULT to Strictness.LEGACY_STRICT
    
    * Update JavaDoc with new strictness functionality
    
    * Replace default with legacy strict for JsonReader javadoc
    
    * Add JSONReader test cases for U2028 and U2029
    
    * Refactor JSONReader#peekKeyWord() based on @eamonmcmanus's suggestion
    
    * Deprecate setLenient in favor of setStrictness
    
    ---------
    
    Co-authored-by: Carl Peterson 
    Co-authored-by: Gustaf Johansson 
    Co-authored-by: gustajoh <58432871+gustajoh@users.noreply.github.com>
    Co-authored-by: LMC117 <2295699210@qq.com>
    Co-authored-by: Marcono1234 
    
    * Strictness follow-up (#2408)
    
    * Strictness mode follow-up
    
    - Remove mentions of `null` Gson strictness; this is an implementation detail
    - Fix incorrect / outdated documentation
    - Reduce links to RFC; if there is already a link to it in a previous sentence
      don't link to it again
    - Extend and update tests
    - Minor punctuation changes in documentation for consistency
    
    * Deprecate `setLenient` methods
    
    * `strictness2` fixes & improvements (#2456)
    
    * Adjust ProGuard default rules and shrinking tests (#2420)
    
    * Adjust ProGuard default rules and shrinking tests
    
    * Adjust comment
    
    * Add shrinking test for class without no-args constructor; improve docs
    
    * Improve Unsafe mention in Troubleshooting Guide
    
    * Improve comment for `-if class *`
    
    * Bump com.google.guava:guava from 32.0.1-jre to 32.1.1-jre (#2444)
    
    Bumps [com.google.guava:guava](https://github.com/google/guava) from 32.0.1-jre to 32.1.1-jre.
    - [Release notes](https://github.com/google/guava/releases)
    - [Commits](https://github.com/google/guava/commits)
    
    ---
    updated-dependencies:
    - dependency-name: com.google.guava:guava
      dependency-type: direct:production
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] 
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    
    * Bump com.google.guava:guava-testlib from 32.0.1-jre to 32.1.1-jre (#2443)
    
    Bumps [com.google.guava:guava-testlib](https://github.com/google/guava) from 32.0.1-jre to 32.1.1-jre.
    - [Release notes](https://github.com/google/guava/releases)
    - [Commits](https://github.com/google/guava/commits)
    
    ---
    updated-dependencies:
    - dependency-name: com.google.guava:guava-testlib
      dependency-type: direct:development
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] 
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    
    * Support non-generic type for `TypeToken.getParameterized` for legacy reasons (#2447)
    
    This partially restores the behavior before a589ef20087b4b0f1ec3048d3ceaef1eedccd09d,
    except that back then for a non-generic type a bogus `TypeToken(ParameterizedType)`
    was created, whereas now a `TypeToken(Class)` is created instead.
    
    * Fixed Typo in GsonBuilder.java (#2449)
    
    * Make date-formatting tests less fragile with regular expressions. (#2450)
    
    * Make date-formatting tests less fragile with regular expressions.
    
    This is not great. We should really ensure that formatted dates are the same
    regardless of JDK version. There is code that attempts to do that but it is not
    really effective. So for now we fudge around the differences by using regular
    expressions to paper over the differences.
    
    * Temporarily add test-debugging code.
    
    * Another attempt at debugging a test failure.
    
    * Fix pattern in assertion.
    
    * Modification in test cases (#2454)
    
    * Fixed Typo in GsonBuilder.java
    
    * Suggestions on Test cases
    
    * Modified test cases using assertThrows method (JUnit)
    
    * Update gson/src/test/java/com/google/gson/JsonArrayAsListTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/GsonTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/JsonArrayAsListTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/JsonStreamParserTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/JsonStreamParserTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/JsonStreamParserTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/ToNumberPolicyTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/TypeAdapterTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/TypeAdapterTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/ToNumberPolicyTest.java
    
    Co-authored-by: Marcono1234 
    
    * Update gson/src/test/java/com/google/gson/ToNumberPolicyTest.java
    
    Co-authored-by: Marcono1234 
    
    ---------
    
    Co-authored-by: Marcono1234 
    
    * Minor follow-up changes
    
    ---------
    
    Signed-off-by: dependabot[bot] 
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    Co-authored-by: elevne <97422844+elevne@users.noreply.github.com>
    Co-authored-by: Éamonn McManus 
    Co-authored-by: Wonil 
    
    ---------
    
    Signed-off-by: dependabot[bot] 
    Co-authored-by: Marten 
    Co-authored-by: Gustaf Johansson 
    Co-authored-by: gustajoh <58432871+gustajoh@users.noreply.github.com>
    Co-authored-by: LMC117 <2295699210@qq.com>
    Co-authored-by: Marcono1234 
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    Co-authored-by: elevne <97422844+elevne@users.noreply.github.com>
    Co-authored-by: Wonil 
    ---
     Troubleshooting.md                            |  11 +-
     gson/src/main/java/com/google/gson/Gson.java  | 127 +++++---
     .../java/com/google/gson/GsonBuilder.java     |  57 +++-
     .../java/com/google/gson/JsonElement.java     |   3 +-
     .../main/java/com/google/gson/JsonParser.java |  14 +-
     .../com/google/gson/JsonStreamParser.java     |   4 +-
     .../main/java/com/google/gson/Strictness.java |  29 ++
     .../java/com/google/gson/TypeAdapter.java     |  33 +-
     .../com/google/gson/stream/JsonReader.java    | 163 +++++++---
     .../com/google/gson/stream/JsonWriter.java    |  93 ++++--
     .../gson/stream/MalformedJsonException.java   |   3 +-
     .../java/com/google/gson/GsonBuilderTest.java |  30 ++
     .../test/java/com/google/gson/GsonTest.java   |  11 +-
     .../java/com/google/gson/JsonParserTest.java  |  14 +-
     .../java/com/google/gson/MixedStreamTest.java |   2 +
     .../com/google/gson/ToNumberPolicyTest.java   |   8 +-
     .../com/google/gson/functional/ArrayTest.java |   1 -
     .../google/gson/functional/LeniencyTest.java  |   1 +
     .../gson/functional/PrettyPrintingTest.java   |   3 +-
     .../gson/functional/UncategorizedTest.java    |   1 -
     .../internal/bind/JsonElementReaderTest.java  |   5 +-
     .../internal/bind/JsonTreeReaderTest.java     |   2 +-
     .../internal/bind/JsonTreeWriterTest.java     |  13 +-
     .../gson/stream/JsonReaderPathTest.java       |   3 +-
     .../google/gson/stream/JsonReaderTest.java    | 301 ++++++++++++++----
     .../google/gson/stream/JsonWriterTest.java    | 265 +++++++++------
     26 files changed, 864 insertions(+), 333 deletions(-)
     create mode 100644 gson/src/main/java/com/google/gson/Strictness.java
    
    diff --git a/Troubleshooting.md b/Troubleshooting.md
    index 184f19166e..6bf2b8579a 100644
    --- a/Troubleshooting.md
    +++ b/Troubleshooting.md
    @@ -127,7 +127,7 @@ For example, let's assume you want to deserialize the following JSON data:
     }
     ```
     
    -This will fail with an exception similar to this one: `MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 5 column 4 path $.languages[2]`  
    +This will fail with an exception similar to this one: `MalformedJsonException: Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON at line 5 column 4 path $.languages[2]`  
     The problem here is the trailing comma (`,`) after `"French"`, trailing commas are not allowed by the JSON specification. The location information "line 5 column 4" points to the `]` in the JSON data (with some slight inaccuracies) because Gson expected another value after `,` instead of the closing `]`. The JSONPath `$.languages[2]` in the exception message also points there: `$.` refers to the root object, `languages` refers to its member of that name and `[2]` refers to the (missing) third value in the JSON array value of that member (numbering starts at 0, so it is `[2]` instead of `[3]`).  
     The proper solution here is to fix the malformed JSON data.
     
    @@ -147,9 +147,12 @@ To spot syntax errors in the JSON data easily you can open it in an editor with
     
     **Reason:** Due to legacy reasons Gson performs parsing by default in lenient mode
     
    -**Solution:** See [`Gson` class documentation](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/Gson.html#default-lenient) section "Lenient JSON handling"
    -
    -Note: Even in non-lenient mode Gson deviates slightly from the JSON specification, see [`JsonReader.setLenient`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/stream/JsonReader.html#setLenient(boolean)) for more details.
    +**Solution:** If you are using Gson 2.11.0 or newer, call [`GsonBuilder.setStrictness`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#setStrictness(com.google.gson.Strictness)),
    +[`JsonReader.setStrictness`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/stream/JsonReader.html#setStrictness(com.google.gson.Strictness))
    +and [`JsonWriter.setStrictness`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/stream/JsonWriter.html#setStrictness(com.google.gson.Strictness))
    +with `Strictness.STRICT` to overwrite the default lenient behavior of `Gson` and make these classes strictly adhere to the JSON specification.
    +Otherwise if you are using an older Gson version, see the [`Gson` class documentation](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/Gson.html#default-lenient)
    +section "JSON Strictness handling" for alternative solutions.
     
     ##  `IllegalStateException`: "Expected ... but was ..."
     
    diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java
    index c6f8508ef1..0219c1a186 100644
    --- a/gson/src/main/java/com/google/gson/Gson.java
    +++ b/gson/src/main/java/com/google/gson/Gson.java
    @@ -105,10 +105,14 @@
      * 

    See the Gson User Guide * for a more complete set of examples.

    * - *

    Lenient JSON handling

    + *

    JSON Strictness handling

    * For legacy reasons most of the {@code Gson} methods allow JSON data which does not - * comply with the JSON specification, regardless of whether {@link GsonBuilder#setLenient()} - * is used or not. If this behavior is not desired, the following workarounds can be used: + * comply with the JSON specification when no explicit {@linkplain Strictness strictness} is set (the default). + * To specify the strictness of a {@code Gson} instance, you should set it through + * {@link GsonBuilder#setStrictness(Strictness)}. + * + *

    For older Gson versions, which don't have the strictness mode API, the following + * workarounds can be used: * *

    Serialization

    *
      @@ -132,6 +136,10 @@ * to make sure there is no trailing data *
    * + * Note that the {@code JsonReader} created this way is only 'legacy strict', it mostly adheres + * to the JSON specification but allows small deviations. See {@link JsonReader#setStrictness(Strictness)} + * for details. + * * @see TypeToken * * @author Inderjeet Singh @@ -140,7 +148,8 @@ */ public final class Gson { static final boolean DEFAULT_JSON_NON_EXECUTABLE = false; - static final boolean DEFAULT_LENIENT = false; + // Strictness of `null` is the legacy mode where some Gson APIs are always lenient + static final Strictness DEFAULT_STRICTNESS = null; static final FormattingStyle DEFAULT_FORMATTING_STYLE = FormattingStyle.COMPACT; static final boolean DEFAULT_ESCAPE_HTML = true; static final boolean DEFAULT_SERIALIZE_NULLS = false; @@ -184,7 +193,7 @@ public final class Gson { final boolean generateNonExecutableJson; final boolean htmlSafe; final FormattingStyle formattingStyle; - final boolean lenient; + final Strictness strictness; final boolean serializeSpecialFloatingPointValues; final boolean useJdkUnsafe; final String datePattern; @@ -231,13 +240,15 @@ public final class Gson { *
  • By default, Gson excludes transient or static fields from * consideration for serialization and deserialization. You can change this behavior through * {@link GsonBuilder#excludeFieldsWithModifiers(int...)}.
  • + *
  • No explicit strictness is set. You can change this by calling + * {@link GsonBuilder#setStrictness(Strictness)}.
  • * */ public Gson() { this(Excluder.DEFAULT, DEFAULT_FIELD_NAMING_STRATEGY, Collections.>emptyMap(), DEFAULT_SERIALIZE_NULLS, DEFAULT_COMPLEX_MAP_KEYS, DEFAULT_JSON_NON_EXECUTABLE, DEFAULT_ESCAPE_HTML, - DEFAULT_FORMATTING_STYLE, DEFAULT_LENIENT, DEFAULT_SPECIALIZE_FLOAT_VALUES, + DEFAULT_FORMATTING_STYLE, DEFAULT_STRICTNESS, DEFAULT_SPECIALIZE_FLOAT_VALUES, DEFAULT_USE_JDK_UNSAFE, LongSerializationPolicy.DEFAULT, DEFAULT_DATE_PATTERN, DateFormat.DEFAULT, DateFormat.DEFAULT, Collections.emptyList(), Collections.emptyList(), @@ -248,7 +259,7 @@ public Gson() { Gson(Excluder excluder, FieldNamingStrategy fieldNamingStrategy, Map> instanceCreators, boolean serializeNulls, boolean complexMapKeySerialization, boolean generateNonExecutableGson, boolean htmlSafe, - FormattingStyle formattingStyle, boolean lenient, boolean serializeSpecialFloatingPointValues, + FormattingStyle formattingStyle, Strictness strictness, boolean serializeSpecialFloatingPointValues, boolean useJdkUnsafe, LongSerializationPolicy longSerializationPolicy, String datePattern, int dateStyle, int timeStyle, List builderFactories, @@ -265,7 +276,7 @@ public Gson() { this.generateNonExecutableJson = generateNonExecutableGson; this.htmlSafe = htmlSafe; this.formattingStyle = formattingStyle; - this.lenient = lenient; + this.strictness = strictness; this.serializeSpecialFloatingPointValues = serializeSpecialFloatingPointValues; this.useJdkUnsafe = useJdkUnsafe; this.longSerializationPolicy = longSerializationPolicy; @@ -802,7 +813,7 @@ public void toJson(Object src, Appendable writer) throws JsonIOException { *
        * Type typeOfSrc = new TypeToken<Collection<Foo>>(){}.getType();
        * 
    - * @param writer Writer to which the JSON representation of src needs to be written. + * @param writer Writer to which the JSON representation of src needs to be written * @throws JsonIOException if there was a problem writing to the writer * @since 1.2 * @@ -822,24 +833,38 @@ public void toJson(Object src, Type typeOfSrc, Appendable writer) throws JsonIOE * Writes the JSON representation of {@code src} of type {@code typeOfSrc} to * {@code writer}. * - *

    The JSON data is written in {@linkplain JsonWriter#setLenient(boolean) lenient mode}, - * regardless of the lenient mode setting of the provided writer. The lenient mode setting - * of the writer is restored once this method returns. + *

    If the {@code Gson} instance has an {@linkplain GsonBuilder#setStrictness(Strictness) explicit strictness setting}, + * this setting will be used for writing the JSON regardless of the {@linkplain JsonWriter#getStrictness() strictness} + * of the provided {@link JsonWriter}. For legacy reasons, if the {@code Gson} instance has no explicit strictness setting + * and the writer does not have the strictness {@link Strictness#STRICT}, the JSON will be written in {@link Strictness#LENIENT} + * mode.
    + * Note that in all cases the old strictness setting of the writer will be restored when this method returns. * *

    The 'HTML-safe' and 'serialize {@code null}' settings of this {@code Gson} instance * (configured by the {@link GsonBuilder}) are applied, and the original settings of the * writer are restored once this method returns. * + * @param src the object for which JSON representation is to be created + * @param typeOfSrc the type of the object to be written + * @param writer Writer to which the JSON representation of src needs to be written + * * @throws JsonIOException if there was a problem writing to the writer */ public void toJson(Object src, Type typeOfSrc, JsonWriter writer) throws JsonIOException { @SuppressWarnings("unchecked") TypeAdapter adapter = (TypeAdapter) getAdapter(TypeToken.get(typeOfSrc)); - boolean oldLenient = writer.isLenient(); - writer.setLenient(true); + + Strictness oldStrictness = writer.getStrictness(); + if (this.strictness != null) { + writer.setStrictness(this.strictness); + } else if (writer.getStrictness() != Strictness.STRICT) { + writer.setStrictness(Strictness.LENIENT); + } + boolean oldHtmlSafe = writer.isHtmlSafe(); - writer.setHtmlSafe(htmlSafe); boolean oldSerializeNulls = writer.getSerializeNulls(); + + writer.setHtmlSafe(htmlSafe); writer.setSerializeNulls(serializeNulls); try { adapter.write(writer, src); @@ -848,7 +873,7 @@ public void toJson(Object src, Type typeOfSrc, JsonWriter writer) throws JsonIOE } catch (AssertionError e) { throw new AssertionError("AssertionError (GSON " + GsonBuildConfig.VERSION + "): " + e.getMessage(), e); } finally { - writer.setLenient(oldLenient); + writer.setStrictness(oldStrictness); writer.setHtmlSafe(oldHtmlSafe); writer.setSerializeNulls(oldSerializeNulls); } @@ -892,7 +917,10 @@ public void toJson(JsonElement jsonElement, Appendable writer) throws JsonIOExce *
  • {@link GsonBuilder#disableHtmlEscaping()}
  • *
  • {@link GsonBuilder#generateNonExecutableJson()}
  • *
  • {@link GsonBuilder#serializeNulls()}
  • - *
  • {@link GsonBuilder#setLenient()}
  • + *
  • {@link GsonBuilder#setStrictness(Strictness)}. If no + * {@linkplain GsonBuilder#setStrictness(Strictness) explicit strictness has been set} the created + * writer will have a strictness of {@link Strictness#LEGACY_STRICT}. Otherwise, the strictness of + * the {@code Gson} instance will be used for the created writer.
  • *
  • {@link GsonBuilder#setPrettyPrinting()}
  • *
  • {@link GsonBuilder#setFormattingStyle(FormattingStyle)}
  • * @@ -904,7 +932,7 @@ public JsonWriter newJsonWriter(Writer writer) throws IOException { JsonWriter jsonWriter = new JsonWriter(writer); jsonWriter.setFormattingStyle(formattingStyle); jsonWriter.setHtmlSafe(htmlSafe); - jsonWriter.setLenient(lenient); + jsonWriter.setStrictness(strictness == null ? Strictness.LEGACY_STRICT : strictness); jsonWriter.setSerializeNulls(serializeNulls); return jsonWriter; } @@ -914,35 +942,50 @@ public JsonWriter newJsonWriter(Writer writer) throws IOException { * *

    The following settings are considered: *

      - *
    • {@link GsonBuilder#setLenient()}
    • + *
    • {@link GsonBuilder#setStrictness(Strictness)}. If no + * {@linkplain GsonBuilder#setStrictness(Strictness) explicit strictness has been set} the created + * reader will have a strictness of {@link Strictness#LEGACY_STRICT}. Otherwise, the strictness of + * the {@code Gson} instance will be used for the created reader.
    • *
    */ public JsonReader newJsonReader(Reader reader) { JsonReader jsonReader = new JsonReader(reader); - jsonReader.setLenient(lenient); + jsonReader.setStrictness(strictness == null ? Strictness.LEGACY_STRICT : strictness); return jsonReader; } /** * Writes the JSON for {@code jsonElement} to {@code writer}. * - *

    The JSON data is written in {@linkplain JsonWriter#setLenient(boolean) lenient mode}, - * regardless of the lenient mode setting of the provided writer. The lenient mode setting - * of the writer is restored once this method returns. + *

    If the {@code Gson} instance has an {@linkplain GsonBuilder#setStrictness(Strictness) explicit strictness setting}, + * this setting will be used for writing the JSON regardless of the {@linkplain JsonWriter#getStrictness() strictness} + * of the provided {@link JsonWriter}. For legacy reasons, if the {@code Gson} instance has no explicit strictness setting + * and the writer does not have the strictness {@link Strictness#STRICT}, the JSON will be written in {@link Strictness#LENIENT} + * mode.
    + * Note that in all cases the old strictness setting of the writer will be restored when this method returns. * *

    The 'HTML-safe' and 'serialize {@code null}' settings of this {@code Gson} instance * (configured by the {@link GsonBuilder}) are applied, and the original settings of the * writer are restored once this method returns. * + * @param jsonElement the JSON element to be written + * @param writer the JSON writer to which the provided element will be written * @throws JsonIOException if there was a problem writing to the writer */ public void toJson(JsonElement jsonElement, JsonWriter writer) throws JsonIOException { - boolean oldLenient = writer.isLenient(); - writer.setLenient(true); + Strictness oldStrictness = writer.getStrictness(); boolean oldHtmlSafe = writer.isHtmlSafe(); - writer.setHtmlSafe(htmlSafe); boolean oldSerializeNulls = writer.getSerializeNulls(); + + writer.setHtmlSafe(htmlSafe); writer.setSerializeNulls(serializeNulls); + + if (this.strictness != null) { + writer.setStrictness(this.strictness); + } else if (writer.getStrictness() != Strictness.STRICT) { + writer.setStrictness(Strictness.LENIENT); + } + try { Streams.write(jsonElement, writer); } catch (IOException e) { @@ -950,7 +993,7 @@ public void toJson(JsonElement jsonElement, JsonWriter writer) throws JsonIOExce } catch (AssertionError e) { throw new AssertionError("AssertionError (GSON " + GsonBuildConfig.VERSION + "): " + e.getMessage(), e); } finally { - writer.setLenient(oldLenient); + writer.setStrictness(oldStrictness); writer.setHtmlSafe(oldHtmlSafe); writer.setSerializeNulls(oldSerializeNulls); } @@ -1169,9 +1212,12 @@ private static void assertFullConsumption(Object obj, JsonReader reader) { *

    Unlike the other {@code fromJson} methods, no exception is thrown if the JSON data has * multiple top-level JSON elements, or if there is trailing data. * - *

    The JSON data is parsed in {@linkplain JsonReader#setLenient(boolean) lenient mode}, - * regardless of the lenient mode setting of the provided reader. The lenient mode setting - * of the reader is restored once this method returns. + *

    If the {@code Gson} instance has an {@linkplain GsonBuilder#setStrictness(Strictness) explicit strictness setting}, + * this setting will be used for reading the JSON regardless of the {@linkplain JsonReader#getStrictness() strictness} + * of the provided {@link JsonReader}. For legacy reasons, if the {@code Gson} instance has no explicit strictness setting + * and the reader does not have the strictness {@link Strictness#STRICT}, the JSON will be written in {@link Strictness#LENIENT} + * mode.
    + * Note that in all cases the old strictness setting of the reader will be restored when this method returns. * * @param the type of the desired object * @param reader the reader whose next JSON value should be deserialized @@ -1198,9 +1244,12 @@ public T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, J *

    Unlike the other {@code fromJson} methods, no exception is thrown if the JSON data has * multiple top-level JSON elements, or if there is trailing data. * - *

    The JSON data is parsed in {@linkplain JsonReader#setLenient(boolean) lenient mode}, - * regardless of the lenient mode setting of the provided reader. The lenient mode setting - * of the reader is restored once this method returns. + *

    If the {@code Gson} instance has an {@linkplain GsonBuilder#setStrictness(Strictness) explicit strictness setting}, + * this setting will be used for reading the JSON regardless of the {@linkplain JsonReader#getStrictness() strictness} + * of the provided {@link JsonReader}. For legacy reasons, if the {@code Gson} instance has no explicit strictness setting + * and the reader does not have the strictness {@link Strictness#STRICT}, the JSON will be written in {@link Strictness#LENIENT} + * mode.
    + * Note that in all cases the old strictness setting of the reader will be restored when this method returns. * * @param the type of the desired object * @param reader the reader whose next JSON value should be deserialized @@ -1220,8 +1269,14 @@ public T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, J */ public T fromJson(JsonReader reader, TypeToken typeOfT) throws JsonIOException, JsonSyntaxException { boolean isEmpty = true; - boolean oldLenient = reader.isLenient(); - reader.setLenient(true); + Strictness oldStrictness = reader.getStrictness(); + + if (this.strictness != null) { + reader.setStrictness(this.strictness); + } else if (reader.getStrictness() != Strictness.STRICT) { + reader.setStrictness(Strictness.LENIENT); + } + try { JsonToken unused = reader.peek(); isEmpty = false; @@ -1244,7 +1299,7 @@ public T fromJson(JsonReader reader, TypeToken typeOfT) throws JsonIOExce } catch (AssertionError e) { throw new AssertionError("AssertionError (GSON " + GsonBuildConfig.VERSION + "): " + e.getMessage(), e); } finally { - reader.setLenient(oldLenient); + reader.setStrictness(oldStrictness); } } diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java index c72c411f07..68eb7d718b 100644 --- a/gson/src/main/java/com/google/gson/GsonBuilder.java +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java @@ -21,14 +21,15 @@ import static com.google.gson.Gson.DEFAULT_ESCAPE_HTML; import static com.google.gson.Gson.DEFAULT_FORMATTING_STYLE; import static com.google.gson.Gson.DEFAULT_JSON_NON_EXECUTABLE; -import static com.google.gson.Gson.DEFAULT_LENIENT; import static com.google.gson.Gson.DEFAULT_NUMBER_TO_NUMBER_STRATEGY; import static com.google.gson.Gson.DEFAULT_OBJECT_TO_NUMBER_STRATEGY; import static com.google.gson.Gson.DEFAULT_SERIALIZE_NULLS; import static com.google.gson.Gson.DEFAULT_SPECIALIZE_FLOAT_VALUES; +import static com.google.gson.Gson.DEFAULT_STRICTNESS; import static com.google.gson.Gson.DEFAULT_USE_JDK_UNSAFE; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.InlineMe; import com.google.gson.annotations.Since; import com.google.gson.annotations.Until; import com.google.gson.internal.$Gson$Preconditions; @@ -71,12 +72,16 @@ * .create(); * * - *

    NOTES: + *

    Notes: *

      - *
    • the order of invocation of configuration methods does not matter.
    • - *
    • The default serialization of {@link Date} and its subclasses in Gson does + *
    • The order of invocation of configuration methods does not matter.
    • + *
    • The default serialization of {@link Date} and its subclasses in Gson does * not contain time-zone information. So, if you are using date/time instances, * use {@code GsonBuilder} and its {@code setDateFormat} methods.
    • + *
    • By default no explicit {@link Strictness} is set; some of the {@link Gson} methods + * behave as if {@link Strictness#LEGACY_STRICT} was used whereas others behave as + * if {@link Strictness#LENIENT} was used. Prefer explicitly setting a strictness + * with {@link #setStrictness(Strictness)} to avoid this legacy behavior. *
    * * @author Inderjeet Singh @@ -100,7 +105,7 @@ public final class GsonBuilder { private boolean escapeHtmlChars = DEFAULT_ESCAPE_HTML; private FormattingStyle formattingStyle = DEFAULT_FORMATTING_STYLE; private boolean generateNonExecutableJson = DEFAULT_JSON_NON_EXECUTABLE; - private boolean lenient = DEFAULT_LENIENT; + private Strictness strictness = DEFAULT_STRICTNESS; private boolean useJdkUnsafe = DEFAULT_USE_JDK_UNSAFE; private ToNumberStrategy objectToNumberStrategy = DEFAULT_OBJECT_TO_NUMBER_STRATEGY; private ToNumberStrategy numberToNumberStrategy = DEFAULT_NUMBER_TO_NUMBER_STRATEGY; @@ -130,7 +135,7 @@ public GsonBuilder() { this.generateNonExecutableJson = gson.generateNonExecutableJson; this.escapeHtmlChars = gson.htmlSafe; this.formattingStyle = gson.formattingStyle; - this.lenient = gson.lenient; + this.strictness = gson.strictness; this.serializeSpecialFloatingPointValues = gson.serializeSpecialFloatingPointValues; this.longSerializationPolicy = gson.longSerializationPolicy; this.datePattern = gson.datePattern; @@ -521,18 +526,40 @@ public GsonBuilder setFormattingStyle(FormattingStyle formattingStyle) { } /** - * Configures Gson to allow JSON data which does not strictly comply with the JSON specification. + * Sets the strictness of this builder to {@link Strictness#LENIENT}. * - *

    Note: Due to legacy reasons most methods of Gson are always lenient, regardless of - * whether this builder method is used. + * @deprecated This method is equivalent to calling {@link #setStrictness(Strictness)} with + * {@link Strictness#LENIENT}: {@code setStrictness(Strictness.LENIENT)} * - * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern - * @see JsonReader#setLenient(boolean) - * @see JsonWriter#setLenient(boolean) + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern. + * @see JsonReader#setStrictness(Strictness) + * @see JsonWriter#setStrictness(Strictness) + * @see #setStrictness(Strictness) */ + @Deprecated + @InlineMe(replacement = "this.setStrictness(Strictness.LENIENT)", imports = "com.google.gson.Strictness") @CanIgnoreReturnValue public GsonBuilder setLenient() { - lenient = true; + return setStrictness(Strictness.LENIENT); + } + + /** + * Sets the strictness of this builder to the provided parameter. + * + *

    This changes how strict the + * RFC 8259 JSON specification is enforced when parsing or + * writing JSON. For details on this, refer to {@link JsonReader#setStrictness(Strictness)} and + * {@link JsonWriter#setStrictness(Strictness)}.

    + * + * @param strictness the new strictness mode. May not be {@code null}. + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern. + * @see JsonReader#setStrictness(Strictness) + * @see JsonWriter#setStrictness(Strictness) + * @since $next-version$ + */ + @CanIgnoreReturnValue + public GsonBuilder setStrictness(Strictness strictness) { + this.strictness = Objects.requireNonNull(strictness); return this; } @@ -711,7 +738,7 @@ public GsonBuilder registerTypeHierarchyAdapter(Class baseType, Object typeAd } /** - * Section 2.4 of JSON specification disallows + * Section 6 of JSON specification disallows * special double values (NaN, Infinity, -Infinity). However, * Javascript * specification (see section 4.3.20, 4.3.22, 4.3.23) allows these values as valid Javascript @@ -804,7 +831,7 @@ public Gson create() { return new Gson(excluder, fieldNamingPolicy, new HashMap<>(instanceCreators), serializeNulls, complexMapKeySerialization, - generateNonExecutableJson, escapeHtmlChars, formattingStyle, lenient, + generateNonExecutableJson, escapeHtmlChars, formattingStyle, strictness, serializeSpecialFloatingPointValues, useJdkUnsafe, longSerializationPolicy, datePattern, dateStyle, timeStyle, new ArrayList<>(this.factories), new ArrayList<>(this.hierarchyFactories), factories, diff --git a/gson/src/main/java/com/google/gson/JsonElement.java b/gson/src/main/java/com/google/gson/JsonElement.java index 23e5654aac..1b440d0532 100644 --- a/gson/src/main/java/com/google/gson/JsonElement.java +++ b/gson/src/main/java/com/google/gson/JsonElement.java @@ -321,7 +321,8 @@ public String toString() { try { StringWriter stringWriter = new StringWriter(); JsonWriter jsonWriter = new JsonWriter(stringWriter); - jsonWriter.setLenient(true); + // Make writer lenient because toString() must not fail, even if for example JsonPrimitive contains NaN + jsonWriter.setStrictness(Strictness.LENIENT); Streams.write(this, jsonWriter); return stringWriter.toString(); } catch (IOException e) { diff --git a/gson/src/main/java/com/google/gson/JsonParser.java b/gson/src/main/java/com/google/gson/JsonParser.java index 20d3750cea..557d00c8e0 100644 --- a/gson/src/main/java/com/google/gson/JsonParser.java +++ b/gson/src/main/java/com/google/gson/JsonParser.java @@ -41,7 +41,7 @@ public JsonParser() {} * An exception is thrown if the JSON string has multiple top-level JSON elements, * or if there is trailing data. * - *

    The JSON string is parsed in {@linkplain JsonReader#setLenient(boolean) lenient mode}. + *

    The JSON string is parsed in {@linkplain JsonReader#setStrictness(Strictness) lenient mode}. * * @param json JSON text * @return a parse tree of {@link JsonElement}s corresponding to the specified JSON @@ -57,7 +57,7 @@ public static JsonElement parseString(String json) throws JsonSyntaxException { * An exception is thrown if the JSON string has multiple top-level JSON elements, * or if there is trailing data. * - *

    The JSON data is parsed in {@linkplain JsonReader#setLenient(boolean) lenient mode}. + *

    The JSON data is parsed in {@linkplain JsonReader#setStrictness(Strictness) lenient mode}. * * @param reader JSON text * @return a parse tree of {@link JsonElement}s corresponding to the specified JSON @@ -87,8 +87,8 @@ public static JsonElement parseReader(Reader reader) throws JsonIOException, Jso * Unlike the other {@code parse} methods, no exception is thrown if the JSON data has * multiple top-level JSON elements, or if there is trailing data. * - *

    The JSON data is parsed in {@linkplain JsonReader#setLenient(boolean) lenient mode}, - * regardless of the lenient mode setting of the provided reader. The lenient mode setting + *

    The JSON data is parsed in {@linkplain JsonReader#setStrictness(Strictness) lenient mode}, + * regardless of the strictness setting of the provided reader. The strictness setting * of the reader is restored once this method returns. * * @throws JsonParseException if there is an IOException or if the specified @@ -97,8 +97,8 @@ public static JsonElement parseReader(Reader reader) throws JsonIOException, Jso */ public static JsonElement parseReader(JsonReader reader) throws JsonIOException, JsonSyntaxException { - boolean lenient = reader.isLenient(); - reader.setLenient(true); + Strictness strictness = reader.getStrictness(); + reader.setStrictness(Strictness.LENIENT); try { return Streams.parse(reader); } catch (StackOverflowError e) { @@ -106,7 +106,7 @@ public static JsonElement parseReader(JsonReader reader) } catch (OutOfMemoryError e) { throw new JsonParseException("Failed parsing JSON source: " + reader + " to Json", e); } finally { - reader.setLenient(lenient); + reader.setStrictness(strictness); } } diff --git a/gson/src/main/java/com/google/gson/JsonStreamParser.java b/gson/src/main/java/com/google/gson/JsonStreamParser.java index cbc2883ca8..7d2629368b 100644 --- a/gson/src/main/java/com/google/gson/JsonStreamParser.java +++ b/gson/src/main/java/com/google/gson/JsonStreamParser.java @@ -28,7 +28,7 @@ /** * A streaming parser that allows reading of multiple {@link JsonElement}s from the specified reader * asynchronously. The JSON data is parsed in lenient mode, see also - * {@link JsonReader#setLenient(boolean)}. + * {@link JsonReader#setStrictness(Strictness)}. * *

    This class is conditionally thread-safe (see Item 70, Effective Java second edition). To * properly use this class across multiple threads, you will need to add some external @@ -66,7 +66,7 @@ public JsonStreamParser(String json) { */ public JsonStreamParser(Reader reader) { parser = new JsonReader(reader); - parser.setLenient(true); + parser.setStrictness(Strictness.LENIENT); lock = new Object(); } diff --git a/gson/src/main/java/com/google/gson/Strictness.java b/gson/src/main/java/com/google/gson/Strictness.java new file mode 100644 index 0000000000..f3bd3fe08f --- /dev/null +++ b/gson/src/main/java/com/google/gson/Strictness.java @@ -0,0 +1,29 @@ +package com.google.gson; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * Modes that indicate how strictly a JSON {@linkplain JsonReader reader} or + * {@linkplain JsonWriter writer} follows the syntax laid out in the + * RFC 8259 JSON specification. + * + *

    You can look at {@link JsonReader#setStrictness(Strictness)} to see how the strictness + * affects the {@link JsonReader} and you can look at + * {@link JsonWriter#setStrictness(Strictness)} to see how the strictness + * affects the {@link JsonWriter}.

    + * + * @see JsonReader#setStrictness(Strictness) + * @see JsonWriter#setStrictness(Strictness) + * @since $next-version$ + */ +public enum Strictness { + /** Allow large deviations from the JSON specification. */ + LENIENT, + + /** Allow certain small deviations from the JSON specification for legacy reasons. */ + LEGACY_STRICT, + + /** Strict compliance with the JSON specification. */ + STRICT +} diff --git a/gson/src/main/java/com/google/gson/TypeAdapter.java b/gson/src/main/java/com/google/gson/TypeAdapter.java index 5fdea225a5..d15b1e080c 100644 --- a/gson/src/main/java/com/google/gson/TypeAdapter.java +++ b/gson/src/main/java/com/google/gson/TypeAdapter.java @@ -134,10 +134,10 @@ public TypeAdapter() { /** * Converts {@code value} to a JSON document and writes it to {@code out}. - * Unlike Gson's similar {@link Gson#toJson(JsonElement, Appendable) toJson} - * method, this write is strict. Create a {@link - * JsonWriter#setLenient(boolean) lenient} {@code JsonWriter} and call - * {@link #write(JsonWriter, Object)} for lenient writing. + * The strictness {@link Strictness#LEGACY_STRICT} is used for writing the JSON data. + * To use a different strictness setting create a {@link JsonWriter}, call its + * {@link JsonWriter#setStrictness(Strictness)} method and then use + * {@link #write(JsonWriter, Object)} for writing. * * @param value the Java object to convert. May be null. * @since 2.2 @@ -207,10 +207,11 @@ public final TypeAdapter nullSafe() { } /** - * Converts {@code value} to a JSON document. Unlike Gson's similar {@link - * Gson#toJson(Object) toJson} method, this write is strict. Create a {@link - * JsonWriter#setLenient(boolean) lenient} {@code JsonWriter} and call - * {@link #write(JsonWriter, Object)} for lenient writing. + * Converts {@code value} to a JSON document. + * The strictness {@link Strictness#LEGACY_STRICT} is used for writing the JSON data. + * To use a different strictness setting create a {@link JsonWriter}, call its + * {@link JsonWriter#setStrictness(Strictness)} method and then use + * {@link #write(JsonWriter, Object)} for writing. * * @throws JsonIOException wrapping {@code IOException}s thrown by {@link #write(JsonWriter, Object)} * @param value the Java object to convert. May be null. @@ -253,10 +254,10 @@ public final JsonElement toJsonTree(T value) { public abstract T read(JsonReader in) throws IOException; /** - * Converts the JSON document in {@code in} to a Java object. Unlike Gson's - * similar {@link Gson#fromJson(Reader, Class) fromJson} method, this - * read is strict. Create a {@link JsonReader#setLenient(boolean) lenient} - * {@code JsonReader} and call {@link #read(JsonReader)} for lenient reading. + * Converts the JSON document in {@code in} to a Java object. The strictness + * {@link Strictness#LEGACY_STRICT} is used for reading the JSON data. To use a different + * strictness setting create a {@link JsonReader}, call its {@link JsonReader#setStrictness(Strictness)} + * method and then use {@link #read(JsonReader)} for reading. * *

    No exception is thrown if the JSON data has multiple top-level JSON elements, * or if there is trailing data. @@ -270,10 +271,10 @@ public final T fromJson(Reader in) throws IOException { } /** - * Converts the JSON document in {@code json} to a Java object. Unlike Gson's - * similar {@link Gson#fromJson(String, Class) fromJson} method, this read is - * strict. Create a {@link JsonReader#setLenient(boolean) lenient} {@code - * JsonReader} and call {@link #read(JsonReader)} for lenient reading. + * Converts the JSON document in {@code json} to a Java object. The strictness + * {@link Strictness#LEGACY_STRICT} is used for reading the JSON data. To use a different + * strictness setting create a {@link JsonReader}, call its {@link JsonReader#setStrictness(Strictness)} + * method and then use {@link #read(JsonReader)} for reading. * *

    No exception is thrown if the JSON data has multiple top-level JSON elements, * or if there is trailing data. diff --git a/gson/src/main/java/com/google/gson/stream/JsonReader.java b/gson/src/main/java/com/google/gson/stream/JsonReader.java index de7aef5ff5..0d106a1ccc 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -16,6 +16,7 @@ package com.google.gson.stream; +import com.google.gson.Strictness; import com.google.gson.internal.JsonReaderInternalAccess; import com.google.gson.internal.TroubleshootingGuide; import com.google.gson.internal.bind.JsonTreeReader; @@ -27,7 +28,7 @@ import java.util.Objects; /** - * Reads a JSON (RFC 7159) + * Reads a JSON (RFC 8259) * encoded value as a stream of tokens. This stream includes both literal * values (strings, numbers, booleans, and nulls) as well as the begin and * end delimiters of objects and arrays. The tokens are traversed in @@ -181,7 +182,7 @@ *

    Prefixing JSON files with ")]}'\n" makes them non-executable * by {@code