-
Notifications
You must be signed in to change notification settings - Fork 428
Description
Summary
The @PolyNull polymorphic qualifier is not correctly resolved when it appears in nested generic type parameters (e.g., Supplier<@PolyNull Integer>). Instead of being resolved based on the actual method reference's return type, it defaults to @NonNull, causing incorrect type checking errors.
Commands
javac \
-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
-J--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \
-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED \
-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \
-processorpath checker/dist/checker.jar \
-cp checker/dist/checker-qual.jar \
-processor org.checkerframework.checker.nullness.NullnessChecker \
checker/tests/nullness/TryPolyNullness.javaCode
// File: TryPolyNullness.java
import org.checkerframework.checker.nullness.qual.*;
import java.util.function.*;
import java.util.Random;
public class TryPolyNullness {
private static @Nullable Integer supplierDemo() {
var random = new Random().nextInt(100);
if(random % 2 == 0) {
return null;
}
return 1;
}
private static @PolyNull Integer trySupplier(Supplier<@PolyNull Integer> supplier) {
return supplier.get();
}
public static void main(String[] args) {
trySupplier(TryPolyNullness::supplierDemo);
// workaround by TYPE_CAST
// trySupplier((Supplier<@Nullable Integer>)TryPolyNullness::supplierDemo);
}
}Actual Behavior
The checker reports an error similar to:
checker/tests/nullness/TryPolyNullness.java:18: error: [methodref.return] Incompatible return type
trySupplier(TryPolyNullness::supplierDemo);
^
found : @Initialized @Nullable Integer
required: @Initialized @NonNull Integer
Consequence: method in @Initialized @NonNull TryPolyNullness
@Initialized @Nullable Integer supplierDemo()
is not a valid method reference for method in @Initialized @NonNull Supplier<@Initialized @NonNull Integer>
@Initialized @NonNull Integer get(@Initialized @NonNull Supplier<@Initialized @NonNull Integer> this)
1 error
Expected Behavior
There’s no error. The program’s behavior should be the same as if a type cast were applied in the method call signature.
trySupplier((Supplier<@Nullable Integer>)TryPolyNullness::supplierDemo); Why I raised this issue
This bug was discovered while attempting to fix issue #7340, where Map.merge, Map.compute, and Map.computeIfAbsent were changed from using @PolyNull to @Nullable in PR #227 (merged in typetools/jdk).
The original design using @PolyNull was actually reasonable because @PolyNull should be able to dynamically infer its instantiation based on the annotations of the return type of the function passed as an argument. For example, when calling map.merge(key, value, Integer::sum), the @PolyNull should resolve to @NonNull because Integer::sum returns @NonNull Integer. Similarly, when passing a function that returns @Nullable, the @PolyNull should resolve to @Nullable.
However, it appears that the type inference mechanism for @PolyNull in nested generic type parameters is not working correctly. This led to the workaround in PR #227 to change @PolyNull to @Nullable to avoid type inference issues. But this workaround causes legitimate code to fail type checking, as reported in issue #7340.
To better understand and isolate the root cause, I simplified the test case from issue #7340 to a minimal reproducible case using Supplier<@PolyNull Integer>.
I wanted to fix this bug, but upon investigation, I found that it touches core framework code. To avoid potentially introducing unintended side effects, I'm documenting my findings here for review by the maintainers.
Root Cause Analysis
I debug from
- the function
checkMethodReferenceAsOverrideinBaseTypeVisitor. - It goes to
METHOD_INVOCATIONswitch branches ingetFunctionalInterfaceTypeinAnnotatedTypeFactory.java(Note: If I use type_case, it will go to the type_case branch which is correct). - Then in the
GenericAnnotatedTypeFactory.java,poly.resolveis invoked.
@Override
public void methodFromUsePreSubstitution(
ExpressionTree tree, AnnotatedExecutableType type, boolean resolvePolyQuals) {
super.methodFromUsePreSubstitution(tree, type, resolvePolyQuals);
if (tree instanceof MethodInvocationTree && resolvePolyQuals) {
poly.resolve((MethodInvocationTree) tree, type);
}
}- function
resolveinAbstractQualifierPolymorphism.class
atypeFactory::getAnnotatedType will return a nonNull for TryPolyNullness::supplierDemo
@Override
public void resolve(MethodInvocationTree tree, AnnotatedExecutableType type) {
.....
List<AnnotatedTypeMirror> parameters =
AnnotatedTypes.adaptParameters(atypeFactory, type, tree.getArguments(), tree);
List<AnnotatedTypeMirror> arguments =
CollectionsPlume.mapList(atypeFactory::getAnnotatedType, tree.getArguments());
......
}