Skip to content

Commit b1c399f

Browse files
authored
Improve TypeToken creation validation (#2072)
* Add comments regarding multiple bounds of wildcard * Remove WildcardType check in getCollectionElementType The returned Type is never a wildcard due to the changes made to getSupertype by commit b1fb9ca. * Remove redundant getRawType call from MapTypeAdapterFactory getRawType(TypeToken.getType()) is the same as calling TypeToken.getRawType(). * Make TypeToken members private * Remove incorrect statement about TypeToken wildcards It is possible to use wildcards as part of the type argument, e.g.: `new TypeToken<List<? extends CharSequence>>() {}` * Only allow direct subclasses of TypeToken Previously subclasses of subclasses (...) of TypeToken were allowed which can behave incorrectly when retrieving the type argument, e.g.: class SubTypeToken<T> extends TypeToken<Integer> {} new SubTypeToken<String>() {}.getType() This returned `String` despite the class extending TypeToken<Integer>. * Throw exception when TypeToken captures type variable Due to type erasure the runtime type argument for a type variable is not available. Therefore there is no point in capturing a type variable and it might even give a false sense of type-safety. * Make $Gson$Types members private * Rename $Gson$Types.getGenericSupertype parameter Rename the method parameter to match the documentation of the method and to be similar to getSupertype(...). * Improve tests and handle raw TypeToken supertype better * Make some $Gson$Types members package-private again to prevent synthetic accessors * Remove TypeToken check for type variable As mentioned in review comments, there are cases during serialization where usage of the type variable is not so problematic (but still not ideal).
1 parent feaf8dd commit b1c399f

File tree

4 files changed

+110
-40
lines changed

4 files changed

+110
-40
lines changed

gson/src/main/java/com/google/gson/internal/$Gson$Types.java

+25-22
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ public static Class<?> getRawType(Type type) {
154154
return Object.class;
155155

156156
} else if (type instanceof WildcardType) {
157-
return getRawType(((WildcardType) type).getUpperBounds()[0]);
157+
Type[] bounds = ((WildcardType) type).getUpperBounds();
158+
// Currently the JLS only permits one bound for wildcards so using first bound is safe
159+
assert bounds.length == 1;
160+
return getRawType(bounds[0]);
158161

159162
} else {
160163
String className = type == null ? "null" : type.getClass().getName();
@@ -163,7 +166,7 @@ public static Class<?> getRawType(Type type) {
163166
}
164167
}
165168

166-
static boolean equal(Object a, Object b) {
169+
private static boolean equal(Object a, Object b) {
167170
return a == b || (a != null && a.equals(b));
168171
}
169172

@@ -225,10 +228,6 @@ public static boolean equals(Type a, Type b) {
225228
}
226229
}
227230

228-
static int hashCodeOrZero(Object o) {
229-
return o != null ? o.hashCode() : 0;
230-
}
231-
232231
public static String typeToString(Type type) {
233232
return type instanceof Class ? ((Class<?>) type).getName() : type.toString();
234233
}
@@ -238,19 +237,19 @@ public static String typeToString(Type type) {
238237
* IntegerSet}, the result for when supertype is {@code Set.class} is {@code Set<Integer>} and the
239238
* result when the supertype is {@code Collection.class} is {@code Collection<Integer>}.
240239
*/
241-
static Type getGenericSupertype(Type context, Class<?> rawType, Class<?> toResolve) {
242-
if (toResolve == rawType) {
240+
private static Type getGenericSupertype(Type context, Class<?> rawType, Class<?> supertype) {
241+
if (supertype == rawType) {
243242
return context;
244243
}
245244

246245
// we skip searching through interfaces if unknown is an interface
247-
if (toResolve.isInterface()) {
246+
if (supertype.isInterface()) {
248247
Class<?>[] interfaces = rawType.getInterfaces();
249248
for (int i = 0, length = interfaces.length; i < length; i++) {
250-
if (interfaces[i] == toResolve) {
249+
if (interfaces[i] == supertype) {
251250
return rawType.getGenericInterfaces()[i];
252-
} else if (toResolve.isAssignableFrom(interfaces[i])) {
253-
return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], toResolve);
251+
} else if (supertype.isAssignableFrom(interfaces[i])) {
252+
return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], supertype);
254253
}
255254
}
256255
}
@@ -259,17 +258,17 @@ static Type getGenericSupertype(Type context, Class<?> rawType, Class<?> toResol
259258
if (!rawType.isInterface()) {
260259
while (rawType != Object.class) {
261260
Class<?> rawSupertype = rawType.getSuperclass();
262-
if (rawSupertype == toResolve) {
261+
if (rawSupertype == supertype) {
263262
return rawType.getGenericSuperclass();
264-
} else if (toResolve.isAssignableFrom(rawSupertype)) {
265-
return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, toResolve);
263+
} else if (supertype.isAssignableFrom(rawSupertype)) {
264+
return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, supertype);
266265
}
267266
rawType = rawSupertype;
268267
}
269268
}
270269

271270
// we can't resolve this further
272-
return toResolve;
271+
return supertype;
273272
}
274273

275274
/**
@@ -279,10 +278,13 @@ static Type getGenericSupertype(Type context, Class<?> rawType, Class<?> toResol
279278
*
280279
* @param supertype a superclass of, or interface implemented by, this.
281280
*/
282-
static Type getSupertype(Type context, Class<?> contextRawType, Class<?> supertype) {
281+
private static Type getSupertype(Type context, Class<?> contextRawType, Class<?> supertype) {
283282
if (context instanceof WildcardType) {
284283
// wildcards are useless for resolving supertypes. As the upper bound has the same raw type, use it instead
285-
context = ((WildcardType)context).getUpperBounds()[0];
284+
Type[] bounds = ((WildcardType)context).getUpperBounds();
285+
// Currently the JLS only permits one bound for wildcards so using first bound is safe
286+
assert bounds.length == 1;
287+
context = bounds[0];
286288
}
287289
checkArgument(supertype.isAssignableFrom(contextRawType));
288290
return resolve(context, contextRawType,
@@ -306,9 +308,6 @@ public static Type getArrayComponentType(Type array) {
306308
public static Type getCollectionElementType(Type context, Class<?> contextRawType) {
307309
Type collectionType = getSupertype(context, contextRawType, Collection.class);
308310

309-
if (collectionType instanceof WildcardType) {
310-
collectionType = ((WildcardType)collectionType).getUpperBounds()[0];
311-
}
312311
if (collectionType instanceof ParameterizedType) {
313312
return ((ParameterizedType) collectionType).getActualTypeArguments()[0];
314313
}
@@ -440,7 +439,7 @@ private static Type resolve(Type context, Class<?> contextRawType, Type toResolv
440439
return toResolve;
441440
}
442441

443-
static Type resolveTypeVariable(Type context, Class<?> contextRawType, TypeVariable<?> unknown) {
442+
private static Type resolveTypeVariable(Type context, Class<?> contextRawType, TypeVariable<?> unknown) {
444443
Class<?> declaredByRaw = declaringClassOf(unknown);
445444

446445
// we can't reduce this further
@@ -522,6 +521,10 @@ public ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments
522521
&& $Gson$Types.equals(this, (ParameterizedType) other);
523522
}
524523

524+
private static int hashCodeOrZero(Object o) {
525+
return o != null ? o.hashCode() : 0;
526+
}
527+
525528
@Override public int hashCode() {
526529
return Arrays.hashCode(typeArguments)
527530
^ rawType.hashCode()

gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,7 @@ public MapTypeAdapterFactory(ConstructorConstructor constructorConstructor,
120120
return null;
121121
}
122122

123-
Class<?> rawTypeOfSrc = $Gson$Types.getRawType(type);
124-
Type[] keyAndValueTypes = $Gson$Types.getMapKeyAndValueTypes(type, rawTypeOfSrc);
123+
Type[] keyAndValueTypes = $Gson$Types.getMapKeyAndValueTypes(type, rawType);
125124
TypeAdapter<?> keyAdapter = getKeyAdapter(gson, keyAndValueTypes[0]);
126125
TypeAdapter<?> valueAdapter = gson.getAdapter(TypeToken.get(keyAndValueTypes[1]));
127126
ObjectConstructor<T> constructor = constructorConstructor.get(typeToken);

gson/src/main/java/com/google/gson/reflect/TypeToken.java

+27-14
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,20 @@
3737
* <p>
3838
* {@code TypeToken<List<String>> list = new TypeToken<List<String>>() {};}
3939
*
40-
* <p>This syntax cannot be used to create type literals that have wildcard
41-
* parameters, such as {@code Class<?>} or {@code List<? extends CharSequence>}.
40+
* <p>Capturing a type variable as type argument of a {@code TypeToken} should
41+
* be avoided. Due to type erasure the runtime type of a type variable is not
42+
* available to Gson and therefore it cannot provide the functionality one
43+
* might expect, which gives a false sense of type-safety at compilation time
44+
* and can lead to an unexpected {@code ClassCastException} at runtime.
4245
*
4346
* @author Bob Lee
4447
* @author Sven Mawson
4548
* @author Jesse Wilson
4649
*/
4750
public class TypeToken<T> {
48-
final Class<? super T> rawType;
49-
final Type type;
50-
final int hashCode;
51+
private final Class<? super T> rawType;
52+
private final Type type;
53+
private final int hashCode;
5154

5255
/**
5356
* Constructs a new type literal. Derives represented class from type
@@ -59,7 +62,7 @@ public class TypeToken<T> {
5962
*/
6063
@SuppressWarnings("unchecked")
6164
protected TypeToken() {
62-
this.type = getSuperclassTypeParameter(getClass());
65+
this.type = getTypeTokenTypeArgument();
6366
this.rawType = (Class<? super T>) $Gson$Types.getRawType(type);
6467
this.hashCode = type.hashCode();
6568
}
@@ -68,23 +71,33 @@ protected TypeToken() {
6871
* Unsafe. Constructs a type literal manually.
6972
*/
7073
@SuppressWarnings("unchecked")
71-
TypeToken(Type type) {
74+
private TypeToken(Type type) {
7275
this.type = $Gson$Types.canonicalize($Gson$Preconditions.checkNotNull(type));
7376
this.rawType = (Class<? super T>) $Gson$Types.getRawType(this.type);
7477
this.hashCode = this.type.hashCode();
7578
}
7679

7780
/**
78-
* Returns the type from super class's type parameter in {@link $Gson$Types#canonicalize
81+
* Verifies that {@code this} is an instance of a direct subclass of TypeToken and
82+
* returns the type argument for {@code T} in {@link $Gson$Types#canonicalize
7983
* canonical form}.
8084
*/
81-
static Type getSuperclassTypeParameter(Class<?> subclass) {
82-
Type superclass = subclass.getGenericSuperclass();
83-
if (superclass instanceof Class) {
84-
throw new RuntimeException("Missing type parameter.");
85+
private Type getTypeTokenTypeArgument() {
86+
Type superclass = getClass().getGenericSuperclass();
87+
if (superclass instanceof ParameterizedType) {
88+
ParameterizedType parameterized = (ParameterizedType) superclass;
89+
if (parameterized.getRawType() == TypeToken.class) {
90+
return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
91+
}
92+
}
93+
// Check for raw TypeToken as superclass
94+
else if (superclass == TypeToken.class) {
95+
throw new IllegalStateException("TypeToken must be created with a type argument: new TypeToken<...>() {}; "
96+
+ "When using code shrinkers (ProGuard, R8, ...) make sure that generic signatures are preserved.");
8597
}
86-
ParameterizedType parameterized = (ParameterizedType) superclass;
87-
return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
98+
99+
// User created subclass of subclass of TypeToken
100+
throw new IllegalStateException("Must only create direct subclasses of TypeToken");
88101
}
89102

90103
/**

gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java

+57-2
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,24 @@
2727
/**
2828
* @author Jesse Wilson
2929
*/
30-
@SuppressWarnings({"deprecation"})
3130
public final class TypeTokenTest extends TestCase {
32-
31+
// These fields are accessed using reflection by the tests below
3332
List<Integer> listOfInteger = null;
3433
List<Number> listOfNumber = null;
3534
List<String> listOfString = null;
3635
List<?> listOfUnknown = null;
3736
List<Set<String>> listOfSetOfString = null;
3837
List<Set<?>> listOfSetOfUnknown = null;
3938

39+
@SuppressWarnings({"deprecation"})
4040
public void testIsAssignableFromRawTypes() {
4141
assertTrue(TypeToken.get(Object.class).isAssignableFrom(String.class));
4242
assertFalse(TypeToken.get(String.class).isAssignableFrom(Object.class));
4343
assertTrue(TypeToken.get(RandomAccess.class).isAssignableFrom(ArrayList.class));
4444
assertFalse(TypeToken.get(ArrayList.class).isAssignableFrom(RandomAccess.class));
4545
}
4646

47+
@SuppressWarnings({"deprecation"})
4748
public void testIsAssignableFromWithTypeParameters() throws Exception {
4849
Type a = getClass().getDeclaredField("listOfInteger").getGenericType();
4950
Type b = getClass().getDeclaredField("listOfNumber").getGenericType();
@@ -56,6 +57,7 @@ public void testIsAssignableFromWithTypeParameters() throws Exception {
5657
assertFalse(TypeToken.get(b).isAssignableFrom(a));
5758
}
5859

60+
@SuppressWarnings({"deprecation"})
5961
public void testIsAssignableFromWithBasicWildcards() throws Exception {
6062
Type a = getClass().getDeclaredField("listOfString").getGenericType();
6163
Type b = getClass().getDeclaredField("listOfUnknown").getGenericType();
@@ -69,6 +71,7 @@ public void testIsAssignableFromWithBasicWildcards() throws Exception {
6971
// assertTrue(TypeToken.get(b).isAssignableFrom(a));
7072
}
7173

74+
@SuppressWarnings({"deprecation"})
7275
public void testIsAssignableFromWithNestedWildcards() throws Exception {
7376
Type a = getClass().getDeclaredField("listOfSetOfString").getGenericType();
7477
Type b = getClass().getDeclaredField("listOfSetOfUnknown").getGenericType();
@@ -102,4 +105,56 @@ public void testParameterizedFactory() {
102105
Type listOfListOfString = TypeToken.getParameterized(List.class, listOfString).getType();
103106
assertEquals(expectedListOfListOfListOfString, TypeToken.getParameterized(List.class, listOfListOfString));
104107
}
108+
109+
private static class CustomTypeToken extends TypeToken<String> {
110+
}
111+
112+
public void testTypeTokenNonAnonymousSubclass() {
113+
TypeToken<?> typeToken = new CustomTypeToken();
114+
assertEquals(String.class, typeToken.getRawType());
115+
assertEquals(String.class, typeToken.getType());
116+
}
117+
118+
/**
119+
* User must only create direct subclasses of TypeToken, but not subclasses
120+
* of subclasses (...) of TypeToken.
121+
*/
122+
public void testTypeTokenSubSubClass() {
123+
class SubTypeToken<T> extends TypeToken<String> {}
124+
class SubSubTypeToken1<T> extends SubTypeToken<T> {}
125+
class SubSubTypeToken2 extends SubTypeToken<Integer> {}
126+
127+
try {
128+
new SubTypeToken<Integer>() {};
129+
fail();
130+
} catch (IllegalStateException expected) {
131+
assertEquals("Must only create direct subclasses of TypeToken", expected.getMessage());
132+
}
133+
134+
try {
135+
new SubSubTypeToken1<Integer>();
136+
fail();
137+
} catch (IllegalStateException expected) {
138+
assertEquals("Must only create direct subclasses of TypeToken", expected.getMessage());
139+
}
140+
141+
try {
142+
new SubSubTypeToken2();
143+
fail();
144+
} catch (IllegalStateException expected) {
145+
assertEquals("Must only create direct subclasses of TypeToken", expected.getMessage());
146+
}
147+
}
148+
149+
@SuppressWarnings("rawtypes")
150+
public void testTypeTokenRaw() {
151+
try {
152+
new TypeToken() {};
153+
fail();
154+
} catch (IllegalStateException expected) {
155+
assertEquals("TypeToken must be created with a type argument: new TypeToken<...>() {}; "
156+
+ "When using code shrinkers (ProGuard, R8, ...) make sure that generic signatures are preserved.",
157+
expected.getMessage());
158+
}
159+
}
105160
}

0 commit comments

Comments
 (0)