Skip to content

@PolyNull cannot be resolved correctly in nested generic type parameters #7359

@Gaoyan1999

Description

@Gaoyan1999

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.java

Code

// 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

  1. the function checkMethodReferenceAsOverride in BaseTypeVisitor.
  2. It goes to METHOD_INVOCATION switch branches in getFunctionalInterfaceType in AnnotatedTypeFactory.java (Note: If I use type_case, it will go to the type_case branch which is correct).
  3. Then in theGenericAnnotatedTypeFactory.java, poly.resolve is 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);
    }
  }
  1. function resolve in AbstractQualifierPolymorphism.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());
   ......
  }

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions