From b42b6fbc472fb7e8a506acb9bec5875b298c5540 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 14 Feb 2026 18:59:03 -0800 Subject: [PATCH 1/8] WIP --- .../GenericTypePrettyPrintingVisitor.java | 12 ++++++++ .../nullaway/jspecify/GenericMethodTests.java | 28 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/GenericTypePrettyPrintingVisitor.java b/nullaway/src/main/java/com/uber/nullaway/generics/GenericTypePrettyPrintingVisitor.java index dbfb8814c8..7d36eaba41 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericTypePrettyPrintingVisitor.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericTypePrettyPrintingVisitor.java @@ -67,6 +67,18 @@ public String visitClassType(Type.ClassType t, @Nullable Void s) { return sb.toString(); } + @Override + public String visitTypeVar(Type.TypeVar t, @Nullable Void unused) { + StringBuilder sb = new StringBuilder(); + for (Attribute.TypeCompound compound : t.getAnnotationMirrors()) { + sb.append('@'); + sb.append(compound.type.accept(this, null)); + sb.append(' '); + } + sb.append(t.tsym.getSimpleName()); + return sb.toString(); + } + private String prettyIntersectionType(Type.IntersectionClassType t) { return t.getBounds().stream() .map(type -> ((Type) type).accept(this, null)) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java index d53ab579bb..a17ac27207 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java @@ -1528,6 +1528,34 @@ public static T notNull(@Nullable T object, String message) { .doTest(); } + @Test + public void issue1455() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Foo.java", + """ + import org.jspecify.annotations.Nullable; + import org.jspecify.annotations.NullMarked; + @NullMarked + class Foo { + interface Supplier { + T get(); + } + Supplier<@Nullable OuterT> sup = make(); + Supplier<@Nullable OuterT> make() { + throw new RuntimeException(); + } + > T acceptSup(T supplier) { + return supplier; + } + void test() { + acceptSup(sup); + } + } + """) + .doTest(); + } + private CompilationTestHelper makeHelper() { return makeTestHelperWithArgs( JSpecifyJavacConfig.withJSpecifyModeArgs( From d602c3eb72e423e5fe2fd55a3fff84ac812bbe2b Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 14 Feb 2026 19:53:08 -0800 Subject: [PATCH 2/8] WIP --- .../nullaway/generics/GenericsChecks.java | 71 +++++++++++++++++++ .../uber/nullaway/jspecify/GenericsTests.java | 4 +- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java index 2e397298ab..c801fe0d91 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -36,6 +36,7 @@ import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Types; import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.util.ListBuffer; import com.sun.tools.javac.util.Name; import com.sun.tools.javac.util.Names; import com.uber.nullaway.CodeAnnotationInfo; @@ -1596,6 +1597,9 @@ private Type substituteTypeArgsInGenericMethodType( } Type.MethodType methodTypeAtCallSite = castToNonNull(ASTHelpers.getType(invocationTree.getMethodSelect())).asMethodType(); + methodTypeAtCallSite = + restoreMissingNullabilityFromSingleTopLevelTypeVarArguments( + invocationTree, methodType, methodTypeAtCallSite, state); if (result instanceof InferenceSuccess successResult) { return TypeSubstitutionUtils.updateMethodTypeWithInferredNullability( methodTypeAtCallSite, methodType, successResult.typeVarNullability, state, config); @@ -1608,6 +1612,73 @@ private Type substituteTypeArgsInGenericMethodType( state.getTypes(), methodType, forAllType.tvars, explicitTypeArgs, config); } + /** + * For some calls, javac drops nested type-use nullability annotations in inferred substitutions + * for method type variables. Recover these annotations from the corresponding actual argument + * types, in cases where a method type variable appears exactly once as a top-level formal + * parameter type. + */ + @SuppressWarnings("ReferenceEquality") + private Type.MethodType restoreMissingNullabilityFromSingleTopLevelTypeVarArguments( + MethodInvocationTree invocationTree, + Type.MethodType origMethodType, + Type.MethodType methodTypeAtCallSite, + VisitorState state) { + Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(invocationTree); + if (methodSymbol == null || methodSymbol.isVarArgs()) { + return methodTypeAtCallSite; + } + com.sun.tools.javac.util.List origArgTypes = origMethodType.getParameterTypes(); + com.sun.tools.javac.util.List callSiteArgTypes = methodTypeAtCallSite.getParameterTypes(); + List callArgs = invocationTree.getArguments(); + if (origArgTypes.size() != callSiteArgTypes.size() || callArgs.size() != origArgTypes.size()) { + return methodTypeAtCallSite; + } + + Map topLevelTypeVarCounts = new HashMap<>(); + for (Type origArgType : origArgTypes) { + if (origArgType instanceof Type.TypeVar typeVar && typeVar.tsym.owner == methodSymbol) { + topLevelTypeVarCounts.merge((Symbol.TypeVariableSymbol) typeVar.tsym, 1, Integer::sum); + } + } + + ListBuffer updatedArgTypes = new ListBuffer<>(); + boolean changed = false; + for (int i = 0; i < origArgTypes.size(); i++) { + Type updatedType = callSiteArgTypes.get(i); + Type origArgType = origArgTypes.get(i); + if (origArgType instanceof Type.TypeVar typeVar + && !(updatedType instanceof Type.TypeVar) + && topLevelTypeVarCounts.getOrDefault((Symbol.TypeVariableSymbol) typeVar.tsym, 0) == 1) { + Type actualArgType = getTreeType(callArgs.get(i), state); + if (actualArgType != null + && !actualArgType.isRaw() + && state + .getTypes() + .isSameType( + state.getTypes().erasure(actualArgType), + state.getTypes().erasure(updatedType))) { + Type restoredType = + TypeSubstitutionUtils.restoreExplicitNullabilityAnnotations( + actualArgType, updatedType, config, Collections.emptyMap()); + if (restoredType != updatedType) { + changed = true; + updatedType = restoredType; + } + } + } + updatedArgTypes.append(updatedType); + } + if (!changed) { + return methodTypeAtCallSite; + } + return new Type.MethodType( + updatedArgTypes.toList(), + methodTypeAtCallSite.getReturnType(), + methodTypeAtCallSite.getThrownTypes(), + methodTypeAtCallSite.tsym); + } + /** * An invocation of a generic method, and the corresponding information about its assignment * context, for the purposes of inference. diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java index fcd99e7319..02ac8d2257 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java @@ -2422,7 +2422,7 @@ void testNegative(Function f) { foo(f); } void testPositive1(Function<@Nullable V, V> f) { - // BUG: Diagnostic contains: incompatible types: Function<@org.jspecify.annotations.Nullable V, V> + // BUG: Diagnostic contains: incompatible types: Function<@Nullable V, V> cannot be converted to Function foo(f); } void testPositive2(Function f) { @@ -2430,7 +2430,7 @@ void testPositive2(Function f) { foo(f); } void testPositive3(Function<@Nullable V, @Nullable V> f) { - // BUG: Diagnostic contains: incompatible types: Function<@org.jspecify.annotations.Nullable V, @org.jspecify.annotations.Nullable V> cannot be converted to + // BUG: Diagnostic contains: incompatible types: Function<@Nullable V, @Nullable V> cannot be converted to Function foo(f); } } From e8840474ed09b22f9080b9724859117930de2cad Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 14 Feb 2026 22:24:25 -0800 Subject: [PATCH 3/8] better fix --- .../nullaway/generics/GenericsChecks.java | 62 ++++++++++++------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java index c801fe0d91..077da256af 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -1615,8 +1615,7 @@ private Type substituteTypeArgsInGenericMethodType( /** * For some calls, javac drops nested type-use nullability annotations in inferred substitutions * for method type variables. Recover these annotations from the corresponding actual argument - * types, in cases where a method type variable appears exactly once as a top-level formal - * parameter type. + * types, while preserving one consistent top-level substitution per method type variable. */ @SuppressWarnings("ReferenceEquality") private Type.MethodType restoreMissingNullabilityFromSingleTopLevelTypeVarArguments( @@ -1626,6 +1625,7 @@ private Type.MethodType restoreMissingNullabilityFromSingleTopLevelTypeVarArgume VisitorState state) { Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(invocationTree); if (methodSymbol == null || methodSymbol.isVarArgs()) { + // TODO handle varargs methods return methodTypeAtCallSite; } com.sun.tools.javac.util.List origArgTypes = origMethodType.getParameterTypes(); @@ -1635,35 +1635,49 @@ private Type.MethodType restoreMissingNullabilityFromSingleTopLevelTypeVarArgume return methodTypeAtCallSite; } - Map topLevelTypeVarCounts = new HashMap<>(); - for (Type origArgType : origArgTypes) { - if (origArgType instanceof Type.TypeVar typeVar && typeVar.tsym.owner == methodSymbol) { - topLevelTypeVarCounts.merge((Symbol.TypeVariableSymbol) typeVar.tsym, 1, Integer::sum); - } - } - + Map repairedTopLevelSubstitutions = new HashMap<>(); ListBuffer updatedArgTypes = new ListBuffer<>(); boolean changed = false; for (int i = 0; i < origArgTypes.size(); i++) { Type updatedType = callSiteArgTypes.get(i); Type origArgType = origArgTypes.get(i); if (origArgType instanceof Type.TypeVar typeVar - && !(updatedType instanceof Type.TypeVar) - && topLevelTypeVarCounts.getOrDefault((Symbol.TypeVariableSymbol) typeVar.tsym, 0) == 1) { - Type actualArgType = getTreeType(callArgs.get(i), state); - if (actualArgType != null - && !actualArgType.isRaw() - && state - .getTypes() - .isSameType( - state.getTypes().erasure(actualArgType), - state.getTypes().erasure(updatedType))) { - Type restoredType = - TypeSubstitutionUtils.restoreExplicitNullabilityAnnotations( - actualArgType, updatedType, config, Collections.emptyMap()); - if (restoredType != updatedType) { + && typeVar.tsym.owner == methodSymbol + && !(updatedType instanceof Type.TypeVar)) { + Symbol.TypeVariableSymbol typeVarSymbol = (Symbol.TypeVariableSymbol) typeVar.tsym; + Type repairedSubstitution = repairedTopLevelSubstitutions.get(typeVarSymbol); + if (repairedSubstitution != null) { + if (!state + .getTypes() + .isSameType( + state.getTypes().erasure(repairedSubstitution), + state.getTypes().erasure(updatedType))) { + // Inconsistent substitution for the same top-level type variable; bail out. + return methodTypeAtCallSite; + } + if (repairedSubstitution != updatedType) { changed = true; - updatedType = restoredType; + updatedType = repairedSubstitution; + } + } else { + Type actualArgType = getTreeType(callArgs.get(i), state); + if (actualArgType != null + && !actualArgType.isRaw() + && state + .getTypes() + .isSameType( + state.getTypes().erasure(actualArgType), + state.getTypes().erasure(updatedType))) { + Type restoredType = + TypeSubstitutionUtils.restoreExplicitNullabilityAnnotations( + actualArgType, updatedType, config, Collections.emptyMap()); + repairedTopLevelSubstitutions.put(typeVarSymbol, restoredType); + if (restoredType != updatedType) { + changed = true; + updatedType = restoredType; + } + } else { + repairedTopLevelSubstitutions.put(typeVarSymbol, updatedType); } } } From be220e793557933c42b50a3630ec241d708d75aa Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 15 Feb 2026 11:10:17 -0800 Subject: [PATCH 4/8] comments --- .../main/java/com/uber/nullaway/generics/GenericsChecks.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java index 077da256af..3fc48c87ae 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -1635,6 +1635,9 @@ private Type.MethodType restoreMissingNullabilityFromSingleTopLevelTypeVarArgume return methodTypeAtCallSite; } + // use this map to store repaired substitutions for method type variables, to ensure we use the + // same repaired + // substitution for all occurrences of the same method type variable Map repairedTopLevelSubstitutions = new HashMap<>(); ListBuffer updatedArgTypes = new ListBuffer<>(); boolean changed = false; @@ -1659,7 +1662,7 @@ private Type.MethodType restoreMissingNullabilityFromSingleTopLevelTypeVarArgume changed = true; updatedType = repairedSubstitution; } - } else { + } else { // need to compute the substitution Type actualArgType = getTreeType(callArgs.get(i), state); if (actualArgType != null && !actualArgType.isRaw() From 38e6005b38c2d84efdaeee7d01ccead84b5a6675 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 21 Feb 2026 07:58:07 -0800 Subject: [PATCH 5/8] undo changes --- .../generics/GenericTypePrettyPrintingVisitor.java | 12 ------------ .../com/uber/nullaway/jspecify/GenericsTests.java | 4 ++-- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/GenericTypePrettyPrintingVisitor.java b/nullaway/src/main/java/com/uber/nullaway/generics/GenericTypePrettyPrintingVisitor.java index 7d36eaba41..dbfb8814c8 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericTypePrettyPrintingVisitor.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericTypePrettyPrintingVisitor.java @@ -67,18 +67,6 @@ public String visitClassType(Type.ClassType t, @Nullable Void s) { return sb.toString(); } - @Override - public String visitTypeVar(Type.TypeVar t, @Nullable Void unused) { - StringBuilder sb = new StringBuilder(); - for (Attribute.TypeCompound compound : t.getAnnotationMirrors()) { - sb.append('@'); - sb.append(compound.type.accept(this, null)); - sb.append(' '); - } - sb.append(t.tsym.getSimpleName()); - return sb.toString(); - } - private String prettyIntersectionType(Type.IntersectionClassType t) { return t.getBounds().stream() .map(type -> ((Type) type).accept(this, null)) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java index 02ac8d2257..fcd99e7319 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java @@ -2422,7 +2422,7 @@ void testNegative(Function f) { foo(f); } void testPositive1(Function<@Nullable V, V> f) { - // BUG: Diagnostic contains: incompatible types: Function<@Nullable V, V> cannot be converted to Function + // BUG: Diagnostic contains: incompatible types: Function<@org.jspecify.annotations.Nullable V, V> foo(f); } void testPositive2(Function f) { @@ -2430,7 +2430,7 @@ void testPositive2(Function f) { foo(f); } void testPositive3(Function<@Nullable V, @Nullable V> f) { - // BUG: Diagnostic contains: incompatible types: Function<@Nullable V, @Nullable V> cannot be converted to Function + // BUG: Diagnostic contains: incompatible types: Function<@org.jspecify.annotations.Nullable V, @org.jspecify.annotations.Nullable V> cannot be converted to foo(f); } } From bcd15ee7d291e2b4cdd8aa726b1dfd9b46eb26de Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 21 Feb 2026 14:46:05 -0800 Subject: [PATCH 6/8] fix failing tests --- .../java/com/uber/nullaway/generics/GenericsChecks.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java index bbd3028b16..7b2c4d9b44 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -1949,10 +1949,10 @@ private Type substituteTypeArgsInGenericMethodType( } Type.MethodType methodTypeAtCallSite = castToNonNull(ASTHelpers.getType(invocationTree.getMethodSelect())).asMethodType(); - methodTypeAtCallSite = - restoreMissingNullabilityFromSingleTopLevelTypeVarArguments( - invocationTree, methodType, methodTypeAtCallSite, state); if (result instanceof InferenceSuccess successResult) { + methodTypeAtCallSite = + restoreNestedNullabilityForTypeVarArguments( + invocationTree, methodType, methodTypeAtCallSite, state); return TypeSubstitutionUtils.updateMethodTypeWithInferredNullability( methodTypeAtCallSite, methodType, successResult.typeVarNullability, state, config); } else { @@ -1970,7 +1970,7 @@ private Type substituteTypeArgsInGenericMethodType( * types, while preserving one consistent top-level substitution per method type variable. */ @SuppressWarnings("ReferenceEquality") - private Type.MethodType restoreMissingNullabilityFromSingleTopLevelTypeVarArguments( + private Type.MethodType restoreNestedNullabilityForTypeVarArguments( MethodInvocationTree invocationTree, Type.MethodType origMethodType, Type.MethodType methodTypeAtCallSite, From 7f4d0cc73068cd9c112e33f8c1c1ae35adebcc1b Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 21 Feb 2026 17:24:31 -0800 Subject: [PATCH 7/8] coderabbit feedback --- .../main/java/com/uber/nullaway/generics/GenericsChecks.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java index 7b2c4d9b44..4b8bab701e 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -1976,7 +1976,7 @@ private Type.MethodType restoreNestedNullabilityForTypeVarArguments( Type.MethodType methodTypeAtCallSite, VisitorState state) { Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(invocationTree); - if (methodSymbol == null || methodSymbol.isVarArgs()) { + if (methodSymbol.isVarArgs()) { // TODO handle varargs methods return methodTypeAtCallSite; } From a2682e4e42a02949d2cd6d9e79468ce0f7dc7a5c Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Mon, 23 Mar 2026 19:57:06 -0700 Subject: [PATCH 8/8] various tune-ups --- .../nullaway/generics/GenericsChecks.java | 82 +++++++++++-------- .../nullaway/jspecify/GenericMethodTests.java | 10 +++ 2 files changed, 57 insertions(+), 35 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java index 4b8bab701e..205b045770 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -1965,9 +1965,21 @@ private Type substituteTypeArgsInGenericMethodType( } /** - * For some calls, javac drops nested type-use nullability annotations in inferred substitutions - * for method type variables. Recover these annotations from the corresponding actual argument - * types, while preserving one consistent top-level substitution per method type variable. + * In narrow cases, javac drops nested type-use nullability annotations on type variables in its + * inferred type for a generic method at a call site. See + * https://github.com/uber/NullAway/issues/1455. This method aims to restore those annotations + * based on the types of actual parameters. It does not attempt to be a very general fix, as we do + * not fully understand the scenarios where this can arise. + * + * @param invocationTree the method invocation tree for the generic method call + * @param origMethodType the declared method type for the generic method (to identify formal + * parameters whose type is a type variable of the method) + * @param methodTypeAtCallSite the method type for the generic method as inferred by javac at the + * call site + * @param state the visitor state + * @return a method type based on {@code methodTypeAtCallSite} but with some nested nullability + * annotations on type variables restored to match those on actual parameters passed at the + * call site */ @SuppressWarnings("ReferenceEquality") private Type.MethodType restoreNestedNullabilityForTypeVarArguments( @@ -1977,66 +1989,66 @@ private Type.MethodType restoreNestedNullabilityForTypeVarArguments( VisitorState state) { Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(invocationTree); if (methodSymbol.isVarArgs()) { - // TODO handle varargs methods - return methodTypeAtCallSite; - } - com.sun.tools.javac.util.List origArgTypes = origMethodType.getParameterTypes(); - com.sun.tools.javac.util.List callSiteArgTypes = methodTypeAtCallSite.getParameterTypes(); - List callArgs = invocationTree.getArguments(); - if (origArgTypes.size() != callSiteArgTypes.size() || callArgs.size() != origArgTypes.size()) { + // skip handling of varargs for now return methodTypeAtCallSite; } + com.sun.tools.javac.util.List genericMethodParamTypes = + origMethodType.getParameterTypes(); + com.sun.tools.javac.util.List callSiteParamTypes = + methodTypeAtCallSite.getParameterTypes(); + List actualParams = invocationTree.getArguments(); // use this map to store repaired substitutions for method type variables, to ensure we use the - // same repaired - // substitution for all occurrences of the same method type variable + // same repaired substitution for all occurrences of the same type variable Map repairedTopLevelSubstitutions = new HashMap<>(); ListBuffer updatedArgTypes = new ListBuffer<>(); boolean changed = false; - for (int i = 0; i < origArgTypes.size(); i++) { - Type updatedType = callSiteArgTypes.get(i); - Type origArgType = origArgTypes.get(i); - if (origArgType instanceof Type.TypeVar typeVar - && typeVar.tsym.owner == methodSymbol - && !(updatedType instanceof Type.TypeVar)) { + for (int i = 0; i < genericMethodParamTypes.size(); i++) { + Type callSiteParamType = callSiteParamTypes.get(i); + Type genericMethodParamType = genericMethodParamTypes.get(i); + // only attempt a repair when the generic method's parameter type is a type variable of the + // method + if (genericMethodParamType instanceof Type.TypeVar typeVar + && typeVar.tsym.owner == methodSymbol) { Symbol.TypeVariableSymbol typeVarSymbol = (Symbol.TypeVariableSymbol) typeVar.tsym; Type repairedSubstitution = repairedTopLevelSubstitutions.get(typeVarSymbol); if (repairedSubstitution != null) { - if (!state - .getTypes() - .isSameType( - state.getTypes().erasure(repairedSubstitution), - state.getTypes().erasure(updatedType))) { - // Inconsistent substitution for the same top-level type variable; bail out. - return methodTypeAtCallSite; - } - if (repairedSubstitution != updatedType) { + // re-use the previous substitution, to ensure consistency + if (repairedSubstitution != callSiteParamType) { changed = true; - updatedType = repairedSubstitution; + callSiteParamType = repairedSubstitution; } } else { // need to compute the substitution - Type actualArgType = getTreeType(callArgs.get(i), state); + Type actualArgType = getTreeType(actualParams.get(i), state); + // only handle cases of non-raw actual parameter types that have the same base type as the + // inferred parameter type at the call site if (actualArgType != null && !actualArgType.isRaw() && state .getTypes() .isSameType( state.getTypes().erasure(actualArgType), - state.getTypes().erasure(updatedType))) { + state.getTypes().erasure(callSiteParamType))) { + // restore explicit nested annotations from the actual parameter type to the call site + // parameter type (this will only apply to nested type variables within + // callSiteParamType) Type restoredType = TypeSubstitutionUtils.restoreExplicitNullabilityAnnotations( - actualArgType, updatedType, config, Collections.emptyMap()); + actualArgType, callSiteParamType, config, Collections.emptyMap()); + // remember the substitution so we use it consistently at other parameter positions repairedTopLevelSubstitutions.put(typeVarSymbol, restoredType); - if (restoredType != updatedType) { + if (restoredType != callSiteParamType) { changed = true; - updatedType = restoredType; + callSiteParamType = restoredType; } } else { - repairedTopLevelSubstitutions.put(typeVarSymbol, updatedType); + // remember that we did _not_ change anything, again for consistency across parameter + // positions + repairedTopLevelSubstitutions.put(typeVarSymbol, callSiteParamType); } } } - updatedArgTypes.append(updatedType); + updatedArgTypes.append(callSiteParamType); } if (!changed) { return methodTypeAtCallSite; diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java index 717d02b635..42aee80c40 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java @@ -1551,6 +1551,16 @@ > T acceptSup(T supplier) { void test() { acceptSup(sup); } + > void acceptTwoSup(T supplier1, T supplier2) { + } + Supplier sup2 = make2(); + Supplier make2() { + throw new RuntimeException(); + } + void test2() { + // BUG: Diagnostic contains: incompatible types: Supplier cannot be converted to @NonNull Supplier<@Nullable OuterT> + acceptTwoSup(sup, sup2); + } } """) .doTest();