ClassCastException in `ClassDeclarationNullnessAnnotUtils.dfsWithFormals` for a field read through `? extends W` of a class with an intersection bound

Open
#2,030 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
66/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
java
Domain
testing-qa

Research direction

Start with ClassDeclarationNullnessAnnotUtils.dfsWithFormals and the regression commit d04c9b07ddaede1bfd735a298703cfec60aa445e; compare its behavior with parent e12c544c5. Reproduce using the supplied Java case and wildcard handling options, then trace the intersection bound through TypeSubstitutionUtils.asSuper. Done means the reproducer no longer crashes and reports only the uninitialized field diagnostic.

Written by the indexing model from the issue text.

Description

With wildcard handling on (JSpecifyExperimental=true, or HandleWildcardGenerics=true alone), NullAway crashes with a ClassCastException in ClassDeclarationNullnessAnnotUtils.dfsWithFormals when it checks an assignment from a field read through C<? extends W>, where C's type parameter has an intersection bound E & I and W is a type variable bounded by a subclass of E.

Versions: NullAway master at 5023d67b8bea439fc13beae0f3719d9781308577 (0.14.3-SNAPSHOT), Error Prone 2.50.0, JDK 21.0.9.

This is a regression. git bisect over 09fdea5a2..a03b7bb90 with the reproducer below names d04c9b07ddaede1bfd735a298703cfec60aa445e, "Avoid stale wildcard bounds after javac supertype inspection (#1849)", as the first commit that crashes; its parent, e12c544c5, does not crash.

Options: -XepOpt:NullAway:AnnotatedPackages=com.uber -XepOpt:NullAway:JSpecifyMode=true -XepOpt:NullAway:JSpecifyExperimental=true

package com.uber;
import org.jspecify.annotations.NullMarked;
@NullMarked
class Test {
  static class E {}
  interface I {}
  static class F extends E {}
  static class C<T extends E & I> { T field; }
  static <W extends F> void test(C<? extends W> arg) {
    F vf = arg.field;
  }
}

Actual:

com/uber/Test.java:10: error: An unhandled exception was thrown by the Error Prone static analysis plugin.
    F vf = arg.field;
      ^
     Please report this at https://github.com/google/error-prone/issues/new and include the following, as well as a reproducing code sample (if possible):
  
     error-prone version: 2.50.0
     BugPattern: NullAway
     Stack Trace:
     java.lang.ClassCastException: class com.sun.tools.javac.code.Type$TypeVar cannot be cast to class javax.lang.model.type.DeclaredType (com.sun.tools.javac.code.Type$TypeVar is in module jdk.compiler of loader 'app'; javax.lang.model.type.DeclaredType is in module java.compiler of loader 'platform')
  	at com.uber.nullaway.generics.ClassDeclarationNullnessAnnotUtils.dfsWithFormals(ClassDeclarationNullnessAnnotUtils.java:76)
  	at com.uber.nullaway.generics.ClassDeclarationNullnessAnnotUtils.inheritancePath(ClassDeclarationNullnessAnnotUtils.java:102)
  	at com.uber.nullaway.generics.ClassDeclarationNullnessAnnotUtils.getAnnotatedSupertype(ClassDeclarationNullnessAnnotUtils.java:43)
  	at com.uber.nullaway.generics.TypeSubstitutionUtils.asSuper(TypeSubstitutionUtils.java:53)
  	at com.uber.nullaway.generics.CheckIdenticalNullabilityVisitor.visitClassType(CheckIdenticalNullabilityVisitor.java:90)
  	at com.uber.nullaway.generics.CheckIdenticalNullabilityVisitor.visitClassType(CheckIdenticalNullabilityVisitor.java:25)
  	at jdk.compiler/com.sun.tools.javac.code.Type$ClassType.accept(Type.java:1050)
  	at com.uber.nullaway.generics.GenericsChecks.identicalTypeParameterNullability(GenericsChecks.java:2398)
  	at com.uber.nullaway.generics.GenericsChecks.subtypeParameterNullability(GenericsChecks.java:2454)
  	at com.uber.nullaway.generics.GenericsChecks.subtypeParameterNullability(GenericsChecks.java:2410)
  	at com.uber.nullaway.generics.GenericsChecks.checkTypeParameterNullnessForAssignability(GenericsChecks.java:1098)
  	at com.uber.nullaway.NullAway.matchVariable(NullAway.java:1944)
  	...

Expected: no crash. The only diagnostic is the one NullAway already reports before the crash, @NonNull field 'Test$C.field' not initialized on line 8.

Each line below changed the reproducer as stated, with the same options unless the line names others:

  • E ve = arg.field; in place of F vf = arg.field; crashes the same way.
  • -XepOpt:NullAway:HandleWildcardGenerics=true in place of JSpecifyExperimental=true crashes the same way.
  • JSpecifyMode=true without JSpecifyExperimental or HandleWildcardGenerics: no crash.
  • C<T extends E>, a bound that is not an intersection: no crash.
  • test(C<? extends F> arg), a wildcard bound that is not a type variable: no crash.
  • W vt = arg.field; in place of F vf = arg.field;: no crash.

Nearest reports: the open #1850, "Stop relying on mutable WildcardType.bound outside validated contexts", is the follow-up to #1849 but names no crash. The open #1972 and #1836 handle intersection bounds; the crash also occurs at the head of #1972 (f8876954d). The head of #1836 does not crash, but its base, 09fdea5a2, predates #1849 and does not crash either. Searched issues and pull requests for dfsWithFormals, ClassDeclarationNullnessAnnotUtils, TypeVar cannot be cast to DeclaredType, intersection captured wildcard ClassCastException, and intersection bound wildcard.

Suspected cause (a guess; the report stands without it): since #1849, TypeSubstitutionUtils.asSuper reaches ClassDeclarationNullnessAnnotUtils.inheritancePath with the upper bound of the captured ? extends W, an intersection of W, E, and I. dfsWithFormals casts every direct supertype of that intersection to DeclaredType, and one of them is the type variable W.

I found this by running NullAway over the javac test test/langtools/tools/javac/generics/6531090/T6531090a.java from openjdk/jdk with @NullMarked added to its class.

Dominant language
Java
Stars
4.1k
Forks
379
Avg merge
1d 2h
Merged PRs (30d)
106

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from uber/NullAway

All issues in uber/NullAway

Similar issues

More Java issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.