From b961a07bacc36e5ce72491dc3712cd4056bf195f Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 9 Aug 2026 16:30:27 -0700 Subject: [PATCH 1/3] Generalize generic-call inference internals to call expressions --- .../nullaway/generics/GenericsChecks.java | 360 ++++++++---------- 1 file changed, 165 insertions(+), 195 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 cae79a1213..b959fc317a 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -101,8 +101,8 @@ private InferenceFailure(@Nullable String errorMessage) { * its type argument nullability. The call must not have any explicit type arguments. If a tree is * not present as a key in this map, it means inference has not yet been attempted for that call. */ - private final Map - inferredTypeVarNullabilityForGenericCalls = new LinkedHashMap<>(); + private final Map inferredTypeVarNullabilityForGenericCalls = + new LinkedHashMap<>(); /** * Maps poly expressions for which we have computed a context-derived type to that type, if @@ -753,7 +753,7 @@ private void reportInvalidOverridingMethodParamTypeError( if (TreeInfo.isDiamond((JCTree) newClassTree)) { if (newClassTree.getClassBody() != null) { // Keep existing behavior for diamond anonymous classes, which are not yet fully - // supported. Tracked in https://github.com/uber/NullAway/issues/1475 + // supported. Tracked in https://github.com/uber/NullAway/issues/1475 return null; } // For constructor calls using diamond operator, infer from assignment context. @@ -836,10 +836,9 @@ private void reportInvalidOverridingMethodParamTypeError( // call. We invoke getEnclosingTypeForCallExpression, which will run // inference if needed, and then recompute the type as a member of the returned // enclosing type - Symbol.MethodSymbol symbol = castToNonNull(ASTHelpers.getSymbol(invocationTree)); Type.MethodType methodType = - getMethodTypeForInvocation( - symbol, invocationTree, state.getPath(), state, calledFromDataflow); + getInferenceExecutableType( + invocationTree, state.getPath(), state, calledFromDataflow); // restore explicit annotations from the return type Type returnType = methodType.getReturnType(); result = @@ -928,6 +927,16 @@ private static boolean hasInferredClassTypeArguments(NewClassTree newClassTree) return newClassType != null && !newClassType.getTypeArguments().isEmpty(); } + private static boolean isCallNeedingInference(ExpressionTree expressionTree) { + if (expressionTree instanceof MethodInvocationTree methodInvocation) { + Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(methodInvocation); + return methodSymbol != null + && methodSymbol.type instanceof Type.ForAll + && methodInvocation.getTypeArguments().isEmpty(); + } + return false; + } + /** * Gets the inferred type of lambda parameter, if the lambda was passed to a generic method and * its type was inferred previously @@ -1117,10 +1126,10 @@ public void registerVarLocalDeclaration(VariableTree tree) { boolean assignedToLocal, VisitorState state, boolean calledFromDataflow) { - if (isGenericCallNeedingInference(rhsTree)) { - return inferGenericMethodCallType( + if (isCallNeedingInference(rhsTree)) { + return inferCallType( state.withPath(pathToRhs), - (MethodInvocationTree) rhsTree, + rhsTree, pathToRhs, typeFromAssignmentContext, assignedToLocal, @@ -1154,63 +1163,68 @@ private ConstraintSolver makeSolver(VisitorState state, NullAway analysis) { } /** - * Infers the type of a generic method call based on the assignment context. Side-effects the - * #inferredSubstitutionsForGenericMethodCalls map with the inferred type. + * Infers the type of a generic method call or diamond constructor call based on its assignment + * context. Side-effects the cache of inferred nullability substitutions for omitted type + * arguments. * * @param state the visitor state - * @param invocationTree the method invocation tree representing the call to a generic method - * @param path the tree path to the invocationTree if available and possibly distinct from {@code + * @param callTree the call expression representing the generic method call or diamond constructor + * call + * @param path the tree path to {@code callTree} if available and possibly distinct from {@code * state.getPath()} * @param typeFromAssignmentContext the type being "assigned to" in the assignment context - * @param assignedToLocal true if the method call result is assigned to a local variable, false - * otherwise + * @param assignedToLocal true if the call result is assigned to a local variable, false otherwise * @param calledFromDataflow true if this inference is being done as part of dataflow analysis - * @return the type of the method call after inference + * @return the type of the call after inference */ - private Type inferGenericMethodCallType( + private Type inferCallType( VisitorState state, - MethodInvocationTree invocationTree, + ExpressionTree callTree, @Nullable TreePath path, @Nullable Type typeFromAssignmentContext, boolean assignedToLocal, boolean calledFromDataflow) { - Verify.verify(isGenericCallNeedingInference(invocationTree)); - Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(invocationTree); + Verify.verify(isCallNeedingInference(callTree)); Map typeVarNullability = null; - CallInferenceResult result = inferredTypeVarNullabilityForGenericCalls.get(invocationTree); - if (result == null) { // have not yet attempted inference for this call + CallInferenceResult result = inferredTypeVarNullabilityForGenericCalls.get(callTree); + if (result == null) { result = runInferenceForCall( state, path, - invocationTree, + callTree, typeFromAssignmentContext, assignedToLocal, calledFromDataflow); } - if (result instanceof InferenceSuccess) { - typeVarNullability = ((InferenceSuccess) result).typeVarNullability; + if (result instanceof InferenceSuccess success) { + typeVarNullability = success.typeVarNullability; + } + Type typeAtCallSite = castToNonNull(ASTHelpers.getType(callTree)); + if (callTree instanceof MethodInvocationTree) { + Type methodReturnType = + getInferenceExecutableType(callTree, path, state, calledFromDataflow).getReturnType(); + return TypeSubstitutionUtils.updateTypeWithInferredNullability( + typeAtCallSite, methodReturnType, typeVarNullability, state, config); } - Type methodReturnType = - getMethodTypeForInvocation(methodSymbol, invocationTree, path, state, calledFromDataflow) - .getReturnType(); - Type returnTypeAtCallSite = castToNonNull(ASTHelpers.getType(invocationTree)); + Verify.verify(callTree instanceof NewClassTree); + Symbol.MethodSymbol ctorSymbol = getMethodSymbolForCall(callTree); + Type constructedTypeWithTypeVars = ctorSymbol.owner.type; return TypeSubstitutionUtils.updateTypeWithInferredNullability( - returnTypeAtCallSite, methodReturnType, typeVarNullability, state, config); + typeAtCallSite, constructedTypeWithTypeVars, typeVarNullability, state, config); } /** - * Runs inference for a generic method call, side-effecting the + * Runs inference for a generic call, side-effecting the * #inferredTypeVarNullabilityForGenericCalls map with the result. * * @param state the visitor state - * @param path the tree path to the invocationTree if available and possibly distinct from {@code + * @param path the tree path to the call tree if available and possibly distinct from {@code * state.getPath()} - * @param invocationTree the method invocation tree representing the call to a generic method + * @param callTree the method invocation tree or constructor call tree representing the call * @param typeFromAssignmentContext the type being "assigned to" in the assignment context, or * {@code null} if the type is unavailable or the method result is not assigned anywhere - * @param assignedToLocal true if the method call result is assigned to a local variable, false - * otherwise + * @param assignedToLocal true if the call result is assigned to a local variable, false otherwise * @param calledFromDataflow true if this inference is being done as part of dataflow analysis * @return the inference result, either success with inferred type variable nullability or failure * with an error message @@ -1218,16 +1232,13 @@ private Type inferGenericMethodCallType( private CallInferenceResult runInferenceForCall( VisitorState state, @Nullable TreePath path, - MethodInvocationTree invocationTree, + ExpressionTree callTree, @Nullable Type typeFromAssignmentContext, boolean assignedToLocal, boolean calledFromDataflow) { - Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(invocationTree); ConstraintSolver solver = makeSolver(state, analysis); - // allInvocations tracks the top-level invocations and any nested invocations that also - // require inference - Set allInvocations = new LinkedHashSet<>(); - allInvocations.add(invocationTree); + Set allCalls = new LinkedHashSet<>(); + allCalls.add(callTree); Map typeVarNullability; try { generateConstraintsForCall( @@ -1236,31 +1247,23 @@ private CallInferenceResult runInferenceForCall( typeFromAssignmentContext, assignedToLocal, solver, - methodSymbol, - invocationTree, - allInvocations, + callTree, + allCalls, calledFromDataflow); typeVarNullability = new HashMap<>(solver.solve()); - // The solver only computes a solution for variables that appear in constraints. For - // unconstrained variables, treat them as NONNULL, consistent with solver behavior for - // unconstrained variables that do appear in the constraint graph. - for (int i = 0; i < methodSymbol.getTypeParameters().size(); i++) { - Symbol.TypeVariableSymbol typeVar = methodSymbol.getTypeParameters().get(i); + for (Symbol.TypeVariableSymbol typeVar : getCallTypeParameters(callTree)) { typeVarNullability.putIfAbsent(typeVar, ConstraintSolver.InferredNullability.NONNULL); } InferenceSuccess successResult = new InferenceSuccess(typeVarNullability); - // don't cache result if we were called from dataflow, since the result may rely on dataflow - // facts that do not reflect the fixed point if (!calledFromDataflow) { - for (MethodInvocationTree invTree : allInvocations) { - inferredTypeVarNullabilityForGenericCalls.put(invTree, successResult); + for (Tree inferredCall : allCalls) { + inferredTypeVarNullabilityForGenericCalls.put(inferredCall, successResult); } // Store inferred types for lambda or method reference arguments - Type.MethodType methodType = - getMethodTypeForInvocation( - methodSymbol, invocationTree, path, state, calledFromDataflow); - new InvocationArguments(invocationTree, methodType) + Type.MethodType callMethodType = + getInferenceExecutableType(callTree, path, state, calledFromDataflow); + new InvocationArguments(callTree, callMethodType) .forEach( (argument, argPos, formalParamType, unused) -> { if (argument instanceof LambdaExpressionTree @@ -1291,14 +1294,14 @@ private CallInferenceResult runInferenceForCall( ErrorMessage.MessageTypes.GENERIC_INFERENCE_FAILURE, inferenceFailureMessage); state.reportMatch( errorBuilder.createErrorDescription( - errorMessage, analysis.buildDescription(invocationTree), state, null)); + errorMessage, analysis.buildDescription(callTree), state, null)); } InferenceFailure failureResult = new InferenceFailure(inferenceFailureMessage); // don't cache result if we were called from dataflow, since the result may rely on dataflow // facts that do not reflect the fixed point if (!calledFromDataflow) { - for (MethodInvocationTree invTree : allInvocations) { - inferredTypeVarNullabilityForGenericCalls.put(invTree, failureResult); + for (Tree inferredCall : allCalls) { + inferredTypeVarNullabilityForGenericCalls.put(inferredCall, failureResult); } } return failureResult; @@ -1311,49 +1314,62 @@ private String inferenceFailureMessage(UnsatisfiableConstraintsException e) { e.getTypeVariable()); } + private com.sun.tools.javac.util.List getCallTypeParameters( + ExpressionTree callTree) { + if (callTree instanceof MethodInvocationTree invocationTree) { + return ASTHelpers.getSymbol(invocationTree).getTypeParameters(); + } + Verify.verify(callTree instanceof NewClassTree); + Symbol.MethodSymbol ctorSymbol = getMethodSymbolForCall(callTree); + return ctorSymbol.owner.getTypeParameters(); + } + /** - * Gets the type of a method at an invocation, substituting type arguments from the receiver and - * applying any handler-provided models. - * - *

Receiver substitution is necessary when an enclosing class type variable appears in the - * method signature. For example, for a method returning {@code T} on a receiver {@code - * Foo<@Nullable Object>}, the invocation return type is {@code @Nullable Object}, not the - * declaration-site type variable {@code T}. + * Gets the declaration-site executable type for inference, substituting type arguments from a + * method invocation's receiver and applying any handler-provided models. */ - private Type.MethodType getMethodTypeForInvocation( - Symbol.MethodSymbol methodSymbol, - MethodInvocationTree methodInvocationTree, + private Type.MethodType getInferenceExecutableType( + ExpressionTree callTree, @Nullable TreePath path, VisitorState state, boolean calledFromDataflow) { - Type invokedMethodType = methodSymbol.type; - Type enclosingType = - getEnclosingTypeForCallExpression( - methodSymbol, methodInvocationTree, path, state, calledFromDataflow); - if (enclosingType != null) { - invokedMethodType = - TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, methodSymbol, config); + Symbol.MethodSymbol methodSymbol = getMethodSymbolForCall(callTree); + Type executableType = methodSymbol.type; + if (callTree instanceof MethodInvocationTree invocationTree) { + Type enclosingType = + getEnclosingTypeForCallExpression( + methodSymbol, invocationTree, path, state, calledFromDataflow); + if (enclosingType != null) { + executableType = + TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, methodSymbol, config); + } } return handler.onOverrideMethodType( - methodSymbol, invokedMethodType.asMethodType(), state, methodInvocationTree); + methodSymbol, + executableType.asMethodType(), + state, + callTree instanceof MethodInvocationTree invocationTree ? invocationTree : null); + } + + private Symbol.MethodSymbol getMethodSymbolForCall(ExpressionTree callTree) { + return (Symbol.MethodSymbol) castToNonNull(ASTHelpers.getSymbol(callTree)); } /** - * Generates inference constraints for a generic method call, including nested calls. + * Generates inference constraints for a generic call, including nested generic method calls and + * diamond constructor calls. * * @param state the visitor state - * @param path the tree path to the invocationTree if available and possibly distinct from {@code + * @param path the tree path to the call tree if available and possibly distinct from {@code * state.getPath()} * @param typeFromAssignmentContext the type being "assigned to" in the assignment context of the - * call, or {@code null} if the type is unavailable or the method result is not assigned + * call, or {@code null} if the type is unavailable or the call result is not assigned * anywhere - * @param assignedToLocal whether the method call result is assigned to a local variable + * @param assignedToLocal whether the call result is assigned to a local variable * @param solver the constraint solver - * @param methodSymbol the symbol for the method being called - * @param methodInvocationTree the method invocation tree representing the call - * @param allInvocations a set of all method invocations that require inference, including nested - * ones. This is an output parameter that gets mutated while generating the constraints to add - * nested invocations. + * @param callTree the call tree representing the generic method call or diamond constructor call + * @param allCalls a set of all calls that require inference, including nested ones. This is an + * output parameter that gets mutated while generating the constraints to add nested calls. * @param calledFromDataflow whether this method is being called from dataflow analysis * @throws UnsatisfiableConstraintsException if the constraints are determined to be unsatisfiable */ @@ -1363,30 +1379,29 @@ private void generateConstraintsForCall( @Nullable Type typeFromAssignmentContext, boolean assignedToLocal, ConstraintSolver solver, - Symbol.MethodSymbol methodSymbol, - MethodInvocationTree methodInvocationTree, - Set allInvocations, + ExpressionTree callTree, + Set allCalls, boolean calledFromDataflow) throws UnsatisfiableConstraintsException { + Symbol.MethodSymbol methodSymbol = getMethodSymbolForCall(callTree); Type.MethodType methodType = - getMethodTypeForInvocation( - methodSymbol, methodInvocationTree, path, state, calledFromDataflow); - // first, handle the return type flow + getInferenceExecutableType(callTree, path, state, calledFromDataflow); if (typeFromAssignmentContext != null) { - solver.addSubtypeConstraint( - methodType.getReturnType(), typeFromAssignmentContext, assignedToLocal); - } - // then, handle parameters - TreePath pathToInvocation = - path != null ? path : pathWithLeaf(state.getPath(), methodInvocationTree); - new InvocationArguments(methodInvocationTree, methodType) + Type callResultType = + (callTree instanceof MethodInvocationTree) + ? methodType.getReturnType() + : methodSymbol.owner.type; + solver.addSubtypeConstraint(callResultType, typeFromAssignmentContext, assignedToLocal); + } + TreePath pathToCall = path != null ? path : pathWithLeaf(state.getPath(), callTree); + new InvocationArguments(callTree, methodType) .forEach( (argument, argPos, formalParamType, unused) -> { - TreePath pathToArgument = new TreePath(pathToInvocation, argument); + TreePath pathToArgument = new TreePath(pathToCall, argument); generateConstraintsForPseudoAssignment( state.withPath(pathToArgument), solver, - allInvocations, + allCalls, argument, formalParamType, calledFromDataflow); @@ -1399,9 +1414,8 @@ private void generateConstraintsForCall( * * @param state the visitor state * @param solver the constraint solver - * @param allInvocations a set of all method invocations that require inference, including nested - * ones. This is an output parameter that gets mutated while generating the constraints to add - * nested invocations. + * @param allCalls a set of all calls that require inference, including nested ones. This is an + * output parameter that gets mutated while generating the constraints to add nested calls. * @param rhsExpr the right-hand side expression of the pseudo-assignment * @param lhsType the left-hand side type of the pseudo-assignment * @param calledFromDataflow whether this method is being called from dataflow analysis @@ -1409,7 +1423,7 @@ private void generateConstraintsForCall( private void generateConstraintsForPseudoAssignment( VisitorState state, ConstraintSolver solver, - Set allInvocations, + Set allCalls, ExpressionTree rhsExpr, Type lhsType, boolean calledFromDataflow) { @@ -1419,20 +1433,10 @@ private void generateConstraintsForPseudoAssignment( state = exprTreeAndState.state(); // if the parameter is itself a generic call requiring inference, generate constraints for // that call - if (isGenericCallNeedingInference(rhsExpr)) { - MethodInvocationTree invTree = (MethodInvocationTree) rhsExpr; - Symbol.MethodSymbol symbol = ASTHelpers.getSymbol(invTree); - allInvocations.add(invTree); + if (isCallNeedingInference(rhsExpr)) { + allCalls.add(rhsExpr); generateConstraintsForCall( - state, - state.getPath(), - lhsType, - false, - solver, - symbol, - invTree, - allInvocations, - calledFromDataflow); + state, state.getPath(), lhsType, false, solver, rhsExpr, allCalls, calledFromDataflow); } else if (rhsExpr instanceof ConditionalExpressionTree conditionalExpressionTree) { // generate constraints for both the true and false sub-expressions of the conditional // expression @@ -1441,7 +1445,7 @@ private void generateConstraintsForPseudoAssignment( generateConstraintsForPseudoAssignment( state.withPath(pathToTrueExpression), solver, - allInvocations, + allCalls, trueExpression, lhsType, calledFromDataflow); @@ -1450,13 +1454,13 @@ private void generateConstraintsForPseudoAssignment( generateConstraintsForPseudoAssignment( state.withPath(pathToFalseExpression), solver, - allInvocations, + allCalls, falseExpression, lhsType, calledFromDataflow); } else if (rhsExpr instanceof LambdaExpressionTree lambda) { handleLambdaInGenericMethodInference( - state, state.getPath(), solver, allInvocations, lhsType, lambda, calledFromDataflow); + state, state.getPath(), solver, allCalls, lhsType, lambda, calledFromDataflow); } else if (rhsExpr instanceof MemberReferenceTree memberReferenceTree) { handleMethodRefInGenericMethodInference(state, solver, lhsType, memberReferenceTree); } else { // all other cases @@ -1475,12 +1479,11 @@ private void generateConstraintsForPseudoAssignment( * is a method invocation then recursively call generateConstraintsForCall * * @param state the visitor state - * @param path the tree path to the invocationTree if available and possibly distinct from {@code + * @param path the tree path to the enclosing call if available and possibly distinct from {@code * state.getPath()} * @param solver the constraint solver - * @param allInvocations a set of all method invocations that require inference, including nested - * ones. This is an output parameter that gets mutated while generating the constraints to add - * nested invocations. + * @param allCalls a set of all calls that require inference, including nested ones. This is an + * output parameter that gets mutated while generating the constraints to add nested calls. * @param lhsType the type to which the lambda is being assigned * @param lambda The lambda argument * @param calledFromDataflow whether this method is being called from dataflow analysis @@ -1489,7 +1492,7 @@ private void handleLambdaInGenericMethodInference( VisitorState state, @Nullable TreePath path, ConstraintSolver solver, - Set allInvocations, + Set allCalls, Type lhsType, LambdaExpressionTree lambda, boolean calledFromDataflow) { @@ -1513,7 +1516,7 @@ private void handleLambdaInGenericMethodInference( generateConstraintsForPseudoAssignment( state.withPath(returnedExpressionPath), solver, - allInvocations, + allCalls, returnedExpression, fiReturnType, calledFromDataflow); @@ -1528,7 +1531,7 @@ private void handleLambdaInGenericMethodInference( generateConstraintsForPseudoAssignment( state.withPath(returnExprPath), solver, - allInvocations, + allCalls, returnExpr, fiReturnType, calledFromDataflow); @@ -1812,20 +1815,6 @@ private Type updateTypeWithNullness( } } - private static boolean isGenericCallNeedingInference(ExpressionTree argument) { - // For now, we only support calls to generic methods. - // TODO also support calls to generic constructors that use the diamond operator - // https://github.com/uber/NullAway/issues/1470 - if (argument instanceof MethodInvocationTree methodInvocation) { - Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(methodInvocation); - // true for generic method calls with no explicit type arguments - return methodSymbol != null - && methodSymbol.type instanceof Type.ForAll - && methodInvocation.getTypeArguments().isEmpty(); - } - return false; - } - /** * Checks that the nullability of type parameters for a returned expression matches that of the * type parameters of the enclosing method's return type. @@ -1849,11 +1838,11 @@ public void checkTypeParameterNullnessForFunctionReturnType( TreePath pathToRetExpr = new TreePath(state.getPath(), retExpr); Type returnExpressionType = getTreeType(retExpr, state.withPath(pathToRetExpr)); if (returnExpressionType != null) { - if (isGenericCallNeedingInference(retExpr)) { + if (isCallNeedingInference(retExpr)) { returnExpressionType = - inferGenericMethodCallType( + inferCallType( state.withPath(pathToRetExpr), - (MethodInvocationTree) retExpr, + retExpr, pathToRetExpr, formalReturnType, false, @@ -2143,7 +2132,7 @@ private TargetTypeAndAssignmentKind getTargetTypeFromParentContext( } // 2c. `foo(..., [expr]);` => target is called method's formal argument type if (parent instanceof MethodInvocationTree parentInvocation) { - if (isGenericCallNeedingInference(parentInvocation)) { + if (isCallNeedingInference(parentInvocation)) { // The parent invocation's formal parameter type is still part of the inference problem, not // a solved target type. Generic method inference will handle this expression from the // parent call side. @@ -2324,13 +2313,11 @@ public void compareGenericTypeParameterNullabilityForCall( actualParameterType = getTreeType(currentActualParam, state.withPath(pathToParam)); } if (actualParameterType != null) { - if (isGenericCallNeedingInference(currentActualParam)) { - // infer the type of the method call based on the assignment context - // and the formal parameter type + if (isCallNeedingInference(currentActualParam)) { actualParameterType = - inferGenericMethodCallType( + inferCallType( state.withPath(pathToParam), - (MethodInvocationTree) currentActualParam, + currentActualParam, pathToParam, formalParameter, false, @@ -2375,7 +2362,7 @@ private Type.MethodType getInferredMethodTypeForGenericMethodReference( } Tree parentTree = parentPath != null ? parentPath.getLeaf() : null; if (parentTree instanceof MethodInvocationTree methodInvocationTree - && isGenericCallNeedingInference(methodInvocationTree)) { + && isCallNeedingInference(methodInvocationTree)) { CallInferenceResult inferenceResult = inferredTypeVarNullabilityForGenericCalls.get(methodInvocationTree); if (inferenceResult instanceof InferenceSuccess successResult) { @@ -2616,15 +2603,15 @@ private Type substituteTypeArgsInGenericMethodType( CallInferenceResult result = inferredTypeVarNullabilityForGenericCalls.get(tree); if (result == null) { // have not yet attempted inference for this call - InvocationAndContext invocationAndType = + CallAndContext invocationAndType = path == null - ? new InvocationAndContext(invocationTree, null, false) - : getInvocationAndContextForInference(path, state, calledFromDataflow); + ? new CallAndContext(invocationTree, null, false) + : getCallAndContextForInference(path, state, calledFromDataflow); result = runInferenceForCall( state, path, - invocationAndType.invocation, + invocationAndType.call, invocationAndType.typeFromAssignmentContext, invocationAndType.assignedToLocal, calledFromDataflow); @@ -2686,10 +2673,8 @@ private Type.MethodType restoreNestedNullabilityForTypeVarArguments( * An invocation of a generic method, and the corresponding information about its assignment * context, for the purposes of inference. */ - private record InvocationAndContext( - MethodInvocationTree invocation, - @Nullable Type typeFromAssignmentContext, - boolean assignedToLocal) {} + private record CallAndContext( + ExpressionTree call, @Nullable Type typeFromAssignmentContext, boolean assignedToLocal) {} /** * Given a {@link TreePath} to an invocation of a generic method, collect information about the @@ -2709,9 +2694,9 @@ private record InvocationAndContext( * assignment context information. If no assignment context is available, the * typeFromAssignmentContext field of the result will be null. */ - private InvocationAndContext getInvocationAndContextForInference( + private CallAndContext getCallAndContextForInference( TreePath path, VisitorState state, boolean calledFromDataflow) { - MethodInvocationTree invocation = (MethodInvocationTree) path.getLeaf(); + ExpressionTree call = (ExpressionTree) path.getLeaf(); TreePath parentPath = path.getParentPath(); Tree parent = parentPath.getLeaf(); while (parent instanceof ParenthesizedTree) { @@ -2721,53 +2706,39 @@ private InvocationAndContext getInvocationAndContextForInference( if (parent instanceof AssignmentTree || parent instanceof VariableTree) { TargetTypeAndAssignmentKind targetTypeAndAssignmentKind = getTargetTypeForAssignmentContext(parent, state.withPath(parentPath), calledFromDataflow); - return new InvocationAndContext( - invocation, + return new CallAndContext( + call, targetTypeAndAssignmentKind.typeFromAssignmentContext(), targetTypeAndAssignmentKind.assignedToLocal()); } else if (parent instanceof ReturnTree) { - // find the enclosing method and return its return type TreePath enclosingMethodOrLambda = NullabilityUtil.findEnclosingMethodOrLambdaOrInitializer(parentPath); - // TODO handle lambdas; https://github.com/uber/NullAway/issues/1288 if (enclosingMethodOrLambda != null && enclosingMethodOrLambda.getLeaf() instanceof MethodTree enclosingMethod) { Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(enclosingMethod); if (methodSymbol != null) { - return new InvocationAndContext(invocation, methodSymbol.getReturnType(), false); + return new CallAndContext(call, methodSymbol.getReturnType(), false); } } } else if (parent instanceof ExpressionTree exprParent) { - // could be a parameter to another method call, or part of a conditional expression, etc. - // in any case, just return the type of the parent expression if (exprParent instanceof MethodInvocationTree parentInvocation) { - if (isGenericCallNeedingInference(parentInvocation)) { - // this is the case of a nested generic call, e.g., id(id(x)) where id is generic - // we want to find the outermost invocation that requires inference, since that is - // the one whose assignment context is relevant - return getInvocationAndContextForInference( + if (isCallNeedingInference(parentInvocation)) { + return getCallAndContextForInference( parentPath, state.withPath(parentPath), calledFromDataflow); } - // the generic invocation is either a regular parameter to the parent call, or the - // receiver expression Type formalParamType = getFormalParameterTypeForArgument( parentInvocation, castToNonNull(ASTHelpers.getType(parentInvocation.getMethodSelect())) .asMethodType(), - invocation); + call); if (formalParamType == null) { - // this can happen if the invocation is the receiver expression of the call, e.g., - // id(x).foo() (note that foo() need not be generic) ExpressionTree methodSelect = ASTHelpers.stripParentheses(parentInvocation.getMethodSelect()); if (methodSelect instanceof MemberSelectTree mst) { @SuppressWarnings("ReferenceEquality") // deliberate reference equality check - boolean invocationIsReceiver = - ASTHelpers.stripParentheses(mst.getExpression()) == invocation; - if (invocationIsReceiver) { - // the invocation is the receiver expression, so we want the enclosing type of the - // parent invocation + boolean callIsReceiver = ASTHelpers.stripParentheses(mst.getExpression()) == call; + if (callIsReceiver) { formalParamType = getEnclosingTypeForCallExpression( ASTHelpers.getSymbol(parentInvocation), @@ -2778,13 +2749,13 @@ private InvocationAndContext getInvocationAndContextForInference( } else { throw new RuntimeException( "did not find invocation " - + state.getSourceForNode(invocation) + + state.getSourceForNode(call) + " as receiver expression of " + state.getSourceForNode(parentInvocation)); } } } - return new InvocationAndContext(invocation, formalParamType, false); + return new CallAndContext(call, formalParamType, false); } else if (exprParent instanceof ConditionalExpressionTree) { TreePath conditionalPath = getOutermostConditionalExpressionPath(parentPath); TargetTypeAndAssignmentKind targetTypeAndAssignmentKind = @@ -2792,14 +2763,13 @@ private InvocationAndContext getInvocationAndContextForInference( (ConditionalExpressionTree) conditionalPath.getLeaf(), state.withPath(conditionalPath), calledFromDataflow); - return new InvocationAndContext( - invocation, + return new CallAndContext( + call, targetTypeAndAssignmentKind.typeFromAssignmentContext(), targetTypeAndAssignmentKind.assignedToLocal()); } } - // an unhandled case; for now, give up and return no assignment context - return new InvocationAndContext(invocation, null, false); + return new CallAndContext(call, null, false); } /** @@ -2904,11 +2874,11 @@ public Nullness getGenericParameterNullnessAtInvocation( ExpressionTree receiver = ASTHelpers.stripParentheses(memberSelectTree.getExpression()); TreePath curPath = path != null ? path : state.getPath(); TreePath receiverPath = pathWithLeaf(curPath, receiver); - if (isGenericCallNeedingInference(receiver)) { + if (isCallNeedingInference(receiver)) { enclosingType = - inferGenericMethodCallType( - state, - (MethodInvocationTree) receiver, + inferCallType( + state.withPath(receiverPath), + receiver, receiverPath, null, false, From 9fd69a4ceed3b564f74a9eb6b6e1d4bee44d65a9 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 9 Aug 2026 17:50:35 -0700 Subject: [PATCH 2/3] Infer nullability for diamond constructor type arguments --- .../nullaway/generics/GenericsChecks.java | 124 ++++++++++-------- .../jspecify/ConditionalExprTests.java | 4 - .../jspecify/GenericDiamondTests.java | 56 +++++++- .../uber/nullaway/jspecify/GenericsTests.java | 3 +- 4 files changed, 121 insertions(+), 66 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 b959fc317a..747814d729 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -750,19 +750,29 @@ private void reportInvalidOverridingMethodParamTypeError( return typeOrNullIfRaw(result); } if (tree instanceof NewClassTree newClassTree) { - if (TreeInfo.isDiamond((JCTree) newClassTree)) { - if (newClassTree.getClassBody() != null) { - // Keep existing behavior for diamond anonymous classes, which are not yet fully - // supported. Tracked in https://github.com/uber/NullAway/issues/1475 - return null; - } - // For constructor calls using diamond operator, infer from assignment context. - // TODO handle diamond constructor calls passed to generic methods - // https://github.com/uber/NullAway/issues/1470 - Type fromAssignmentContext = - getDiamondTypeFromContext(newClassTree, state, calledFromDataflow); - if (fromAssignmentContext != null) { - return fromAssignmentContext; + if (TreeInfo.isDiamond((JCTree) newClassTree) && newClassTree.getClassBody() != null) { + // Keep existing behavior for diamond anonymous classes, which are not yet fully supported. + // Tracked in https://github.com/uber/NullAway/issues/1475 + return null; + } + if (hasInferredClassTypeArguments(newClassTree)) { + TreePath currentPath = state.getPath(); + @SuppressWarnings("ReferenceEquality") // deliberate reference equality check + boolean currentPathLeafIsTree = + currentPath != null && ASTHelpers.stripParentheses(currentPath.getLeaf()) == tree; + if (currentPathLeafIsTree) { + DirectCallContext directContext = + getDirectCallContextForInference(currentPath, state, calledFromDataflow); + Type constructorAssignmentContext = + sanitizeAssignmentContextForDiamondConstructor( + directContext.typeFromAssignmentContext); + return inferCallType( + state, + newClassTree, + currentPath, + constructorAssignmentContext, + directContext.assignedToLocal, + calledFromDataflow); } } if (newClassTree.getIdentifier() instanceof ParameterizedTypeTree paramTypedTree @@ -871,27 +881,6 @@ private void reportInvalidOverridingMethodParamTypeError( return type; } - /** - * Gets the type of a constructor call using a diamond operator from its assignment context, if - * available. - */ - private @Nullable Type getDiamondTypeFromContext( - NewClassTree tree, VisitorState state, boolean calledFromDataflow) { - return getDiamondTypeFromParentContext( - tree, state, castToNonNull(state.getPath().getParentPath()), calledFromDataflow); - } - - /** - * Computes the assignment-context type for an inferred constructor call, given a path to its - * parent context. - */ - private @Nullable Type getDiamondTypeFromParentContext( - NewClassTree tree, VisitorState state, TreePath parentPath, boolean calledFromDataflow) { - return getTargetTypeFromParentContext( - tree, new TreePath(parentPath, tree), state, calledFromDataflow) - .typeFromAssignmentContext(); - } - /** * Returns the inferred/declared formal parameter type corresponding to actual parameter {@code * argumentTree}. @@ -934,7 +923,8 @@ private static boolean isCallNeedingInference(ExpressionTree expressionTree) { && methodSymbol.type instanceof Type.ForAll && methodInvocation.getTypeArguments().isEmpty(); } - return false; + return expressionTree instanceof NewClassTree newClassTree + && hasInferredClassTypeArguments(newClassTree); } /** @@ -1355,6 +1345,16 @@ private Symbol.MethodSymbol getMethodSymbolForCall(ExpressionTree callTree) { return (Symbol.MethodSymbol) castToNonNull(ASTHelpers.getSymbol(callTree)); } + /** + * A bare method type variable does not provide useful structure for inferring nullability of a + * diamond constructor's class type arguments. In such cases we let constructor inference rely on + * constructor arguments and any more structured surrounding context instead. + */ + private @Nullable Type sanitizeAssignmentContextForDiamondConstructor( + @Nullable Type contextType) { + return contextType instanceof Type.TypeVar ? null : contextType; + } + /** * Generates inference constraints for a generic call, including nested generic method calls and * diamond constructor calls. @@ -2227,17 +2227,6 @@ public void compareGenericTypeParameterNullabilityForCall( } Type invokedMethodType = methodSymbol.type; Type enclosingType = null; - if (tree instanceof NewClassTree newClassTree) { - if (hasInferredClassTypeArguments(newClassTree)) { - TreePath currentPath = state.getPath(); - if (currentPath != null && ASTHelpers.stripParentheses(currentPath.getLeaf()) == tree) { - TreePath parentPath = currentPath.getParentPath(); - if (parentPath != null) { - enclosingType = getDiamondTypeFromParentContext(newClassTree, state, parentPath, false); - } - } - } - } if (enclosingType == null) { enclosingType = getEnclosingTypeForCallExpression(methodSymbol, tree, null, state, false); } @@ -2676,6 +2665,9 @@ private Type.MethodType restoreNestedNullabilityForTypeVarArguments( private record CallAndContext( ExpressionTree call, @Nullable Type typeFromAssignmentContext, boolean assignedToLocal) {} + private record DirectCallContext( + @Nullable Type typeFromAssignmentContext, boolean assignedToLocal) {} + /** * Given a {@link TreePath} to an invocation of a generic method, collect information about the * appropriate invocation on which to perform type inference, and the relevant information from @@ -2703,11 +2695,34 @@ private CallAndContext getCallAndContextForInference( parentPath = parentPath.getParentPath(); parent = parentPath.getLeaf(); } + if (call instanceof MethodInvocationTree + && parent instanceof MethodInvocationTree parentInvocation + && isCallNeedingInference(parentInvocation)) { + return getCallAndContextForInference( + parentPath, state.withPath(parentPath), calledFromDataflow); + } + DirectCallContext directContext = + getDirectCallContextForInference(path, state, calledFromDataflow); + return new CallAndContext( + call, directContext.typeFromAssignmentContext, directContext.assignedToLocal); + } + + /** + * Returns the context immediately surrounding a call, without traversing nested generic calls. + */ + private DirectCallContext getDirectCallContextForInference( + TreePath path, VisitorState state, boolean calledFromDataflow) { + ExpressionTree call = (ExpressionTree) path.getLeaf(); + TreePath parentPath = path.getParentPath(); + Tree parent = parentPath.getLeaf(); + while (parent instanceof ParenthesizedTree) { + parentPath = parentPath.getParentPath(); + parent = parentPath.getLeaf(); + } if (parent instanceof AssignmentTree || parent instanceof VariableTree) { TargetTypeAndAssignmentKind targetTypeAndAssignmentKind = getTargetTypeForAssignmentContext(parent, state.withPath(parentPath), calledFromDataflow); - return new CallAndContext( - call, + return new DirectCallContext( targetTypeAndAssignmentKind.typeFromAssignmentContext(), targetTypeAndAssignmentKind.assignedToLocal()); } else if (parent instanceof ReturnTree) { @@ -2717,15 +2732,11 @@ private CallAndContext getCallAndContextForInference( && enclosingMethodOrLambda.getLeaf() instanceof MethodTree enclosingMethod) { Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(enclosingMethod); if (methodSymbol != null) { - return new CallAndContext(call, methodSymbol.getReturnType(), false); + return new DirectCallContext(methodSymbol.getReturnType(), false); } } } else if (parent instanceof ExpressionTree exprParent) { if (exprParent instanceof MethodInvocationTree parentInvocation) { - if (isCallNeedingInference(parentInvocation)) { - return getCallAndContextForInference( - parentPath, state.withPath(parentPath), calledFromDataflow); - } Type formalParamType = getFormalParameterTypeForArgument( parentInvocation, @@ -2755,7 +2766,7 @@ private CallAndContext getCallAndContextForInference( } } } - return new CallAndContext(call, formalParamType, false); + return new DirectCallContext(formalParamType, false); } else if (exprParent instanceof ConditionalExpressionTree) { TreePath conditionalPath = getOutermostConditionalExpressionPath(parentPath); TargetTypeAndAssignmentKind targetTypeAndAssignmentKind = @@ -2763,13 +2774,12 @@ private CallAndContext getCallAndContextForInference( (ConditionalExpressionTree) conditionalPath.getLeaf(), state.withPath(conditionalPath), calledFromDataflow); - return new CallAndContext( - call, + return new DirectCallContext( targetTypeAndAssignmentKind.typeFromAssignmentContext(), targetTypeAndAssignmentKind.assignedToLocal()); } } - return new CallAndContext(call, null, false); + return new DirectCallContext(null, false); } /** diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/ConditionalExprTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/ConditionalExprTests.java index 6f17869ff7..2e3b6b45b4 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/ConditionalExprTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/ConditionalExprTests.java @@ -273,10 +273,6 @@ T get() { } } void test(boolean flag) { - // TODO: we should infer Box<@Nullable Object> (or something like that) as the type - // of inferredFromInitializer, but currently we do not. When we do, the warning on - // the next line should go away. https://github.com/uber/NullAway/issues/1633 - // BUG: Diagnostic contains: passing @Nullable parameter var inferredFromInitializer = flag ? new Box<>(null) : new Box<>("fallback"); Box<@Nullable String> explicitNullableTarget = flag ? new Box<>(null) : new Box<>("fallback"); diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java index af0c14bc7b..b8f185ee52 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java @@ -63,9 +63,6 @@ T get() { } } void test() { - // NOTE: reporting a warning on the next line is a current limitation - // of NullAway; there should be no warning. See https://github.com/uber/NullAway/issues/1633. - // BUG: Diagnostic contains: passing @Nullable parameter var inferredFromInitializer = new Box<>(null); // BUG: Diagnostic contains: passing @Nullable parameter Box explicitNonNullTarget = new Box<>(null); @@ -243,6 +240,59 @@ static class FooImpl implements Foo<@Nullable T> { .doTest(); } + @Test + public void inferFromReturn() { + makeHelper() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.*; + @NullMarked + public class Test { + interface Supplier { public T get(); } + public interface LazyValue extends Supplier {} + record NullableLazyValue(Supplier supplier) implements LazyValue { + public T get() { + return supplier.get(); + } + } + static LazyValue<@Nullable K> nullable(Supplier<@Nullable K> supplier) { + return new NullableLazyValue<>(supplier); + } + } + """) + .doTest(); + } + + @Test + public void inferFromParams() { + makeHelper() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.*; + @NullMarked + public class Test { + static class Foo {} + static class Bar { + Bar(Foo foo1, Foo foo2) { + } + } + static void testNegative1(Foo f1, Foo f2) { + new Bar<>(f1, f2); + } + static void testNegative2(Foo<@Nullable String> f1, Foo<@Nullable String> f2) { + new Bar<>(f1, f2); + } + static void testPositive(Foo f1, Foo<@Nullable String> f2) { + // BUG: Diagnostic contains: incompatible types + new Bar<>(f1, f2); + } + } + """) + .doTest(); + } + private CompilationTestHelper makeHelper() { return makeTestHelperWithArgs( JSpecifyJavacConfig.withJSpecifyModeArgs( 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 fb75f932e7..37aa7dd5cd 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java @@ -896,8 +896,7 @@ public String function(T1 o) { } } static void testPositive() { - // TODO: we should report an error here, since B's type parameter - // cannot be @Nullable; we do not catch this yet + // BUG: Diagnostic contains: incompatible types: B cannot be converted to A<@Nullable Object> A<@Nullable Object> p = new B<>(); } static void testNegative() { From d7c7536a24e3d2d4f20528df42eb5b81ae65220b Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sun, 9 Aug 2026 17:52:51 -0700 Subject: [PATCH 3/3] Support nested call inference and prevent reentrant repair --- .../nullaway/generics/GenericsChecks.java | 189 +++++++++++++----- .../jspecify/GenericDiamondTests.java | 105 ++++++++++ 2 files changed, 240 insertions(+), 54 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 747814d729..1c0bc81954 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -116,6 +116,13 @@ private InferenceFailure(@Nullable String errorMessage) { /** Maps each {@code var}-declared local to its declaration tree */ private final Map varLocalDeclarations = new LinkedHashMap<>(); + /** + * Tracks generic method invocations currently undergoing nested-nullability repair so re-entrant + * requests for the same invocation can use the already inferred call-site method type rather than + * recursing back through the same repair logic. + */ + private final Set nestedNullabilityRepairInProgress = new LinkedHashSet<>(); + public @Nullable Type getInferredPolyExpressionType(Tree tree) { Preconditions.checkArgument( tree instanceof LambdaExpressionTree || tree instanceof MemberReferenceTree, @@ -846,9 +853,10 @@ private void reportInvalidOverridingMethodParamTypeError( // call. We invoke getEnclosingTypeForCallExpression, which will run // inference if needed, and then recompute the type as a member of the returned // enclosing type + Symbol.MethodSymbol symbol = castToNonNull(ASTHelpers.getSymbol(invocationTree)); Type.MethodType methodType = - getInferenceExecutableType( - invocationTree, state.getPath(), state, calledFromDataflow); + getInvokedMethodTypeAtCall( + symbol, invocationTree, state.getPath(), state, calledFromDataflow); // restore explicit annotations from the return type Type returnType = methodType.getReturnType(); result = @@ -2608,9 +2616,25 @@ private Type substituteTypeArgsInGenericMethodType( Type.MethodType methodTypeAtCallSite = castToNonNull(ASTHelpers.getType(invocationTree.getMethodSelect())).asMethodType(); if (result instanceof InferenceSuccess successResult) { - methodTypeAtCallSite = - restoreNestedNullabilityForTypeVarArguments( - invocationTree, methodType, methodTypeAtCallSite, path, state, calledFromDataflow); + // Repairing dropped nested nullability annotations can itself inspect actual argument + // types. For diamond constructor arguments, that can re-enter method-type computation for + // this same invocation while we are still repairing it. In that case, use the already + // inferred method type and skip the repair on the recursive call. + if (!nestedNullabilityRepairInProgress.contains(invocationTree)) { + nestedNullabilityRepairInProgress.add(invocationTree); + try { + methodTypeAtCallSite = + restoreNestedNullabilityForTypeVarArguments( + invocationTree, + methodType, + methodTypeAtCallSite, + path, + state, + calledFromDataflow); + } finally { + nestedNullabilityRepairInProgress.remove(invocationTree); + } + } return TypeSubstitutionUtils.updateMethodTypeWithInferredNullability( methodTypeAtCallSite, methodType, successResult.typeVarNullability, state, config); } else { @@ -2689,27 +2713,25 @@ private record DirectCallContext( private CallAndContext getCallAndContextForInference( TreePath path, VisitorState state, boolean calledFromDataflow) { ExpressionTree call = (ExpressionTree) path.getLeaf(); + DirectCallContext directContext = + getDirectCallContextForInference(path, state, calledFromDataflow); TreePath parentPath = path.getParentPath(); Tree parent = parentPath.getLeaf(); while (parent instanceof ParenthesizedTree) { parentPath = parentPath.getParentPath(); parent = parentPath.getLeaf(); } - if (call instanceof MethodInvocationTree - && parent instanceof MethodInvocationTree parentInvocation - && isCallNeedingInference(parentInvocation)) { - return getCallAndContextForInference( - parentPath, state.withPath(parentPath), calledFromDataflow); + if (parent instanceof ExpressionTree exprParent) { + if ((exprParent instanceof MethodInvocationTree || exprParent instanceof NewClassTree) + && isCallNeedingInference(exprParent)) { + return getCallAndContextForInference( + parentPath, state.withPath(parentPath), calledFromDataflow); + } } - DirectCallContext directContext = - getDirectCallContextForInference(path, state, calledFromDataflow); return new CallAndContext( call, directContext.typeFromAssignmentContext, directContext.assignedToLocal); } - /** - * Returns the context immediately surrounding a call, without traversing nested generic calls. - */ private DirectCallContext getDirectCallContextForInference( TreePath path, VisitorState state, boolean calledFromDataflow) { ExpressionTree call = (ExpressionTree) path.getLeaf(); @@ -2725,7 +2747,8 @@ private DirectCallContext getDirectCallContextForInference( return new DirectCallContext( targetTypeAndAssignmentKind.typeFromAssignmentContext(), targetTypeAndAssignmentKind.assignedToLocal()); - } else if (parent instanceof ReturnTree) { + } + if (parent instanceof ReturnTree) { TreePath enclosingMethodOrLambda = NullabilityUtil.findEnclosingMethodOrLambdaOrInitializer(parentPath); if (enclosingMethodOrLambda != null @@ -2735,53 +2758,110 @@ private DirectCallContext getDirectCallContextForInference( return new DirectCallContext(methodSymbol.getReturnType(), false); } } - } else if (parent instanceof ExpressionTree exprParent) { - if (exprParent instanceof MethodInvocationTree parentInvocation) { - Type formalParamType = - getFormalParameterTypeForArgument( - parentInvocation, - castToNonNull(ASTHelpers.getType(parentInvocation.getMethodSelect())) - .asMethodType(), - call); - if (formalParamType == null) { - ExpressionTree methodSelect = - ASTHelpers.stripParentheses(parentInvocation.getMethodSelect()); - if (methodSelect instanceof MemberSelectTree mst) { - @SuppressWarnings("ReferenceEquality") // deliberate reference equality check - boolean callIsReceiver = ASTHelpers.stripParentheses(mst.getExpression()) == call; - if (callIsReceiver) { - formalParamType = - getEnclosingTypeForCallExpression( - ASTHelpers.getSymbol(parentInvocation), - parentInvocation, - parentPath, - state.withPath(parentPath), - calledFromDataflow); - } else { - throw new RuntimeException( - "did not find invocation " - + state.getSourceForNode(call) - + " as receiver expression of " - + state.getSourceForNode(parentInvocation)); - } + return new DirectCallContext(null, false); + } + if (parent instanceof MethodInvocationTree parentInvocation) { + Type.MethodType parentMethodType = + getInvokedMethodTypeAtCall( + ASTHelpers.getSymbol(parentInvocation), + parentInvocation, + parentPath, + state.withPath(parentPath), + calledFromDataflow); + Type formalParamType = + getFormalParameterTypeForArgument(parentInvocation, parentMethodType, call); + if (formalParamType == null) { + ExpressionTree methodSelect = + ASTHelpers.stripParentheses(parentInvocation.getMethodSelect()); + if (methodSelect instanceof MemberSelectTree mst) { + @SuppressWarnings("ReferenceEquality") // deliberate reference equality check + boolean callIsReceiver = ASTHelpers.stripParentheses(mst.getExpression()) == call; + if (callIsReceiver) { + formalParamType = + getEnclosingTypeForCallExpression( + ASTHelpers.getSymbol(parentInvocation), + parentInvocation, + parentPath, + state.withPath(parentPath), + calledFromDataflow); + } else { + throw new RuntimeException( + "did not find invocation " + + state.getSourceForNode(call) + + " as receiver expression of " + + state.getSourceForNode(parentInvocation)); } } - return new DirectCallContext(formalParamType, false); - } else if (exprParent instanceof ConditionalExpressionTree) { - TreePath conditionalPath = getOutermostConditionalExpressionPath(parentPath); - TargetTypeAndAssignmentKind targetTypeAndAssignmentKind = - getTargetTypeForConditionalExpression( - (ConditionalExpressionTree) conditionalPath.getLeaf(), - state.withPath(conditionalPath), + } + return new DirectCallContext(formalParamType, false); + } + if (parent instanceof ConditionalExpressionTree) { + TreePath conditionalPath = getOutermostConditionalExpressionPath(parentPath); + TargetTypeAndAssignmentKind targetTypeAndAssignmentKind = + getTargetTypeForConditionalExpression( + (ConditionalExpressionTree) conditionalPath.getLeaf(), + state.withPath(conditionalPath), + calledFromDataflow); + return new DirectCallContext( + targetTypeAndAssignmentKind.typeFromAssignmentContext(), + targetTypeAndAssignmentKind.assignedToLocal()); + } + if (parent instanceof NewClassTree parentConstructorCall) { + Type parentClassType; + if (isCallNeedingInference(parentConstructorCall)) { + CallAndContext parentContext = + getCallAndContextForInference(parentPath, state, calledFromDataflow); + parentClassType = + inferCallType( + state, + parentConstructorCall, + parentPath, + parentContext.typeFromAssignmentContext, + parentContext.assignedToLocal, calledFromDataflow); + } else { + parentClassType = getTreeType(parentConstructorCall, state.withPath(parentPath)); + } + if (parentClassType != null) { + Symbol.MethodSymbol parentCtorSymbol = ASTHelpers.getSymbol(parentConstructorCall); + Type parentCtorType = + TypeSubstitutionUtils.memberType( + state.getTypes(), parentClassType, parentCtorSymbol, config); return new DirectCallContext( - targetTypeAndAssignmentKind.typeFromAssignmentContext(), - targetTypeAndAssignmentKind.assignedToLocal()); + getFormalParameterTypeForArgument( + parentConstructorCall, parentCtorType.asMethodType(), call), + false); } + return new DirectCallContext(null, false); } return new DirectCallContext(null, false); } + private Type.MethodType getInvokedMethodTypeAtCall( + Symbol.MethodSymbol methodSymbol, + Tree tree, + @Nullable TreePath path, + VisitorState state, + boolean calledFromDataflow) { + Type invokedMethodType = methodSymbol.type; + Type enclosingType = + getEnclosingTypeForCallExpression(methodSymbol, tree, path, state, calledFromDataflow); + if (enclosingType != null) { + invokedMethodType = + TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, methodSymbol, config); + } + if (tree instanceof MethodInvocationTree + && invokedMethodType instanceof Type.ForAll forAllType) { + invokedMethodType = + substituteTypeArgsInGenericMethodType(tree, forAllType, path, state, calledFromDataflow); + } + return handler.onOverrideMethodType( + methodSymbol, + invokedMethodType.asMethodType(), + state, + tree instanceof MethodInvocationTree invocationTree ? invocationTree : null); + } + /** * Computes the nullness of a formal parameter of a generic method at an invocation, in the * context of the declared type of its receiver argument. If the formal parameter's type is a type @@ -3155,6 +3235,7 @@ public void clearCache() { inferredPolyExpressionTypes.clear(); inferredVarLocalTypes.clear(); varLocalDeclarations.clear(); + nestedNullabilityRepairInProgress.clear(); } public boolean isNullableAnnotated(Type type) { diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java index b8f185ee52..822678ea12 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java @@ -293,6 +293,111 @@ static void testPositive(Foo f1, Foo<@Nullable String> f2) { .doTest(); } + @Test + public void genericMethodCallWithDiamondConstructorParameter() { + makeHelper() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.*; + @NullMarked + public class Test { + interface Foo {} + static class FooImpl implements Foo { + FooImpl(Foo value) {} + } + static Foo<@Nullable String> makeNullableFoo() { + throw new RuntimeException(); + } + static Foo id(Foo foo) { + throw new RuntimeException(); + } + static void takeFooString(Foo foo) {} + static void takeFooNullableString(Foo<@Nullable String> foo) {} + static void testNegative() { + takeFooNullableString(id(new FooImpl<>(makeNullableFoo()))); + } + static void testPositive() { + // BUG: Diagnostic contains: incompatible types + takeFooString(id(new FooImpl<>(makeNullableFoo()))); + } + } + """) + .doTest(); + } + + @Test + public void diamondConstructorWithGenericMethodCallParameter() { + makeHelper() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.*; + @NullMarked + public class Test { + interface Foo {} + static class Box { + Box(Foo foo) {} + } + static Foo id(Foo foo) { + throw new RuntimeException(); + } + static Foo<@Nullable String> makeNullableFoo() { + throw new RuntimeException(); + } + static Box<@Nullable String> testNegative() { + return new Box<>(id(makeNullableFoo())); + } + static Box testPositive() { + // BUG: Diagnostic contains: incompatible types + return new Box<>(id(makeNullableFoo())); + } + } + """) + .doTest(); + } + + @Test + public void genericMethodTypeVarParamWithDiamondArg() { + makeHelper() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.*; + @NullMarked + public class Test { + static class Box { + Box(T value) {} + } + static @Nullable String nullableString() { + throw new RuntimeException(); + } + static void consume(U u) {} + static void testNoStackOverflow() { + consume(new Box<>(nullableString())); + } + } + """) + .doTest(); + } + + @Test + public void arraysAsList() { + makeHelper() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.*; + import java.util.*; + @NullMarked + public class Test { + @Nullable Object[] makeArr() { return new Object[0]; } + List<@Nullable Object> make() { return new ArrayList<>(Arrays.asList(makeArr())); } + } + """) + .doTest(); + } + private CompilationTestHelper makeHelper() { return makeTestHelperWithArgs( JSpecifyJavacConfig.withJSpecifyModeArgs(