From 6468f79ddfe477563cb26c0840d6489b0e57811d Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 25 Apr 2026 20:15:44 -0700 Subject: [PATCH 01/27] WIP --- .../CheckIdenticalNullabilityVisitor.java | 17 ++- .../generics/ConstraintSolverImpl.java | 135 ++++++++++++++++-- .../uber/nullaway/jspecify/WildcardTests.java | 3 - 3 files changed, 136 insertions(+), 19 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java b/nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java index 8e205912db..26c5600937 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java @@ -177,12 +177,21 @@ private boolean extendsBoundContains(Type lhsBound, Type rhsTypeArgument) { * bound for unbounded wildcards and {@code super} wildcards. */ private Type wildcardUpperBound(Type.WildcardType wildcardType) { + Type upperBound; if (wildcardType.kind == BoundKind.EXTENDS) { - return wildcardType.getExtendsBound(); + upperBound = wildcardType.getExtendsBound(); + } else { + // For ? and ? super L, javac stores the wildcard's corresponding type variable in the `bound` + // field. The upper bound of that type variable is the wildcard's effective upper bound. + upperBound = wildcardType.bound.getUpperBound(); } - // For ? and ? super L, javac stores the wildcard's corresponding type variable in the `bound` - // field. The upper bound of that type variable is the wildcard's effective upper bound. - return wildcardType.bound.getUpperBound(); + if (upperBound instanceof Type.WildcardType nestedWildcard) { + return wildcardUpperBound(nestedWildcard); + } + if (upperBound instanceof Type.CapturedType capturedType && capturedType.wildcard != null) { + return wildcardUpperBound(capturedType.wildcard); + } + return upperBound; } /** diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index 51eb67495a..ade5bcfc29 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -5,10 +5,13 @@ import com.google.common.base.Verify; import com.google.errorprone.VisitorState; import com.sun.tools.javac.code.Attribute; +import com.sun.tools.javac.code.BoundKind; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.code.Type.CapturedType; import com.sun.tools.javac.code.Type.ClassType; import com.sun.tools.javac.code.Type.TypeVar; +import com.sun.tools.javac.code.Type.WildcardType; import com.sun.tools.javac.code.Types; import com.uber.nullaway.CodeAnnotationInfo; import com.uber.nullaway.Config; @@ -73,6 +76,9 @@ private static final class VarState { /* All variables seen so far. */ private final Map vars = new HashMap<>(); + /* Captured type variables for which we have already added bound constraints. */ + private final Set capturesWithBoundConstraints = new HashSet<>(); + /* ───────────────────── public API ───────────────────── */ @Override @@ -92,7 +98,10 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo public @Nullable Void visitType(Type subtype, Type supertype) { // handle flow into a type variable. the check for !(subtype instanceof TypeVar) is a // small optimization, as that case should be handled in visitTypeVar. - if (!localVariableType && (supertype instanceof TypeVar) && !(subtype instanceof TypeVar)) { + if (!localVariableType + && (supertype instanceof TypeVar) + && !(subtype instanceof TypeVar) + && !(subtype instanceof WildcardType)) { directlyConstrainTypePair(subtype, supertype); } return null; @@ -116,13 +125,9 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo int numTypeArgs = supertypeTypeArguments.size(); Verify.verify(numTypeArgs == subtypeTypeArguments.size()); for (int i = 0; i < numTypeArgs; i++) { - Type rhsTypeArg = supertypeTypeArguments.get(i); - Type lhsTypeArg = subtypeTypeArguments.get(i); - // constrain in both directions - // TODO should we have a more optimized way to equate two types? this just makes each - // type a subtype of the other - lhsTypeArg.accept(this, rhsTypeArg); - rhsTypeArg.accept(this, lhsTypeArg); + Type supertypeTypeArg = supertypeTypeArguments.get(i); + Type subtypeTypeArg = subtypeTypeArguments.get(i); + constrainTypeArgumentContainment(subtypeTypeArg, supertypeTypeArg); } } // if supertype is not a ClassType, we still call visitType to handle the case where @@ -152,6 +157,92 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo } return visitType(subtype, supertype); } + + private void constrainTypeArgumentContainment(Type subtypeTypeArg, Type supertypeTypeArg) { + if (!config.handleWildcardGenerics()) { + equateTypeArguments(subtypeTypeArg, supertypeTypeArg); + return; + } + WildcardType supertypeWildcard = asWildcard(supertypeTypeArg); + if (supertypeWildcard != null) { + constrainContainedByWildcard(subtypeTypeArg, supertypeWildcard); + return; + } + WildcardType subtypeWildcard = asWildcard(subtypeTypeArg); + if (subtypeWildcard != null) { + constrainWildcardContainedByConcrete(subtypeWildcard, supertypeTypeArg); + return; + } + equateTypeArguments(subtypeTypeArg, supertypeTypeArg); + } + + private void equateTypeArguments(Type subtypeTypeArg, Type supertypeTypeArg) { + // constrain in both directions + // TODO should we have a more optimized way to equate two types? this just makes each + // type a subtype of the other + subtypeTypeArg.accept(this, supertypeTypeArg); + supertypeTypeArg.accept(this, subtypeTypeArg); + } + + private void constrainContainedByWildcard(Type subtypeTypeArg, WildcardType supertypeWildcard) { + switch (supertypeWildcard.kind) { + case UNBOUND, EXTENDS -> { + Type subtypeUpperBound = effectiveWildcardUpperBound(subtypeTypeArg); + subtypeUpperBound.accept(this, wildcardUpperBound(supertypeWildcard)); + } + case SUPER -> { + Type supertypeLowerBound = castToNonNull(supertypeWildcard.getSuperBound()); + WildcardType subtypeWildcard = asWildcard(subtypeTypeArg); + if (subtypeWildcard != null) { + if (subtypeWildcard.kind == BoundKind.SUPER) { + supertypeLowerBound.accept(this, castToNonNull(subtypeWildcard.getSuperBound())); + } + } else { + supertypeLowerBound.accept(this, subtypeTypeArg); + } + } + } + } + + private void constrainWildcardContainedByConcrete( + WildcardType subtypeWildcard, Type supertypeTypeArg) { + if (subtypeWildcard.kind == BoundKind.SUPER) { + castToNonNull(subtypeWildcard.getSuperBound()).accept(this, supertypeTypeArg); + } else { + wildcardUpperBound(subtypeWildcard).accept(this, supertypeTypeArg); + } + } + + private Type effectiveWildcardUpperBound(Type typeArg) { + WildcardType wildcardType = asWildcard(typeArg); + return wildcardType == null ? typeArg : wildcardUpperBound(wildcardType); + } + + private Type wildcardUpperBound(WildcardType wildcardType) { + Type upperBound; + if (wildcardType.kind == BoundKind.EXTENDS) { + upperBound = wildcardType.getExtendsBound(); + } else { + upperBound = wildcardType.bound.getUpperBound(); + } + if (upperBound instanceof WildcardType nestedWildcard) { + return wildcardUpperBound(nestedWildcard); + } + if (upperBound instanceof CapturedType capturedType && capturedType.wildcard != null) { + return wildcardUpperBound(capturedType.wildcard); + } + return upperBound; + } + + private @Nullable WildcardType asWildcard(Type typeArg) { + if (typeArg instanceof WildcardType wildcardType) { + return wildcardType; + } + if (typeArg instanceof CapturedType capturedType) { + return capturedType.wildcard; + } + return null; + } } @Override @@ -209,6 +300,9 @@ public Map solve() throws UnsatisfiableConstraints } private void directlyConstrainTypePair(Type s, Type t) throws UnsatisfiableConstraintsException { + addCaptureBoundConstraints(s); + addCaptureBoundConstraints(t); + /* variable-to-variable edge */ if (isTypeVariable(s) && isTypeVariable(t)) { TypeVariable sv = (TypeVariable) s; @@ -272,16 +366,33 @@ private VarState getState(Element typeVarElement) { private boolean isTypeVariable(Type t) { if (t instanceof TypeVar tv) { - // For now ignore capture variables, like "capture#1 of ? extends X". Also, only treat as a - // type variable if it _doesn't_ have an explicit @Nullable or @NonNull annotation. - return !tv.isCaptured() - && !Nullness.hasNullableAnnotation(tv.getAnnotationMirrors().stream(), config) + // Only treat as a type variable if it _doesn't_ have an explicit @Nullable or @NonNull + // annotation. + return !Nullness.hasNullableAnnotation(tv.getAnnotationMirrors().stream(), config) && !Nullness.hasNonNullAnnotation(tv.getAnnotationMirrors().stream(), config); } else { return false; } } + private void addCaptureBoundConstraints(Type type) { + if (!(type instanceof CapturedType capturedType) || !isTypeVariable(capturedType)) { + return; + } + Element captureElement = capturedType.asElement(); + if (!capturesWithBoundConstraints.add(captureElement)) { + return; + } + Type lowerBound = capturedType.getLowerBound(); + if (lowerBound != null && !(lowerBound instanceof NullType)) { + lowerBound.accept(new AddSubtypeConstraintsVisitor(false), capturedType); + } + Type upperBound = capturedType.getUpperBound(); + if (upperBound != null) { + capturedType.accept(new AddSubtypeConstraintsVisitor(false), upperBound); + } + } + private boolean isKnownNullable(Type t) { return t instanceof NullType || Nullness.hasNullableAnnotation(t.getAnnotationMirrors().stream(), config); diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java index bcf6ba4037..101563e285 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -4,7 +4,6 @@ import com.uber.nullaway.NullAwayTestsBase; import com.uber.nullaway.generics.JSpecifyJavacConfig; import java.util.Arrays; -import org.junit.Ignore; import org.junit.Test; public class WildcardTests extends NullAwayTestsBase { @@ -341,7 +340,6 @@ void testLocals( .doTest(); } - @Ignore("bad interaction between wildcard support and generic method inference") @Test public void wildcardSuperBoundsAndInference() { makeHelperWithInferenceFailureWarning() @@ -370,7 +368,6 @@ void test2(BiFunction Date: Sat, 25 Apr 2026 20:24:24 -0700 Subject: [PATCH 02/27] more --- .../generics/ConstraintSolverImpl.java | 26 ++++++++++++++ .../generics/TypeSubstitutionUtils.java | 3 ++ .../uber/nullaway/jspecify/WildcardTests.java | 36 +++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index ade5bcfc29..a32b7561eb 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -96,6 +96,18 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo @Override public @Nullable Void visitType(Type subtype, Type supertype) { + if (config.handleWildcardGenerics()) { + WildcardType supertypeWildcard = asWildcard(supertype); + if (supertypeWildcard != null) { + constrainSubtypeToWildcard(subtype, supertypeWildcard); + return null; + } + WildcardType subtypeWildcard = asWildcard(subtype); + if (subtypeWildcard != null) { + constrainWildcardToSupertype(subtypeWildcard, supertype); + return null; + } + } // handle flow into a type variable. the check for !(subtype instanceof TypeVar) is a // small optimization, as that case should be handled in visitTypeVar. if (!localVariableType @@ -213,6 +225,20 @@ private void constrainWildcardContainedByConcrete( } } + private void constrainSubtypeToWildcard(Type subtype, WildcardType supertypeWildcard) { + if (supertypeWildcard.kind != BoundKind.SUPER) { + subtype.accept(this, wildcardUpperBound(supertypeWildcard)); + } + } + + private void constrainWildcardToSupertype(WildcardType subtypeWildcard, Type supertype) { + if (subtypeWildcard.kind == BoundKind.SUPER) { + castToNonNull(subtypeWildcard.getSuperBound()).accept(this, supertype); + } else { + wildcardUpperBound(subtypeWildcard).accept(this, supertype); + } + } + private Type effectiveWildcardUpperBound(Type typeArg) { WildcardType wildcardType = asWildcard(typeArg); return wildcardType == null ? typeArg : wildcardUpperBound(wildcardType); diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java b/nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java index 575552d3a5..de4d8440d8 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java @@ -299,6 +299,9 @@ public Type visitClassType(Type.ClassType t, Type other) { // TODO revisit this decision when we add fuller support for inference and wildcards. return visit(t, wt.getExtendsBound()); } + if (other instanceof Type.WildcardType wt && wt.kind == BoundKind.SUPER) { + return visit(t, wt.getSuperBound()); + } Type updated = updateDirectNullabilityAnnotationsForType(t, other); if (!(other instanceof Type.ClassType)) { diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java index 101563e285..f99dae099d 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -428,6 +428,42 @@ void test() { .doTest(); } + @Test + public void mapStreamValuesToNullable() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.*; + @NullMarked + class Test { + interface List { + Stream stream(); + } + interface Stream { + Stream map(Function mapper); + void forEach(Consumer action); + } + interface Function { + R apply(T t); + } + interface Consumer { + void accept(T t); + } + static @Nullable String mapToNull(String s) { + return null; + } + static void test(List list) { + // legal, should infer R -> @Nullable String + list.stream().map(Test::mapToNull).forEach(s -> { + // BUG: Diagnostic contains: dereferenced expression s is @Nullable + s.hashCode(); + }); + } + }""") + .doTest(); + } + private CompilationTestHelper makeHelper() { return makeTestHelperWithArgs( JSpecifyJavacConfig.withJSpecifyModeArgs( From 93658b0d209e37797e282586882f4d0269ba5d85 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 08:33:55 -0700 Subject: [PATCH 03/27] fixes --- .../CheckIdenticalNullabilityVisitor.java | 6 ++++- .../generics/ConstraintSolverImpl.java | 27 ++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java b/nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java index 26c5600937..f092c450e9 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java @@ -5,6 +5,7 @@ import com.google.errorprone.VisitorState; import com.sun.tools.javac.code.BoundKind; import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Symtab; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Types; import com.uber.nullaway.Config; @@ -183,7 +184,10 @@ private Type wildcardUpperBound(Type.WildcardType wildcardType) { } else { // For ? and ? super L, javac stores the wildcard's corresponding type variable in the `bound` // field. The upper bound of that type variable is the wildcard's effective upper bound. - upperBound = wildcardType.bound.getUpperBound(); + upperBound = + wildcardType.bound == null + ? Symtab.instance(state.context).objectType + : wildcardType.bound.getUpperBound(); } if (upperBound instanceof Type.WildcardType nestedWildcard) { return wildcardUpperBound(nestedWildcard); diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index a32b7561eb..83bc442a87 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -7,6 +7,7 @@ import com.sun.tools.javac.code.Attribute; import com.sun.tools.javac.code.BoundKind; import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Symtab; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Type.CapturedType; import com.sun.tools.javac.code.Type.ClassType; @@ -249,7 +250,10 @@ private Type wildcardUpperBound(WildcardType wildcardType) { if (wildcardType.kind == BoundKind.EXTENDS) { upperBound = wildcardType.getExtendsBound(); } else { - upperBound = wildcardType.bound.getUpperBound(); + upperBound = + wildcardType.bound == null + ? Symtab.instance(state.context).objectType + : wildcardType.bound.getUpperBound(); } if (upperBound instanceof WildcardType nestedWildcard) { return wildcardUpperBound(nestedWildcard); @@ -435,9 +439,9 @@ private boolean upperBoundIsNullable(Element typeVarElement) { } // first, check if library model overrides the upper bound nullability Element enclosingElement = typeVarElement.getEnclosingElement(); - if (enclosingElement instanceof Symbol.MethodSymbol methodSymbol) { - int typeVarIndex = - methodSymbol.getTypeParameters().indexOf((Symbol.TypeVariableSymbol) typeVarElement); + if (enclosingElement instanceof Symbol.MethodSymbol methodSymbol + && typeVarElement instanceof Symbol.TypeVariableSymbol typeVariableSymbol) { + int typeVarIndex = methodSymbol.getTypeParameters().indexOf(typeVariableSymbol); // TODO typeVarIndex is -1 in some cases; see test // com.uber.nullaway.jspecify.GenericMethodTests.instanceGenericMethodWithMethodRefArgument. // Investigate further. @@ -445,9 +449,9 @@ private boolean upperBoundIsNullable(Element typeVarElement) { && handler.onOverrideMethodTypeVariableUpperBound(methodSymbol, typeVarIndex, state)) { return true; } - } else if (enclosingElement instanceof Symbol.ClassSymbol classSymbol) { - int typeVarIndex = - classSymbol.getTypeParameters().indexOf((Symbol.TypeVariableSymbol) typeVarElement); + } else if (enclosingElement instanceof Symbol.ClassSymbol classSymbol + && typeVarElement instanceof Symbol.TypeVariableSymbol typeVariableSymbol) { + int typeVarIndex = classSymbol.getTypeParameters().indexOf(typeVariableSymbol); if (typeVarIndex >= 0 && handler.onOverrideClassTypeVariableUpperBound(classSymbol.toString(), typeVarIndex)) { return true; @@ -461,8 +465,11 @@ private boolean upperBoundIsNullable(Element typeVarElement) { } private boolean fromUnannotatedMethod(Element typeVarElement) { - Symbol enclosingElement = (Symbol) typeVarElement.getEnclosingElement(); - return enclosingElement != null - && codeAnnotationInfo.isSymbolUnannotated(enclosingElement, config, handler); + Element enclosingElement = typeVarElement.getEnclosingElement(); + if (!(enclosingElement instanceof Symbol.MethodSymbol) + && !(enclosingElement instanceof Symbol.ClassSymbol)) { + return false; + } + return codeAnnotationInfo.isSymbolUnannotated((Symbol) enclosingElement, config, handler); } } From acf7a462d4895ac9f94f9ac0a6de97b7d49d135f Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 09:21:34 -0700 Subject: [PATCH 04/27] remove unnecessary --- .../generics/ConstraintSolverImpl.java | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index 83bc442a87..bd460deb49 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -77,9 +77,6 @@ private static final class VarState { /* All variables seen so far. */ private final Map vars = new HashMap<>(); - /* Captured type variables for which we have already added bound constraints. */ - private final Set capturesWithBoundConstraints = new HashSet<>(); - /* ───────────────────── public API ───────────────────── */ @Override @@ -330,9 +327,6 @@ public Map solve() throws UnsatisfiableConstraints } private void directlyConstrainTypePair(Type s, Type t) throws UnsatisfiableConstraintsException { - addCaptureBoundConstraints(s); - addCaptureBoundConstraints(t); - /* variable-to-variable edge */ if (isTypeVariable(s) && isTypeVariable(t)) { TypeVariable sv = (TypeVariable) s; @@ -405,24 +399,6 @@ private boolean isTypeVariable(Type t) { } } - private void addCaptureBoundConstraints(Type type) { - if (!(type instanceof CapturedType capturedType) || !isTypeVariable(capturedType)) { - return; - } - Element captureElement = capturedType.asElement(); - if (!capturesWithBoundConstraints.add(captureElement)) { - return; - } - Type lowerBound = capturedType.getLowerBound(); - if (lowerBound != null && !(lowerBound instanceof NullType)) { - lowerBound.accept(new AddSubtypeConstraintsVisitor(false), capturedType); - } - Type upperBound = capturedType.getUpperBound(); - if (upperBound != null) { - capturedType.accept(new AddSubtypeConstraintsVisitor(false), upperBound); - } - } - private boolean isKnownNullable(Type t) { return t instanceof NullType || Nullness.hasNullableAnnotation(t.getAnnotationMirrors().stream(), config); From 145780e574962efd0e5e3b6cdcb0fc0665c4ac01 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 10:30:36 -0700 Subject: [PATCH 05/27] cleanup --- .../CheckIdenticalNullabilityVisitor.java | 34 ++--------- .../generics/ConstraintSolverImpl.java | 57 ++++--------------- .../uber/nullaway/generics/GenericsUtils.java | 48 ++++++++++++++++ 3 files changed, 64 insertions(+), 75 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java b/nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java index f092c450e9..57caffa1f9 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java @@ -5,7 +5,6 @@ import com.google.errorprone.VisitorState; import com.sun.tools.javac.code.BoundKind; import com.sun.tools.javac.code.Symbol; -import com.sun.tools.javac.code.Symtab; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Types; import com.uber.nullaway.Config; @@ -155,7 +154,8 @@ private boolean typeArgumentContainedBy(Type lhsTypeArgument, Type rhsTypeArgume private boolean wildcardContains(Type.WildcardType lhsWildcard, Type rhsTypeArgument) { return switch (lhsWildcard.kind) { case UNBOUND, EXTENDS -> - extendsBoundContains(wildcardUpperBound(lhsWildcard), rhsTypeArgument); + extendsBoundContains( + GenericsUtils.wildcardUpperBound(lhsWildcard, state), rhsTypeArgument); case SUPER -> superWildcardContains(lhsWildcard, rhsTypeArgument); }; } @@ -166,38 +166,14 @@ private boolean wildcardContains(Type.WildcardType lhsWildcard, Type rhsTypeArgu * actuals whose effective upper bound is {@code T}, containment holds when {@code T <: S}. */ private boolean extendsBoundContains(Type lhsBound, Type rhsTypeArgument) { - if (rhsTypeArgument instanceof Type.WildcardType rhsWildcard) { - Type rhsUpperBound = wildcardUpperBound(rhsWildcard); + Type.WildcardType rhsWildcard = GenericsUtils.asWildcard(rhsTypeArgument); + if (rhsWildcard != null) { + Type rhsUpperBound = GenericsUtils.wildcardUpperBound(rhsWildcard, state); return typeArgumentSubtype(lhsBound, rhsUpperBound); } return typeArgumentSubtype(lhsBound, rhsTypeArgument); } - /** - * Returns the effective upper bound of a wildcard, using the corresponding type variable's upper - * bound for unbounded wildcards and {@code super} wildcards. - */ - private Type wildcardUpperBound(Type.WildcardType wildcardType) { - Type upperBound; - if (wildcardType.kind == BoundKind.EXTENDS) { - upperBound = wildcardType.getExtendsBound(); - } else { - // For ? and ? super L, javac stores the wildcard's corresponding type variable in the `bound` - // field. The upper bound of that type variable is the wildcard's effective upper bound. - upperBound = - wildcardType.bound == null - ? Symtab.instance(state.context).objectType - : wildcardType.bound.getUpperBound(); - } - if (upperBound instanceof Type.WildcardType nestedWildcard) { - return wildcardUpperBound(nestedWildcard); - } - if (upperBound instanceof Type.CapturedType capturedType && capturedType.wildcard != null) { - return wildcardUpperBound(capturedType.wildcard); - } - return upperBound; - } - /** * Returns whether a formal {@code ? super S} contains the actual type argument on the right. For * concrete actuals {@code T} and wildcard actuals {@code ? super T}, containment holds when diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index bd460deb49..7b2ca64b9a 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -7,9 +7,7 @@ import com.sun.tools.javac.code.Attribute; import com.sun.tools.javac.code.BoundKind; import com.sun.tools.javac.code.Symbol; -import com.sun.tools.javac.code.Symtab; import com.sun.tools.javac.code.Type; -import com.sun.tools.javac.code.Type.CapturedType; import com.sun.tools.javac.code.Type.ClassType; import com.sun.tools.javac.code.Type.TypeVar; import com.sun.tools.javac.code.Type.WildcardType; @@ -95,12 +93,12 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo @Override public @Nullable Void visitType(Type subtype, Type supertype) { if (config.handleWildcardGenerics()) { - WildcardType supertypeWildcard = asWildcard(supertype); + WildcardType supertypeWildcard = GenericsUtils.asWildcard(supertype); if (supertypeWildcard != null) { constrainSubtypeToWildcard(subtype, supertypeWildcard); return null; } - WildcardType subtypeWildcard = asWildcard(subtype); + WildcardType subtypeWildcard = GenericsUtils.asWildcard(subtype); if (subtypeWildcard != null) { constrainWildcardToSupertype(subtypeWildcard, supertype); return null; @@ -173,12 +171,12 @@ private void constrainTypeArgumentContainment(Type subtypeTypeArg, Type supertyp equateTypeArguments(subtypeTypeArg, supertypeTypeArg); return; } - WildcardType supertypeWildcard = asWildcard(supertypeTypeArg); + WildcardType supertypeWildcard = GenericsUtils.asWildcard(supertypeTypeArg); if (supertypeWildcard != null) { constrainContainedByWildcard(subtypeTypeArg, supertypeWildcard); return; } - WildcardType subtypeWildcard = asWildcard(subtypeTypeArg); + WildcardType subtypeWildcard = GenericsUtils.asWildcard(subtypeTypeArg); if (subtypeWildcard != null) { constrainWildcardContainedByConcrete(subtypeWildcard, supertypeTypeArg); return; @@ -197,12 +195,13 @@ private void equateTypeArguments(Type subtypeTypeArg, Type supertypeTypeArg) { private void constrainContainedByWildcard(Type subtypeTypeArg, WildcardType supertypeWildcard) { switch (supertypeWildcard.kind) { case UNBOUND, EXTENDS -> { - Type subtypeUpperBound = effectiveWildcardUpperBound(subtypeTypeArg); - subtypeUpperBound.accept(this, wildcardUpperBound(supertypeWildcard)); + Type subtypeUpperBound = GenericsUtils.effectiveWildcardUpperBound(subtypeTypeArg, state); + subtypeUpperBound.accept( + this, GenericsUtils.wildcardUpperBound(supertypeWildcard, state)); } case SUPER -> { Type supertypeLowerBound = castToNonNull(supertypeWildcard.getSuperBound()); - WildcardType subtypeWildcard = asWildcard(subtypeTypeArg); + WildcardType subtypeWildcard = GenericsUtils.asWildcard(subtypeTypeArg); if (subtypeWildcard != null) { if (subtypeWildcard.kind == BoundKind.SUPER) { supertypeLowerBound.accept(this, castToNonNull(subtypeWildcard.getSuperBound())); @@ -219,13 +218,13 @@ private void constrainWildcardContainedByConcrete( if (subtypeWildcard.kind == BoundKind.SUPER) { castToNonNull(subtypeWildcard.getSuperBound()).accept(this, supertypeTypeArg); } else { - wildcardUpperBound(subtypeWildcard).accept(this, supertypeTypeArg); + GenericsUtils.wildcardUpperBound(subtypeWildcard, state).accept(this, supertypeTypeArg); } } private void constrainSubtypeToWildcard(Type subtype, WildcardType supertypeWildcard) { if (supertypeWildcard.kind != BoundKind.SUPER) { - subtype.accept(this, wildcardUpperBound(supertypeWildcard)); + subtype.accept(this, GenericsUtils.wildcardUpperBound(supertypeWildcard, state)); } } @@ -233,43 +232,9 @@ private void constrainWildcardToSupertype(WildcardType subtypeWildcard, Type sup if (subtypeWildcard.kind == BoundKind.SUPER) { castToNonNull(subtypeWildcard.getSuperBound()).accept(this, supertype); } else { - wildcardUpperBound(subtypeWildcard).accept(this, supertype); + GenericsUtils.wildcardUpperBound(subtypeWildcard, state).accept(this, supertype); } } - - private Type effectiveWildcardUpperBound(Type typeArg) { - WildcardType wildcardType = asWildcard(typeArg); - return wildcardType == null ? typeArg : wildcardUpperBound(wildcardType); - } - - private Type wildcardUpperBound(WildcardType wildcardType) { - Type upperBound; - if (wildcardType.kind == BoundKind.EXTENDS) { - upperBound = wildcardType.getExtendsBound(); - } else { - upperBound = - wildcardType.bound == null - ? Symtab.instance(state.context).objectType - : wildcardType.bound.getUpperBound(); - } - if (upperBound instanceof WildcardType nestedWildcard) { - return wildcardUpperBound(nestedWildcard); - } - if (upperBound instanceof CapturedType capturedType && capturedType.wildcard != null) { - return wildcardUpperBound(capturedType.wildcard); - } - return upperBound; - } - - private @Nullable WildcardType asWildcard(Type typeArg) { - if (typeArg instanceof WildcardType wildcardType) { - return wildcardType; - } - if (typeArg instanceof CapturedType capturedType) { - return capturedType.wildcard; - } - return null; - } } @Override diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsUtils.java b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsUtils.java index 968ac6603a..7b0a497a7e 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsUtils.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsUtils.java @@ -4,12 +4,17 @@ import com.google.errorprone.VisitorState; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.MemberReferenceTree; +import com.sun.tools.javac.code.BoundKind; import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Symtab; import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.code.Type.CapturedType; +import com.sun.tools.javac.code.Type.WildcardType; import com.sun.tools.javac.code.Types; import com.sun.tools.javac.tree.JCTree; import com.uber.nullaway.NullabilityUtil; import javax.lang.model.type.TypeKind; +import org.jspecify.annotations.Nullable; /** Utility methods for doing generics-related checking */ public class GenericsUtils { @@ -22,6 +27,49 @@ enum MethodRefTypeRelationKind { PARAMETER } + /** + * Returns the effective upper bound of {@code typeArg}. For concrete type arguments, returns the + * type itself. For wildcards and captured wildcards, returns the wildcard's upper bound, + * recursing through nested wildcards and captures produced by javac. + */ + static Type effectiveWildcardUpperBound(Type typeArg, VisitorState state) { + WildcardType wildcardType = asWildcard(typeArg); + return wildcardType == null ? typeArg : wildcardUpperBound(wildcardType, state); + } + + /** + * Returns the effective upper bound of a wildcard, using the corresponding type variable's upper + * bound for unbounded wildcards and {@code super} wildcards. + */ + static Type wildcardUpperBound(WildcardType wildcardType, VisitorState state) { + Type upperBound; + if (wildcardType.kind == BoundKind.EXTENDS) { + upperBound = wildcardType.getExtendsBound(); + } else { + upperBound = + wildcardType.bound == null + ? Symtab.instance(state.context).objectType + : wildcardType.bound.getUpperBound(); + } + if (upperBound instanceof WildcardType nestedWildcard) { + return wildcardUpperBound(nestedWildcard, state); + } + if (upperBound instanceof CapturedType capturedType && capturedType.wildcard != null) { + return wildcardUpperBound(capturedType.wildcard, state); + } + return upperBound; + } + + static @Nullable WildcardType asWildcard(Type typeArg) { + if (typeArg instanceof WildcardType wildcardType) { + return wildcardType; + } + if (typeArg instanceof CapturedType capturedType) { + return capturedType.wildcard; + } + return null; + } + /** * Handler for method reference type relations, used by {{@link * #processMethodRefTypeRelations(GenericsChecks, Type, MemberReferenceTree, VisitorState, From 4dc86d5421cb6d893d7297c8892e3b33557a6b7f Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 10:48:36 -0700 Subject: [PATCH 06/27] docs --- .../nullaway/generics/ConstraintSolverImpl.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index 7b2ca64b9a..7f3641e5b2 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -213,6 +213,12 @@ private void constrainContainedByWildcard(Type subtypeTypeArg, WildcardType supe } } + /** + * Adds constraints for type-argument containment where the actual argument is a wildcard and + * the formal argument is concrete. For {@code ? extends S} and {@code ?}, containment requires + * {@code S <: supertypeTypeArg}. For {@code ? super S}, use the lower bound and require {@code + * S <: supertypeTypeArg}. + */ private void constrainWildcardContainedByConcrete( WildcardType subtypeWildcard, Type supertypeTypeArg) { if (subtypeWildcard.kind == BoundKind.SUPER) { @@ -222,12 +228,22 @@ private void constrainWildcardContainedByConcrete( } } + /** + * Adds constraints for a top-level subtype relation {@code subtype <: supertypeWildcard}. For + * {@code ? extends S} and {@code ?}, this reduces to {@code subtype <: S}. A {@code ? super S} + * supertype places no useful nullability constraint on {@code subtype}. + */ private void constrainSubtypeToWildcard(Type subtype, WildcardType supertypeWildcard) { if (supertypeWildcard.kind != BoundKind.SUPER) { subtype.accept(this, GenericsUtils.wildcardUpperBound(supertypeWildcard, state)); } } + /** + * Adds constraints for a top-level subtype relation {@code subtypeWildcard <: supertype}. For + * {@code ? extends S} and {@code ?}, this reduces to {@code S <: supertype}. For {@code ? super + * S}, use the lower bound and reduce to {@code S <: supertype}. + */ private void constrainWildcardToSupertype(WildcardType subtypeWildcard, Type supertype) { if (subtypeWildcard.kind == BoundKind.SUPER) { castToNonNull(subtypeWildcard.getSuperBound()).accept(this, supertype); From 07629777c2708ab463898a64df44944cd1494b01 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 10:53:52 -0700 Subject: [PATCH 07/27] cleanup --- .../generics/ConstraintSolverImpl.java | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index 7f3641e5b2..3b07a76cae 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -8,6 +8,7 @@ import com.sun.tools.javac.code.BoundKind; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.code.Type.CapturedType; import com.sun.tools.javac.code.Type.ClassType; import com.sun.tools.javac.code.Type.TypeVar; import com.sun.tools.javac.code.Type.WildcardType; @@ -98,11 +99,6 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo constrainSubtypeToWildcard(subtype, supertypeWildcard); return null; } - WildcardType subtypeWildcard = GenericsUtils.asWildcard(subtype); - if (subtypeWildcard != null) { - constrainWildcardToSupertype(subtypeWildcard, supertype); - return null; - } } // handle flow into a type variable. the check for !(subtype instanceof TypeVar) is a // small optimization, as that case should be handled in visitTypeVar. @@ -166,6 +162,29 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo return visitType(subtype, supertype); } + @Override + public @Nullable Void visitWildcardType(WildcardType subtype, Type supertype) { + if (config.handleWildcardGenerics()) { + constrainWildcardToSupertype(subtype, supertype); + } + return null; + } + + @Override + public @Nullable Void visitCapturedType(CapturedType subtype, Type supertype) { + if (!localVariableType) { + directlyConstrainTypePair(subtype, supertype); + } + if (config.handleWildcardGenerics()) { + WildcardType subtypeWildcard = GenericsUtils.asWildcard(subtype); + if (subtypeWildcard != null) { + constrainWildcardToSupertype(subtypeWildcard, supertype); + return null; + } + } + return visitType(subtype, supertype); + } + private void constrainTypeArgumentContainment(Type subtypeTypeArg, Type supertypeTypeArg) { if (!config.handleWildcardGenerics()) { equateTypeArguments(subtypeTypeArg, supertypeTypeArg); From ab188c138eef42ad37d4a94103e2633e48f6d546 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 11:23:28 -0700 Subject: [PATCH 08/27] comment tweaks --- .../uber/nullaway/generics/ConstraintSolverImpl.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index 3b07a76cae..7e99289083 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -100,12 +100,9 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo return null; } } - // handle flow into a type variable. the check for !(subtype instanceof TypeVar) is a + // handle flow into a type variable. The check for !(subtype instanceof TypeVar) is a // small optimization, as that case should be handled in visitTypeVar. - if (!localVariableType - && (supertype instanceof TypeVar) - && !(subtype instanceof TypeVar) - && !(subtype instanceof WildcardType)) { + if (!localVariableType && (supertype instanceof TypeVar) && !(subtype instanceof TypeVar)) { directlyConstrainTypePair(subtype, supertype); } return null; @@ -135,7 +132,7 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo } } // if supertype is not a ClassType, we still call visitType to handle the case where - // supertype is a TypeVar + // supertype is a TypeVar or a wildcard return visitType(subtype, supertype); } @@ -150,7 +147,7 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo subtypeComponentType.accept(this, superComponentType); } // if supertype is not an ArrayType, we still call visitType to handle the case where - // supertype is a TypeVar + // supertype is a TypeVar or a wildcard return visitType(subtype, supertype); } From d9f11aa0a8d1d553a51c9eed75167a0cb939daa1 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 11:27:25 -0700 Subject: [PATCH 09/27] add assertion --- .../java/com/uber/nullaway/generics/ConstraintSolverImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index 7e99289083..510f8975a4 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -96,6 +96,7 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo if (config.handleWildcardGenerics()) { WildcardType supertypeWildcard = GenericsUtils.asWildcard(supertype); if (supertypeWildcard != null) { + Verify.verify(!localVariableType, "A local variable should not have a wildcard type"); constrainSubtypeToWildcard(subtype, supertypeWildcard); return null; } From b3cbb66dbf7a1b464ae91d1b08748dba14385177 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 12:01:08 -0700 Subject: [PATCH 10/27] javadoc --- .../com/uber/nullaway/generics/ConstraintSolverImpl.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index 510f8975a4..1ab3d5c85c 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -183,6 +183,12 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo return visitType(subtype, supertype); } + /** + * Adds nullability constraints for containment of one type argument by another during generic + * class/interface subtyping. For non-wildcard arguments, NullAway requires identical + * nullability. When either side is a wildcard, containment is reduced to constraints between + * the wildcard bound and the opposing argument. + */ private void constrainTypeArgumentContainment(Type subtypeTypeArg, Type supertypeTypeArg) { if (!config.handleWildcardGenerics()) { equateTypeArguments(subtypeTypeArg, supertypeTypeArg); From 644df503024bc6a259a8483da0245fb3bcbd9e2a Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 12:06:30 -0700 Subject: [PATCH 11/27] cleanup --- .../com/uber/nullaway/generics/ConstraintSolverImpl.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index 1ab3d5c85c..9879ea562e 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -174,11 +174,8 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo directlyConstrainTypePair(subtype, supertype); } if (config.handleWildcardGenerics()) { - WildcardType subtypeWildcard = GenericsUtils.asWildcard(subtype); - if (subtypeWildcard != null) { - constrainWildcardToSupertype(subtypeWildcard, supertype); - return null; - } + constrainWildcardToSupertype(subtype.wildcard, supertype); + return null; } return visitType(subtype, supertype); } From 00e94a0a5b48d81dc7e9cbdf9d92eaa88151c126 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 12:12:22 -0700 Subject: [PATCH 12/27] cleanup --- .../generics/TypeSubstitutionUtils.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java b/nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java index de4d8440d8..a89d94d400 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java @@ -293,16 +293,15 @@ public Type visitMethodType(Type.MethodType t, Type other) { @Override public Type visitClassType(Type.ClassType t, Type other) { - if (other instanceof Type.WildcardType wt && wt.kind == BoundKind.EXTENDS) { - // As a temporary measure, we restore nullability annotations from the upper bound of the - // wildcard. - // TODO revisit this decision when we add fuller support for inference and wildcards. - return visit(t, wt.getExtendsBound()); - } - if (other instanceof Type.WildcardType wt && wt.kind == BoundKind.SUPER) { - return visit(t, wt.getSuperBound()); + if (other instanceof Type.WildcardType wt) { + // When the other type is a wildcard, restore nullability annotations from the bound that + // determines the functional interface type. + if (wt.kind == BoundKind.EXTENDS) { + return visit(t, wt.getExtendsBound()); + } else if (wt.kind == BoundKind.SUPER) { + return visit(t, wt.getSuperBound()); + } } - Type updated = updateDirectNullabilityAnnotationsForType(t, other); if (!(other instanceof Type.ClassType)) { return updated; From 1b1216b15759e3a898172de6347324a11291e281 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 12:52:24 -0700 Subject: [PATCH 13/27] Add test for #1500 --- .../uber/nullaway/jspecify/WildcardTests.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java index f99dae099d..035ecdda04 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -4,6 +4,7 @@ import com.uber.nullaway.NullAwayTestsBase; import com.uber.nullaway.generics.JSpecifyJavacConfig; import java.util.Arrays; +import org.junit.Ignore; import org.junit.Test; public class WildcardTests extends NullAwayTestsBase { @@ -464,6 +465,30 @@ static void test(List list) { .doTest(); } + @Ignore("https://github.com/uber/NullAway/issues/1500") + @Test + public void issue1500() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.*; + @NullMarked + class Test { + static class Foo { + public static Foo of(Foo foo) { + return new Foo<>(); + } + + public Foo or(Foo other) { + return this; + } + } + static final Foo<@Nullable Void> FOO = Foo.of(new Foo<@Nullable Void>()).or(new Foo<@Nullable Void>()); + }""") + .doTest(); + } + private CompilationTestHelper makeHelper() { return makeTestHelperWithArgs( JSpecifyJavacConfig.withJSpecifyModeArgs( From 1ea5c3b6427179f174ee04552acc8211e45b611a Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 14:13:12 -0700 Subject: [PATCH 14/27] another failing test --- .../uber/nullaway/jspecify/WildcardTests.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java index 035ecdda04..fa452cc9cc 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -392,6 +392,31 @@ static void test() { .doTest(); } + @Ignore("https://github.com/uber/NullAway/issues/1522") + @Test + public void issue1522() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.*; + import java.util.function.Function; + import java.util.Optional; + @NullMarked + class Test { + static class Foo { + public final Foo mapNotNull(Function mapper) { + throw new RuntimeException(); + } + } + static Foo after(Foo> foo) { + return foo.mapNotNull(x -> x.orElse(null)); + } + } + """) + .doTest(); + } + /** * Extracted from Caffeine; exposed some subtle bugs in substitutions involving identity of {@code * Type} objects From 2f2c8327428f7bdc4afc9eaa9fd76289526e2301 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 14:52:50 -0700 Subject: [PATCH 15/27] remove unnecessary override --- .../nullaway/generics/ConstraintSolverImpl.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index 9879ea562e..02c030b6cf 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -8,7 +8,6 @@ import com.sun.tools.javac.code.BoundKind; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Type; -import com.sun.tools.javac.code.Type.CapturedType; import com.sun.tools.javac.code.Type.ClassType; import com.sun.tools.javac.code.Type.TypeVar; import com.sun.tools.javac.code.Type.WildcardType; @@ -168,18 +167,6 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo return null; } - @Override - public @Nullable Void visitCapturedType(CapturedType subtype, Type supertype) { - if (!localVariableType) { - directlyConstrainTypePair(subtype, supertype); - } - if (config.handleWildcardGenerics()) { - constrainWildcardToSupertype(subtype.wildcard, supertype); - return null; - } - return visitType(subtype, supertype); - } - /** * Adds nullability constraints for containment of one type argument by another during generic * class/interface subtyping. For non-wildcard arguments, NullAway requires identical From e6e36a1e90aadab8eefec63fb9f5df7e069d609e Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 15:26:55 -0700 Subject: [PATCH 16/27] another test case --- .../uber/nullaway/jspecify/WildcardTests.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java index fa452cc9cc..3f1a4d202e 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -369,6 +369,36 @@ void test2(BiFunction {} + static void take(Box box) {} + static T get(Box box) { + throw new RuntimeException(); + } + Object field = new Object(); + void test(Box nullableBox, Box nonNullBox) { + take(nullableBox); + take(nonNullBox); + // TODO we need to report an additional error besides inference failure here + // See https://github.com/uber/NullAway/issues/1551 + // BUG: Diagnostic contains: Failed to infer type argument nullability + field = get(nullableBox); + field = get(nonNullBox); + } + } + """) + .doTest(); + } + @Test public void genericMethodLambdaArgWildCard() { makeHelperWithInferenceFailureWarning() From 56d03b33d1dcb21b3ff90f6754894fc64d61f343 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 15:36:15 -0700 Subject: [PATCH 17/27] another test --- .../uber/nullaway/jspecify/WildcardTests.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java index 3f1a4d202e..61c9ef2e06 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -399,6 +399,33 @@ void test(Box nullableBox, Box nonNull .doTest(); } + @Test + public void methodRefParameterExtendsWildcardToConcreteParameter() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.NullMarked; + import org.jspecify.annotations.Nullable; + @NullMarked + class Test { + interface Consumer { + void accept(T t); + } + static void acceptNullable(@Nullable String s) {} + static void acceptNonNull(String s) {} + static void use(Consumer consumer) {} + static void useNullable(Consumer consumer) {} + void test() { + use(Test::acceptNullable); + // BUG: Diagnostic contains: parameter s of referenced method is @NonNull + useNullable(Test::acceptNonNull); + } + } + """) + .doTest(); + } + @Test public void genericMethodLambdaArgWildCard() { makeHelperWithInferenceFailureWarning() From 271d1d4b239b15585f9edc2f1e562dec33168c6e Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 16:12:10 -0700 Subject: [PATCH 18/27] add javadoc --- .../com/uber/nullaway/generics/ConstraintSolverImpl.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index 02c030b6cf..9c8cea8e81 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -199,6 +199,13 @@ private void equateTypeArguments(Type subtypeTypeArg, Type supertypeTypeArg) { supertypeTypeArg.accept(this, subtypeTypeArg); } + /** + * Adds constraints for type-argument containment where the formal argument is a wildcard. For + * {@code ? extends S} and {@code ?}, containment requires the actual argument's effective upper + * bound to be a subtype of {@code S}. For {@code ? super S}, concrete actual arguments require + * {@code S <: subtypeTypeArg}; {@code ? super T} actual arguments require {@code S <: T}. Other + * actual wildcard forms place no useful nullability constraint. + */ private void constrainContainedByWildcard(Type subtypeTypeArg, WildcardType supertypeWildcard) { switch (supertypeWildcard.kind) { case UNBOUND, EXTENDS -> { From 9378e235c9d37752272693f95af6deeef7a07779 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 17:58:00 -0700 Subject: [PATCH 19/27] another test --- .../uber/nullaway/jspecify/WildcardTests.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java index 61c9ef2e06..99ea0933ef 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -426,6 +426,40 @@ void test() { .doTest(); } + /** + * Tests inference for {@code Stream.collect} when the stream element type comes from a captured + * wildcard. In this case javac creates a captured type variable for the {@code ? extends + * TypeMirror} bound with no method or class owner. This covers the solver path that treats such + * ownerless captured variables as annotated code rather than asking {@code CodeAnnotationInfo} + * whether their owner is unannotated. + */ + @Test + public void streamCollectJoiningWithCapturedWildcardAccumulator() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + import java.util.List; + import java.util.stream.Collectors; + import org.jspecify.annotations.NullMarked; + @NullMarked + class Test { + interface TypeMirror { + String accept(Object visitor, Object arg); + } + interface BoundContainer { + List getBounds(); + } + String test(BoundContainer b) { + return b.getBounds().stream() + .map(bound -> ((TypeMirror) bound).accept(this, this)) + .collect(Collectors.joining(" & ")); + } + } + """) + .doTest(); + } + @Test public void genericMethodLambdaArgWildCard() { makeHelperWithInferenceFailureWarning() From 0a0e73a893fcefd8a1a29b203903b1118ba778ea Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 26 Apr 2026 18:52:30 -0700 Subject: [PATCH 20/27] remove test --- .../uber/nullaway/jspecify/WildcardTests.java | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java index 99ea0933ef..61c9ef2e06 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -426,40 +426,6 @@ void test() { .doTest(); } - /** - * Tests inference for {@code Stream.collect} when the stream element type comes from a captured - * wildcard. In this case javac creates a captured type variable for the {@code ? extends - * TypeMirror} bound with no method or class owner. This covers the solver path that treats such - * ownerless captured variables as annotated code rather than asking {@code CodeAnnotationInfo} - * whether their owner is unannotated. - */ - @Test - public void streamCollectJoiningWithCapturedWildcardAccumulator() { - makeHelperWithInferenceFailureWarning() - .addSourceLines( - "Test.java", - """ - import java.util.List; - import java.util.stream.Collectors; - import org.jspecify.annotations.NullMarked; - @NullMarked - class Test { - interface TypeMirror { - String accept(Object visitor, Object arg); - } - interface BoundContainer { - List getBounds(); - } - String test(BoundContainer b) { - return b.getBounds().stream() - .map(bound -> ((TypeMirror) bound).accept(this, this)) - .collect(Collectors.joining(" & ")); - } - } - """) - .doTest(); - } - @Test public void genericMethodLambdaArgWildCard() { makeHelperWithInferenceFailureWarning() From 25f57aafa0983fffb7264b1aadae3525a9f63469 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Mon, 27 Apr 2026 10:46:57 -0700 Subject: [PATCH 21/27] tweak test --- .../src/test/java/com/uber/nullaway/jspecify/WildcardTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java index 61c9ef2e06..064e6ec77d 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -443,6 +443,7 @@ static void test() { // legal, should infer R -> Object but then the type of the lambda as // Function via wildcard upper bound Object x = invokeWithReturn(t -> null); + x.hashCode(); } } """) From f57f949f628acce37b33f6714a607a9c7d4a6a78 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Mon, 27 Apr 2026 11:01:38 -0700 Subject: [PATCH 22/27] another failing test for follow-up --- .../test/java/com/uber/nullaway/jspecify/WildcardTests.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java index 064e6ec77d..a7a71ee31b 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -537,12 +537,14 @@ interface Consumer { static @Nullable String mapToNull(String s) { return null; } + static void callHashCode(Object o) { o.hashCode(); } static void test(List list) { - // legal, should infer R -> @Nullable String list.stream().map(Test::mapToNull).forEach(s -> { // BUG: Diagnostic contains: dereferenced expression s is @Nullable s.hashCode(); }); + // TODO we should report an error here (https://github.com/uber/NullAway/issues/1552) + list.stream().map(Test::mapToNull).forEach(Test::callHashCode); } }""") .doTest(); From 6e84a58975c9e499efdc70bafb583b13a9e86cb2 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Mon, 27 Apr 2026 14:24:46 -0700 Subject: [PATCH 23/27] add verify call --- .../java/com/uber/nullaway/generics/ConstraintSolverImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index 9c8cea8e81..aa90df6bc1 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -162,6 +162,7 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo @Override public @Nullable Void visitWildcardType(WildcardType subtype, Type supertype) { if (config.handleWildcardGenerics()) { + Verify.verify(!localVariableType, "A wildcard type cannot be assigned to a local variable"); constrainWildcardToSupertype(subtype, supertype); } return null; From 5acb6569078c2ecdf1dd41ae48c96b5ecdd9a69a Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Tue, 28 Apr 2026 08:06:39 -0700 Subject: [PATCH 24/27] comment --- .../java/com/uber/nullaway/generics/ConstraintSolverImpl.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index aa90df6bc1..cebaad781b 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -221,6 +221,9 @@ private void constrainContainedByWildcard(Type subtypeTypeArg, WildcardType supe if (subtypeWildcard.kind == BoundKind.SUPER) { supertypeLowerBound.accept(this, castToNonNull(subtypeWildcard.getSuperBound())); } + // the subtype wildcard could have an extends bound, but as far as I know we do not + // need to generate constraints for this case + // TODO revisit if needed } else { supertypeLowerBound.accept(this, subtypeTypeArg); } From 36ffec3d82b991653744996c7be99d7d2355b823 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Tue, 28 Apr 2026 12:56:25 -0700 Subject: [PATCH 25/27] coderabbit --- .../java/com/uber/nullaway/generics/ConstraintSolverImpl.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index cebaad781b..b35918fd39 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -164,8 +164,10 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo if (config.handleWildcardGenerics()) { Verify.verify(!localVariableType, "A wildcard type cannot be assigned to a local variable"); constrainWildcardToSupertype(subtype, supertype); + return null; + } else { + return visitType(subtype, supertype); } - return null; } /** From adb858c8a494b17fd1bc7f74675c8f89c3ff2a0b Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Wed, 29 Apr 2026 13:16:01 -0700 Subject: [PATCH 26/27] Revert "coderabbit" This reverts commit 2dcd522c9a3523961fb439623f804a3d760304f4. --- .../java/com/uber/nullaway/generics/ConstraintSolverImpl.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index b35918fd39..cebaad781b 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -164,10 +164,8 @@ class AddSubtypeConstraintsVisitor extends Types.DefaultTypeVisitor<@Nullable Vo if (config.handleWildcardGenerics()) { Verify.verify(!localVariableType, "A wildcard type cannot be assigned to a local variable"); constrainWildcardToSupertype(subtype, supertype); - return null; - } else { - return visitType(subtype, supertype); } + return null; } /** From 272c44731ded8274fe3f56262f0f6bc052fa5d45 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 2 May 2026 18:16:57 -0700 Subject: [PATCH 27/27] address review comments --- .../nullaway/generics/ConstraintSolverImpl.java | 17 +---------------- .../uber/nullaway/generics/GenericsUtils.java | 10 ++++++++-- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java index cebaad781b..092d4be322 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java @@ -186,7 +186,7 @@ private void constrainTypeArgumentContainment(Type subtypeTypeArg, Type supertyp } WildcardType subtypeWildcard = GenericsUtils.asWildcard(subtypeTypeArg); if (subtypeWildcard != null) { - constrainWildcardContainedByConcrete(subtypeWildcard, supertypeTypeArg); + constrainWildcardToSupertype(subtypeWildcard, supertypeTypeArg); return; } equateTypeArguments(subtypeTypeArg, supertypeTypeArg); @@ -231,21 +231,6 @@ private void constrainContainedByWildcard(Type subtypeTypeArg, WildcardType supe } } - /** - * Adds constraints for type-argument containment where the actual argument is a wildcard and - * the formal argument is concrete. For {@code ? extends S} and {@code ?}, containment requires - * {@code S <: supertypeTypeArg}. For {@code ? super S}, use the lower bound and require {@code - * S <: supertypeTypeArg}. - */ - private void constrainWildcardContainedByConcrete( - WildcardType subtypeWildcard, Type supertypeTypeArg) { - if (subtypeWildcard.kind == BoundKind.SUPER) { - castToNonNull(subtypeWildcard.getSuperBound()).accept(this, supertypeTypeArg); - } else { - GenericsUtils.wildcardUpperBound(subtypeWildcard, state).accept(this, supertypeTypeArg); - } - } - /** * Adds constraints for a top-level subtype relation {@code subtype <: supertypeWildcard}. For * {@code ? extends S} and {@code ?}, this reduces to {@code subtype <: S}. A {@code ? super S} diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsUtils.java b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsUtils.java index 7b0a497a7e..f63f45dac3 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsUtils.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsUtils.java @@ -46,10 +46,16 @@ static Type wildcardUpperBound(WildcardType wildcardType, VisitorState state) { if (wildcardType.kind == BoundKind.EXTENDS) { upperBound = wildcardType.getExtendsBound(); } else { + // We have an unbound wildcard or a wildcard with just a lower bound. In such cases, if + // present, we use the upper bound of the formal type variable to which the wildcard is being + // passed (confusingly stored in the `bound` field). E.g., if we have class Foo, and then see Foo, we use @Nullable Object as the upper + // bound. If not present, default to Object. + Type.TypeVar formalTypeVar = wildcardType.bound; upperBound = - wildcardType.bound == null + formalTypeVar == null ? Symtab.instance(state.context).objectType - : wildcardType.bound.getUpperBound(); + : formalTypeVar.getUpperBound(); } if (upperBound instanceof WildcardType nestedWildcard) { return wildcardUpperBound(nestedWildcard, state);