updatedArgTypes = new ListBuffer<>();
+ boolean changed = false;
+ for (int i = 0; i < genericMethodParamTypes.size(); i++) {
+ Type callSiteParamType = callSiteParamTypes.get(i);
+ Type genericMethodParamType = genericMethodParamTypes.get(i);
+ ExpressionTree actualParam = actualParams.get(i);
+ // IMPORTANT: actualArgType is the result of getTreeType(), which will apply NullAway's own
+ // reasoning about nullability of nested types, e.g., by running generic method inference at
+ // nested levels of the expression. This is how actualArgType ends up having the "ground
+ // truth" information about nullability of nested types, which is used to repair the
+ // javac-determined call site type.
+ Type actualArgType =
+ genericsChecks.getTreeType(
+ actualParam,
+ state.withPath(pathWithLeaf(pathToInvocation, actualParam)),
+ calledFromDataflow);
+ if (actualArgType != null) {
+ Type repairedType = repairType(genericMethodParamType, actualArgType, callSiteParamType);
+ if (repairedType != callSiteParamType) {
+ changed = true;
+ callSiteParamType = repairedType;
+ }
+ }
+ updatedArgTypes.append(callSiteParamType);
+ }
+ if (!changed) {
+ return methodTypeAtCallSite;
+ }
+ return new Type.MethodType(
+ updatedArgTypes.toList(),
+ methodTypeAtCallSite.getReturnType(),
+ methodTypeAtCallSite.getThrownTypes(),
+ methodTypeAtCallSite.tsym);
+ }
+
+ private Type repairType(Type genericMethodType, Type actualArgType, Type callSiteType) {
+ return genericMethodType.accept(this, new RepairContext(actualArgType, callSiteType));
+ }
+
+ @Override
+ public Type visitTypeVar(Type.TypeVar typeVar, RepairContext context) {
+ // only repair type variables on the invoked method
+ if (Objects.equals(typeVar.tsym.owner, methodSymbol)) {
+ return repairTypeVarSubstitution(typeVar, context.actualArgType(), context.callSiteType());
+ }
+ return context.callSiteType();
+ }
+
+ /**
+ * when this method is called, {@code genericClassType} appears within some level a parameter type
+ * for the generic method, {@code context.actualArgType()} is the (NullAway-determined) type of
+ * the actual parameter at the same nesting level, and {@code context.callSiteType()} is the
+ * javac-determined type for the parameter at the same nesting level.
+ *
+ * This method recurses through the type arguments of {@code genericClassType}, invoking {@link
+ * #repairType(Type, Type, Type)} passing the corresponding type arguments from the actual
+ * parameter type and javac-determined call site type. If any repair occurs, returns the repaired
+ * type as the new type to be used at this level. (The actual repair logic only kicks in when
+ * visiting a nested type variable.)
+ */
+ @SuppressWarnings("ReferenceEquality")
+ @Override
+ public Type visitClassType(Type.ClassType genericClassType, RepairContext context) {
+ if (!(context.actualArgType() instanceof Type.ClassType)
+ || !(context.callSiteType() instanceof Type.ClassType callSiteClassType)) {
+ return context.callSiteType();
+ }
+ // the actual type can be a subtype of the javac-inferred call-site type, so convert to the
+ // supertype
+ Type.ClassType actualClassType =
+ (Type.ClassType)
+ TypeSubstitutionUtils.asSuper(
+ state.getTypes(),
+ context.actualArgType(),
+ (Symbol.ClassSymbol) callSiteClassType.tsym,
+ config);
+ if (actualClassType == null) {
+ return context.callSiteType();
+ }
+ List genericTypeArgs = genericClassType.getTypeArguments();
+ List actualTypeArgs = actualClassType.getTypeArguments();
+ List callSiteTypeArgs = callSiteClassType.getTypeArguments();
+ if (genericTypeArgs.size() != actualTypeArgs.size()
+ || genericTypeArgs.size() != callSiteTypeArgs.size()) {
+ return context.callSiteType();
+ }
+ boolean changed = false;
+ ListBuffer updatedTypeArgs = new ListBuffer<>();
+ for (int i = 0; i < genericTypeArgs.size(); i++) {
+ Type callSiteTypeArg = callSiteTypeArgs.get(i);
+ Type repairedTypeArg =
+ repairType(genericTypeArgs.get(i), actualTypeArgs.get(i), callSiteTypeArg);
+ if (repairedTypeArg != callSiteTypeArg) {
+ changed = true;
+ }
+ updatedTypeArgs.append(repairedTypeArg);
+ }
+ Type enclosingType = callSiteClassType.getEnclosingType();
+ Type repairedEnclosingType =
+ repairType(
+ genericClassType.getEnclosingType(), actualClassType.getEnclosingType(), enclosingType);
+ if (repairedEnclosingType != enclosingType) {
+ changed = true;
+ }
+ return changed
+ ? TypeMetadataBuilder.TYPE_METADATA_BUILDER.createClassType(
+ callSiteClassType, repairedEnclosingType, updatedTypeArgs.toList())
+ : context.callSiteType();
+ }
+
+ /**
+ * when this method is called, {@code genericArrayType} appears within some level a parameter type
+ * for the generic method, {@code context.actualArgType()} is the (NullAway-determined) type of
+ * the actual parameter at the same nesting level, and {@code context.callSiteType()} is the
+ * javac-determined type for the parameter at the same nesting level.
+ *
+ * This method recurses to the component type of {@code genericArrayType}, invoking {@link
+ * #repairType(Type, Type, Type)} passing the corresponding component type from the actual
+ * parameter type and javac-determined call site type. If any repair occurs, returns the repaired
+ * type as the new type to be used at this level. (The actual repair logic only kicks in when
+ * visiting a nested type variable.)
+ */
+ // suppress since we want to check for a specific identical Type object to check for changes
+ @SuppressWarnings("ReferenceEquality")
+ @Override
+ public Type visitArrayType(Type.ArrayType genericArrayType, RepairContext context) {
+ if (!(context.actualArgType() instanceof Type.ArrayType actualArrayType)
+ || !(context.callSiteType() instanceof Type.ArrayType callSiteArrayType)) {
+ return context.callSiteType();
+ }
+ Type callSiteElemType = callSiteArrayType.getComponentType();
+ Type repairedElemType =
+ repairType(
+ genericArrayType.getComponentType(),
+ actualArrayType.getComponentType(),
+ callSiteElemType);
+ return repairedElemType != callSiteElemType
+ ? TypeMetadataBuilder.TYPE_METADATA_BUILDER.createArrayType(
+ callSiteArrayType, repairedElemType)
+ : context.callSiteType();
+ }
+
+ @Override
+ public Type visitType(Type type, RepairContext context) {
+ return context.callSiteType();
+ }
+
+ /**
+ * For a javac-determined call site type passed in the position of a type variable from the
+ * generic method, update nested types in the call site type based on the corresponding nested
+ * types from the actual parameter.
+ *
+ * @param typeVar the type variable from the generic method
+ * @param actualArgType the actual parameter type passed in the type variable's position at the
+ * call site
+ * @param callSiteType the type javac determined is passed in the type variable's position at the
+ * call site
+ * @return updated type to use at the position in the call site, or {@code callSiteType} if no
+ * repair is needed
+ */
+ private Type repairTypeVarSubstitution(
+ Type.TypeVar typeVar, Type actualArgType, Type callSiteType) {
+ Symbol.TypeVariableSymbol typeVarSymbol = (Symbol.TypeVariableSymbol) typeVar.tsym;
+ return repairedSubstitutions.computeIfAbsent(
+ typeVarSymbol,
+ (unused) -> {
+ Type repairedSubstitution = callSiteType;
+ if (!actualArgType.isRaw() && !callSiteType.isRaw()) {
+ repairedSubstitution =
+ repairNestedTypeVarSubstitutionFromActual(actualArgType, callSiteType);
+ }
+ return repairedSubstitution;
+ });
+ }
+
+ /**
+ * Repairs nested annotations in {@code callSiteType} using the nested types from {@code
+ * actualArgType}, while preserving any direct annotations on {@code callSiteType}.
+ *
+ *
So, for class types, if {@code actualArgType} is {@code Foo<@Nullable Bar>} and {@code
+ * callSiteType} is {@code @Nullable Foo}, we return {@code @Nullable Foo<@Nullable Bar>},
+ * using the top-level type from {@code callSiteType} and the type argument from {@code
+ * actualArgType}.
+ *
+ * Similarly, for array types, if {@code actualArgType} is {@code @Nullable Foo []} and {@code
+ * callSiteType} is {@code Foo @Nullable []}, we return {@code @Nullable Foo @Nullable []}.
+ */
+ private Type repairNestedTypeVarSubstitutionFromActual(Type actualArgType, Type callSiteType) {
+ // only handle cases where base types are identical for now
+ if (!ASTHelpers.isSameType(actualArgType, callSiteType, state)) {
+ return callSiteType;
+ }
+ if (actualArgType instanceof Type.ClassType actualClassType
+ && callSiteType instanceof Type.ClassType callSiteClassType) {
+ List actualTypeArgs = actualClassType.getTypeArguments();
+ if (actualTypeArgs.isEmpty()) {
+ return callSiteType;
+ }
+ // use call site type with type arguments from actual
+ return TypeMetadataBuilder.TYPE_METADATA_BUILDER.createClassType(
+ callSiteClassType, callSiteClassType.getEnclosingType(), actualTypeArgs);
+ }
+ if (actualArgType instanceof Type.ArrayType actualArrayType
+ && callSiteType instanceof Type.ArrayType callSiteArrayType) {
+ // use call site type with component type from actual
+ return TypeMetadataBuilder.TYPE_METADATA_BUILDER.createArrayType(
+ callSiteArrayType, actualArrayType.getComponentType());
+ }
+ return callSiteType;
+ }
+
+ /**
+ * The two types being compared while recursively walking the declared generic method parameter
+ * type. At each recursive step, the visitor uses {@code actualArgType} as the "ground truth" of
+ * nested nullability annotations and applies any repair to the corresponding subtree of {@code
+ * callSiteType}.
+ *
+ * @param actualArgType the subtree of the actual argument type aligned with the current declared
+ * generic method parameter subtree
+ * @param callSiteType the subtree of javac's inferred call-site parameter type to repair
+ */
+ record RepairContext(Type actualArgType, Type callSiteType) {}
+}
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 4b53cf1fe1..8cb3c5ecb5 100644
--- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java
+++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java
@@ -1572,6 +1572,9 @@ > T acceptSup(T supplier) {
return supplier;
}
void test() {
+ // Here, javac computes the formal parameter type as Supplier.
+ // Our repair updates the type to Supplier<@Nullable OuterT>, matching
+ // the actual parameter, so we get no error.
acceptSup(sup);
}
> void acceptTwoSup(T supplier1, T supplier2) {
@@ -1580,6 +1583,7 @@ > void acceptTwoSup(T supplier1, T supplier2) {
Supplier make2() {
throw new RuntimeException();
}
+ // tests that our repair computes a consistent substitution for the type variables
void test2() {
// BUG: Diagnostic contains: incompatible types: Supplier cannot be converted to Supplier<@Nullable OuterT>
acceptTwoSup(sup, sup2);
@@ -1685,6 +1689,60 @@ static class Foo {
.doTest();
}
+ @Test
+ public void caffeineNestedArgToGenericMethod() {
+ makeHelperWithInferenceFailureWarning()
+ .addSourceLines(
+ "Test.java",
+ """
+ import org.jspecify.annotations.NonNull;
+ import org.jspecify.annotations.NullMarked;
+ import org.jspecify.annotations.Nullable;
+ import java.util.Map;
+ import java.util.concurrent.CompletableFuture;
+ @NullMarked
+ class Test {
+ static interface Cache {
+ Policy policy();
+ }
+ static interface Policy {
+ Map> refreshes();
+ }
+ static void m(@Nullable Map map) {}
+ void test(Cache cache) {
+ // javac computes the formal parameter type as @Nullable Map>,
+ // presumably based on the @Nullable Object type argument for cache.
+ // NullAway determines the type of the actual parameter correctly as
+ // Map> (due to the @NonNull annotation on V in the signature for policy).
+ // The type repair in NestedTypeVarSubstitutionRepairVisitor fixes the javac type so we don't report
+ // an error here.
+ m(cache.policy().refreshes());
+ }
+ }""")
+ .doTest();
+ }
+
+ @Test
+ public void nestedGenericMethodRepairPreservesTopLevelNullability() {
+ makeHelperWithInferenceFailureWarning()
+ .addSourceLines(
+ "Test.java",
+ """
+ import org.jspecify.annotations.NullMarked;
+ import org.jspecify.annotations.Nullable;
+ import java.util.concurrent.CompletableFuture;
+ @NullMarked
+ class Test {
+ static class Box {}
+ static void accept(Box<@Nullable T> box) {}
+ void test(Box> box) {
+ // BUG: Diagnostic contains: inference failure: type variable T constrained to be both @NonNull and @Nullable
+ accept(box);
+ }
+ }""")
+ .doTest();
+ }
+
private CompilationTestHelper makeHelper() {
return makeTestHelperWithArgs(
JSpecifyJavacConfig.withJSpecifyModeArgs(