-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix ConstructorConstructor creating mismatching Collection and Map instances #2068
base: main
Are you sure you want to change the base?
Changes from 7 commits
5b83a03
d5d7cdd
c6aa3b1
2b6fa06
6a63611
9952bda
1f496d7
f8ca948
e70a754
86b13f2
ae94ae8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1106,8 +1106,7 @@ public JsonReader newJsonReader(Reader reader) { | |
* @see #fromJson(String, TypeToken) | ||
*/ | ||
public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException { | ||
T object = fromJson(json, TypeToken.get(classOfT)); | ||
return Primitives.wrap(classOfT).cast(object); | ||
return fromJson(json, TypeToken.get(classOfT)); | ||
} | ||
|
||
/** | ||
|
@@ -1198,8 +1197,7 @@ public <T> T fromJson(String json, TypeToken<T> typeOfT) throws JsonSyntaxExcept | |
*/ | ||
public <T> T fromJson(Reader json, Class<T> classOfT) | ||
throws JsonSyntaxException, JsonIOException { | ||
T object = fromJson(json, TypeToken.get(classOfT)); | ||
return Primitives.wrap(classOfT).cast(object); | ||
return fromJson(json, TypeToken.get(classOfT)); | ||
} | ||
|
||
/** | ||
|
@@ -1360,7 +1358,19 @@ public <T> T fromJson(JsonReader reader, TypeToken<T> typeOfT) | |
JsonToken unused = reader.peek(); | ||
isEmpty = false; | ||
TypeAdapter<T> typeAdapter = getAdapter(typeOfT); | ||
return typeAdapter.read(reader); | ||
T object = typeAdapter.read(reader); | ||
Class<?> expectedTypeWrapped = Primitives.wrap(typeOfT.getRawType()); | ||
if (object != null && !expectedTypeWrapped.isInstance(object)) { | ||
throw new ClassCastException( | ||
"Type adapter '" | ||
Comment on lines
+1364
to
+1365
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not completely sure if this custom This check will only help when directly using |
||
+ typeAdapter | ||
+ "' returned wrong type; requested " | ||
+ typeOfT.getRawType() | ||
+ " but got instance of " | ||
+ object.getClass() | ||
+ "\nVerify that the adapter was registed for the correct type."); | ||
Marcono1234 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
return object; | ||
} catch (EOFException e) { | ||
/* | ||
* For compatibility with JSON 1.5 and earlier, we return null for empty | ||
|
@@ -1405,8 +1415,7 @@ public <T> T fromJson(JsonReader reader, TypeToken<T> typeOfT) | |
* @see #fromJson(JsonElement, TypeToken) | ||
*/ | ||
public <T> T fromJson(JsonElement json, Class<T> classOfT) throws JsonSyntaxException { | ||
T object = fromJson(json, TypeToken.get(classOfT)); | ||
return Primitives.wrap(classOfT).cast(object); | ||
return fromJson(json, TypeToken.get(classOfT)); | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,15 +36,9 @@ | |
import java.util.LinkedHashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Queue; | ||
import java.util.Set; | ||
import java.util.SortedMap; | ||
import java.util.SortedSet; | ||
import java.util.TreeMap; | ||
import java.util.TreeSet; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import java.util.concurrent.ConcurrentMap; | ||
import java.util.concurrent.ConcurrentNavigableMap; | ||
import java.util.concurrent.ConcurrentSkipListMap; | ||
|
||
/** Returns a function that can construct an instance of a requested type. */ | ||
|
@@ -321,7 +315,6 @@ public T construct() { | |
} | ||
|
||
/** Constructors for common interface types like Map and List and their subtypes. */ | ||
@SuppressWarnings("unchecked") // use runtime checks to guarantee that 'T' is what it is | ||
private static <T> ObjectConstructor<T> newDefaultImplementationConstructor( | ||
final Type type, Class<? super T> rawType) { | ||
|
||
|
@@ -334,78 +327,136 @@ private static <T> ObjectConstructor<T> newDefaultImplementationConstructor( | |
*/ | ||
|
||
if (Collection.class.isAssignableFrom(rawType)) { | ||
if (SortedSet.class.isAssignableFrom(rawType)) { | ||
return new ObjectConstructor<T>() { | ||
@Override | ||
public T construct() { | ||
return (T) new TreeSet<>(); | ||
} | ||
}; | ||
} else if (Set.class.isAssignableFrom(rawType)) { | ||
return new ObjectConstructor<T>() { | ||
@Override | ||
public T construct() { | ||
return (T) new LinkedHashSet<>(); | ||
} | ||
}; | ||
} else if (Queue.class.isAssignableFrom(rawType)) { | ||
return new ObjectConstructor<T>() { | ||
@Override | ||
public T construct() { | ||
return (T) new ArrayDeque<>(); | ||
} | ||
}; | ||
} else { | ||
return new ObjectConstructor<T>() { | ||
@Override | ||
public T construct() { | ||
return (T) new ArrayList<>(); | ||
} | ||
}; | ||
} | ||
@SuppressWarnings("unchecked") | ||
ObjectConstructor<T> constructor = (ObjectConstructor<T>) newCollectionConstructor(rawType); | ||
return constructor; | ||
} | ||
|
||
if (Map.class.isAssignableFrom(rawType)) { | ||
if (ConcurrentNavigableMap.class.isAssignableFrom(rawType)) { | ||
return new ObjectConstructor<T>() { | ||
@Override | ||
public T construct() { | ||
return (T) new ConcurrentSkipListMap<>(); | ||
} | ||
}; | ||
} else if (ConcurrentMap.class.isAssignableFrom(rawType)) { | ||
return new ObjectConstructor<T>() { | ||
@Override | ||
public T construct() { | ||
return (T) new ConcurrentHashMap<>(); | ||
} | ||
}; | ||
} else if (SortedMap.class.isAssignableFrom(rawType)) { | ||
return new ObjectConstructor<T>() { | ||
@Override | ||
public T construct() { | ||
return (T) new TreeMap<>(); | ||
} | ||
}; | ||
} else if (type instanceof ParameterizedType | ||
&& !String.class.isAssignableFrom( | ||
TypeToken.get(((ParameterizedType) type).getActualTypeArguments()[0]).getRawType())) { | ||
return new ObjectConstructor<T>() { | ||
@Override | ||
public T construct() { | ||
return (T) new LinkedHashMap<>(); | ||
} | ||
}; | ||
} else { | ||
return new ObjectConstructor<T>() { | ||
@Override | ||
public T construct() { | ||
return (T) new LinkedTreeMap<>(); | ||
} | ||
}; | ||
} | ||
@SuppressWarnings("unchecked") | ||
ObjectConstructor<T> constructor = (ObjectConstructor<T>) newMapConstructor(type, rawType); | ||
return constructor; | ||
} | ||
|
||
// Unsupported type; try other means of creating constructor | ||
return null; | ||
Comment on lines
+341
to
+342
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In #419 (comment) the concern was mentioned that the fallback will be Not sure if we should throw an exception then in case of Collection or Map? Or we could maybe add an |
||
} | ||
|
||
// Suppress Error Prone false positive: Pattern is reported for overridden methods, see | ||
// https://github.com/google/error-prone/issues/4563 | ||
@SuppressWarnings("NonApiType") | ||
private static ObjectConstructor<? extends Collection<? extends Object>> newCollectionConstructor( | ||
Class<?> rawType) { | ||
|
||
// First try List implementation | ||
if (rawType.isAssignableFrom(ArrayList.class)) { | ||
return new ObjectConstructor<ArrayList<Object>>() { | ||
@Override | ||
public ArrayList<Object> construct() { | ||
return new ArrayList<>(); | ||
} | ||
}; | ||
} | ||
// Then try Set implementation | ||
else if (rawType.isAssignableFrom(LinkedHashSet.class)) { | ||
return new ObjectConstructor<LinkedHashSet<Object>>() { | ||
@Override | ||
public LinkedHashSet<Object> construct() { | ||
return new LinkedHashSet<>(); | ||
} | ||
}; | ||
} | ||
// Then try SortedSet / NavigableSet implementation | ||
else if (rawType.isAssignableFrom(TreeSet.class)) { | ||
return new ObjectConstructor<TreeSet<Object>>() { | ||
@Override | ||
public TreeSet<Object> construct() { | ||
return new TreeSet<>(); | ||
} | ||
}; | ||
} | ||
// Then try Queue implementation | ||
else if (rawType.isAssignableFrom(ArrayDeque.class)) { | ||
return new ObjectConstructor<ArrayDeque<Object>>() { | ||
@Override | ||
public ArrayDeque<Object> construct() { | ||
return new ArrayDeque<>(); | ||
} | ||
}; | ||
} | ||
|
||
// Was unable to create matching Collection constructor | ||
return null; | ||
} | ||
|
||
private static boolean hasStringKeyType(Type mapType) { | ||
// If mapType is not parameterized, assume it might have String as key type | ||
if (!(mapType instanceof ParameterizedType)) { | ||
return true; | ||
} | ||
|
||
Type[] typeArguments = ((ParameterizedType) mapType).getActualTypeArguments(); | ||
if (typeArguments.length == 0) { | ||
return false; | ||
} | ||
// Consider String and supertypes of it | ||
return TypeToken.get(typeArguments[0]).getRawType().isAssignableFrom(String.class); | ||
} | ||
|
||
// Suppress Error Prone false positive: Pattern is reported for overridden methods, see | ||
// https://github.com/google/error-prone/issues/4563 | ||
@SuppressWarnings("NonApiType") | ||
private static ObjectConstructor<? extends Map<? extends Object, Object>> newMapConstructor( | ||
final Type type, Class<?> rawType) { | ||
// First try Map implementation | ||
/* | ||
* Legacy special casing for Map<String, ...> to avoid DoS from colliding String hashCode | ||
* values for older JDKs; use own LinkedTreeMap<String, Object> instead | ||
*/ | ||
if (rawType.isAssignableFrom(LinkedTreeMap.class) && hasStringKeyType(type)) { | ||
return new ObjectConstructor<LinkedTreeMap<Object, Object>>() { | ||
@Override | ||
public LinkedTreeMap<Object, Object> construct() { | ||
return new LinkedTreeMap<>(); | ||
} | ||
}; | ||
} else if (rawType.isAssignableFrom(LinkedHashMap.class)) { | ||
return new ObjectConstructor<LinkedHashMap<Object, Object>>() { | ||
@Override | ||
public LinkedHashMap<Object, Object> construct() { | ||
return new LinkedHashMap<>(); | ||
} | ||
}; | ||
} | ||
// Then try SortedMap / NavigableMap implementation | ||
else if (rawType.isAssignableFrom(TreeMap.class)) { | ||
return new ObjectConstructor<TreeMap<Object, Object>>() { | ||
@Override | ||
public TreeMap<Object, Object> construct() { | ||
return new TreeMap<>(); | ||
} | ||
}; | ||
} | ||
// Then try ConcurrentMap implementation | ||
else if (rawType.isAssignableFrom(ConcurrentHashMap.class)) { | ||
return new ObjectConstructor<ConcurrentHashMap<Object, Object>>() { | ||
@Override | ||
public ConcurrentHashMap<Object, Object> construct() { | ||
return new ConcurrentHashMap<>(); | ||
} | ||
}; | ||
} | ||
// Then try ConcurrentNavigableMap implementation | ||
else if (rawType.isAssignableFrom(ConcurrentSkipListMap.class)) { | ||
return new ObjectConstructor<ConcurrentSkipListMap<Object, Object>>() { | ||
@Override | ||
public ConcurrentSkipListMap<Object, Object> construct() { | ||
return new ConcurrentSkipListMap<>(); | ||
} | ||
}; | ||
} | ||
|
||
// Was unable to create matching Map constructor | ||
return null; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems all other
fromJson
methods delegate to this one, so I have added the type check (which was previouslyPrimitives.wrap(classOfT).cast(object)
in the otherfromJson
methods) here instead.However, this method did not actually have this check before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran this change against all of Google's internal tests and found a lot of failures with this exception, or with an exception about not being able to instantiate the abstract class
com.google.common.collect.ImmutableList
. In both cases I think the code in question should be fixed, because it is relying on accidental behaviour. A pattern I saw quite often was this:This is using auto-value-gson to serialize and deserialize AutoValue classes. It works even if no
TypeAdapterFactory
has been registered forImmutableList
, because Gson deserializes a plainList
. Auto-value-gson constructs theFoo
instance using its builder, and the deserializedList
works with thesetBars
method. That method callsImmutableList.copyOf(List)
so we do get the rightImmutableList
type. But the additional checks in this PR don't let us get that far.As I say, code like that only works accidentally. My plan is to add a
GuavaCollectionsTypeAdapterFactory
toextras
and update the internal code to use that. I already found several customImmutableListTypeAdapterFactory
andImmutableMapTypeAdapterFactory
classes which could be switched to use this.Some of the other failures were just plain using the wrong types, which again happened to work through erasure and the like.
So in short, I think this change is OK, but it may require some client code to be updated. Most of the updates should be straightforward and will lead to client code that is more correct.
I think it will be a while before we can complete this because of all the adjustments that will need to be made.