Skip to content

Avoid stale wildcard bounds after javac supertype inspection #1840

Description

@msridhar

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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