Background
Caffeine reported this as finding 2 in:
ben-manes/caffeine#2004 (comment)
NullAway reports an incompatibility between two types that both print as the same unbounded-wildcard type:
incompatible types: SeparatelyCompiledQueue<?> cannot be converted to SeparatelyCompiledQueue<?>
(target wildcard upper bound is Object; source wildcard upper bound is @Nullable Object)
Reduced reproduction
Compile this generic class separately so it is read from bytecode:
package com.uber.lib.generics;
import java.util.AbstractQueue;
public abstract class SeparatelyCompiledQueue<E> extends AbstractQueue<E> {
public static long producerIndex(SeparatelyCompiledQueue<?> queue) {
return 0L;
}
}
Then run NullAway together with Error Prone's PreferTestParameter checker on:
package com.uber;
import com.uber.lib.generics.SeparatelyCompiledQueue;
class Test {
static long producerIndex(SeparatelyCompiledQueue<?> queue) {
return SeparatelyCompiledQueue.producerIndex(queue);
}
}
NullAway alone does not report the false positive. PreferTestParameter triggers it by calling Types.unboxedTypeOrType() on the single method parameter.
Cause
While unboxedTypeOrType() searches for boxed primitive supertypes, javac computes supertypes of SeparatelyCompiledQueue<?>. Its substitution machinery calls WildcardType.withTypeVar(), mutating the shared wildcard's bound field. The wildcard remains referenced from the original SeparatelyCompiledQueue<?>, but its bound now points to AbstractQueue.E.
That value is meaningful in the derived AbstractQueue<?> view but stale in the original SeparatelyCompiledQueue<?> context. NullAway trusts the mutable field, sees that AbstractQueue.E comes from unannotated JDK code, and treats its upper bound as nullable.
Capture-converting the complete containing type with Types.capture() reconstructs the correct contextual upper bound (Object) from the generic declaration and current arguments. This also correctly handles dependent formal bounds such as Pair<T, U extends T>.
Expected behavior
NullAway should not report an error for the call above, regardless of whether another Error Prone checker inspected the parameter type first.
Background
Caffeine reported this as finding 2 in:
ben-manes/caffeine#2004 (comment)
NullAway reports an incompatibility between two types that both print as the same unbounded-wildcard type:
Reduced reproduction
Compile this generic class separately so it is read from bytecode:
Then run NullAway together with Error Prone's
PreferTestParameterchecker on:NullAway alone does not report the false positive.
PreferTestParametertriggers it by callingTypes.unboxedTypeOrType()on the single method parameter.Cause
While
unboxedTypeOrType()searches for boxed primitive supertypes, javac computes supertypes ofSeparatelyCompiledQueue<?>. Its substitution machinery callsWildcardType.withTypeVar(), mutating the shared wildcard'sboundfield. The wildcard remains referenced from the originalSeparatelyCompiledQueue<?>, but its bound now points toAbstractQueue.E.That value is meaningful in the derived
AbstractQueue<?>view but stale in the originalSeparatelyCompiledQueue<?>context. NullAway trusts the mutable field, sees thatAbstractQueue.Ecomes from unannotated JDK code, and treats its upper bound as nullable.Capture-converting the complete containing type with
Types.capture()reconstructs the correct contextual upper bound (Object) from the generic declaration and current arguments. This also correctly handles dependent formal bounds such asPair<T, U extends T>.Expected behavior
NullAway should not report an error for the call above, regardless of whether another Error Prone checker inspected the parameter type first.