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 b8b716c765..e4eb77c433 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -111,8 +111,8 @@ private static final class InferenceFailure implements MethodInferenceResult { inferredTypeVarNullabilityForGenericCalls = new LinkedHashMap<>(); /** - * Maps each poly expression ({@code LambdaExpressionTree} or {@code MemberReferenceTree}) passed - * as a parameter to a generic method to its inferred type, if inference succeeded. + * Maps poly expressions for which we have computed a context-derived type to that type, if + * inference succeeded. */ private final Map inferredPolyExpressionTypes = new LinkedHashMap<>(); @@ -597,10 +597,19 @@ private void reportInvalidOverridingMethodParamTypeError( tree = exprTreeAndState.expr(); state = exprTreeAndState.state(); } - if (tree instanceof LambdaExpressionTree || tree instanceof MemberReferenceTree) { + if (tree instanceof LambdaExpressionTree + || tree instanceof MemberReferenceTree + || tree instanceof ConditionalExpressionTree) { Type result = inferredPolyExpressionTypes.get(tree); if (result == null) { - result = ASTHelpers.getType(tree); + // For lambda and method ref poly expressions, their nullness-enhanced type should have + // already been cached via an inference call from a parent tree. In contrast, the + // nullness-enhanced type of a conditional expression tree may not have been computed, so we + // invoke getConditionalExpressionType here to do so + result = + tree instanceof ConditionalExpressionTree conditionalExpressionTree + ? getConditionalExpressionType(conditionalExpressionTree, state, calledFromDataflow) + : ASTHelpers.getType(tree); } return typeOrNullIfRaw(result); } @@ -751,63 +760,9 @@ private void reportInvalidOverridingMethodParamTypeError( */ private @Nullable Type getDiamondTypeFromParentContext( NewClassTree tree, VisitorState state, TreePath parentPath, boolean calledFromDataflow) { - Tree parent = parentPath.getLeaf(); - while (parent instanceof ParenthesizedTree) { - parentPath = parentPath.getParentPath(); - if (parentPath == null) { - return null; - } - parent = parentPath.getLeaf(); - } - if (parent instanceof VariableTree || parent instanceof AssignmentTree) { - return getTreeType(parent, state.withPath(parentPath), calledFromDataflow); - } - if (parent instanceof ReturnTree) { - TreePath enclosingMethodOrLambda = - NullabilityUtil.findEnclosingMethodOrLambdaOrInitializer(parentPath); - if (enclosingMethodOrLambda != null - && enclosingMethodOrLambda.getLeaf() instanceof MethodTree enclosingMethod) { - Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(enclosingMethod); - if (methodSymbol != null) { - return methodSymbol.getReturnType(); - } - } - return null; - } - if (parent instanceof MethodInvocationTree parentInvocation) { - if (isGenericCallNeedingInference(parentInvocation)) { - // TODO support full integration of diamond constructor calls with generic method inference - // https://github.com/uber/NullAway/issues/1470 - // for now, just give up and return null - return null; - } - Type methodType = ASTHelpers.getType(parentInvocation.getMethodSelect()); - if (methodType == null) { - return null; - } - return getFormalParameterTypeForArgument(parentInvocation, methodType.asMethodType(), tree); - } - if (parent instanceof NewClassTree parentConstructorCall) { - // get the type returned by the parent constructor call - Type parentClassType = - getTreeType(parentConstructorCall, state.withPath(parentPath), calledFromDataflow); - if (parentClassType != null) { - Symbol parentCtorSymbol = ASTHelpers.getSymbol(parentConstructorCall); - // get the proper type for the constructor, as a member of the type returned by the - // constructor - Type parentCtorType = - TypeSubstitutionUtils.memberType( - state.getTypes(), parentClassType, parentCtorSymbol, config); - return getFormalParameterTypeForArgument( - parentConstructorCall, parentCtorType.asMethodType(), tree); - } - } - if (parent instanceof ConditionalExpressionTree) { - // TODO infer diamond type from the overall conditional expression type - // tracked in https://github.com/uber/NullAway/issues/1477 - return null; - } - return null; + return getTargetTypeFromParentContext( + tree, new TreePath(parentPath, tree), state, calledFromDataflow) + .typeFromAssignmentContext(); } /** @@ -1044,6 +999,13 @@ public void registerVarLocalDeclaration(VariableTree tree) { typeFromAssignmentContext, assignedToLocal, calledFromDataflow); + } else if (rhsTree instanceof ConditionalExpressionTree conditionalExpressionTree) { + return inferConditionalExpressionType( + state.withPath(pathToRhs), + conditionalExpressionTree, + pathToRhs, + typeFromAssignmentContext, + calledFromDataflow); } else { return getTreeType(rhsTree, state.withPath(pathToRhs), calledFromDataflow); } @@ -1318,6 +1280,27 @@ private void generateConstraintsForPseudoAssignment( invTree, allInvocations, calledFromDataflow); + } else if (rhsExpr instanceof ConditionalExpressionTree conditionalExpressionTree) { + // generate constraints for both the true and false sub-expressions of the conditional + // expression + ExpressionTree trueExpression = conditionalExpressionTree.getTrueExpression(); + TreePath pathToTrueExpression = new TreePath(state.getPath(), trueExpression); + generateConstraintsForPseudoAssignment( + state.withPath(pathToTrueExpression), + solver, + allInvocations, + trueExpression, + lhsType, + calledFromDataflow); + ExpressionTree falseExpression = conditionalExpressionTree.getFalseExpression(); + TreePath pathToFalseExpression = new TreePath(state.getPath(), falseExpression); + generateConstraintsForPseudoAssignment( + state.withPath(pathToFalseExpression), + solver, + allInvocations, + falseExpression, + lhsType, + calledFromDataflow); } else if (rhsExpr instanceof LambdaExpressionTree lambda) { handleLambdaInGenericMethodInference( state, state.getPath(), solver, allInvocations, lhsType, lambda, calledFromDataflow); @@ -1722,6 +1705,17 @@ public void checkTypeParameterNullnessForFunctionReturnType( formalReturnType, false, false); + } else if (retExpr instanceof ConditionalExpressionTree conditionalExpressionTree) { + returnExpressionType = + inferConditionalExpressionType( + state.withPath(pathToRetExpr), + conditionalExpressionTree, + pathToRetExpr, + formalReturnType, + false); + } + if (returnExpressionType == null) { + return; } boolean isReturnTypeValid = subtypeParameterNullability(formalReturnType, returnExpressionType, state); @@ -1816,42 +1810,261 @@ public void checkTypeParameterNullnessForConditionalExpression( Tree truePartTree = tree.getTrueExpression(); Tree falsePartTree = tree.getFalseExpression(); - Type condExprType = getConditionalExpressionType(tree, state); + TargetTypeAndAssignmentKind targetTypeAndAssignmentKind = + getTargetTypeForConditionalExpression(tree, state, false); + boolean hasTargetType = + targetTypeAndAssignmentKind.typeFromAssignmentContext() != null + || inferredPolyExpressionTypes.containsKey(tree); + Type condExprType = + inferConditionalExpressionType( + state, + tree, + state.getPath(), + targetTypeAndAssignmentKind.typeFromAssignmentContext(), + false); + if (!hasTargetType) { + return; + } + if (condExprType == null) { + return; + } + TreePath pathToTruePart = pathWithLeaf(state.getPath(), truePartTree); + TreePath pathToFalsePart = pathWithLeaf(state.getPath(), falsePartTree); Type truePartType = - getTreeType(truePartTree, state.withPath(pathWithLeaf(state.getPath(), truePartTree))); + getTypeForRhsOfAssignment( + tree.getTrueExpression(), + pathToTruePart, + condExprType, + targetTypeAndAssignmentKind.assignedToLocal(), + state, + false); Type falsePartType = - getTreeType(falsePartTree, state.withPath(pathWithLeaf(state.getPath(), falsePartTree))); + getTypeForRhsOfAssignment( + tree.getFalseExpression(), + pathToFalsePart, + condExprType, + targetTypeAndAssignmentKind.assignedToLocal(), + state, + false); // The condExpr type should be the least-upper bound of the true and false part types. To check // the nullability annotations, we check that the true and false parts are assignable to the // type of the whole expression - if (condExprType != null) { - if (truePartType != null) { - if (!subtypeParameterNullability(condExprType, truePartType, state)) { - reportMismatchedTypeForTernaryOperator(truePartTree, condExprType, truePartType, state); - } + if (truePartType != null) { + if (!subtypeParameterNullability(condExprType, truePartType, state)) { + reportMismatchedTypeForTernaryOperator(truePartTree, condExprType, truePartType, state); } - if (falsePartType != null) { - if (!subtypeParameterNullability(condExprType, falsePartType, state)) { - reportMismatchedTypeForTernaryOperator(falsePartTree, condExprType, falsePartType, state); - } + } + if (falsePartType != null) { + if (!subtypeParameterNullability(condExprType, falsePartType, state)) { + reportMismatchedTypeForTernaryOperator(falsePartTree, condExprType, falsePartType, state); } } } + /** + * Returns the type to use for a conditional expression. + * + *

If a target/contextual type for the conditional expression has already been cached, returns + * it. Otherwise, this method tries to recover a target type from the conditional expression's + * parent context and caches it, unless called from dataflow. If no target type is available, + * falls back to javac's type for the conditional expression. + */ private @Nullable Type getConditionalExpressionType( - ConditionalExpressionTree tree, VisitorState state) { - // hack: sometimes array nullability doesn't get computed correctly for a conditional expression - // on the RHS of an assignment. So, look at the type of the assignment tree. - TreePath parentPath = state.getPath().getParentPath(); + ConditionalExpressionTree tree, VisitorState state, boolean calledFromDataflow) { + Type cachedType = inferredPolyExpressionTypes.get(tree); + if (cachedType != null) { + return cachedType; + } + TargetTypeAndAssignmentKind targetTypeAndAssignmentKind = + getTargetTypeForConditionalExpression(tree, state, calledFromDataflow); + Type typeFromAssignmentContext = targetTypeAndAssignmentKind.typeFromAssignmentContext(); + if (typeFromAssignmentContext != null) { + if (!calledFromDataflow) { + inferredPolyExpressionTypes.put(tree, typeFromAssignmentContext); + } + return typeFromAssignmentContext; + } + return typeOrNullIfRaw(ASTHelpers.getType(tree)); + } + + /** + * Infers the type to use for a conditional expression in a pseudo-assignment context. + * + *

If {@code typeFromAssignmentContext} is non-null, it is used as the conditional expression's + * target type. Otherwise, this method tries to recover a target type from the parent context. + * When a target type is found, it is cached unless called from dataflow. If no target type is + * available, this method falls back to javac's type for the conditional expression. Returns + * {@code null} for raw or otherwise unavailable types. + */ + private @Nullable Type inferConditionalExpressionType( + VisitorState state, + ConditionalExpressionTree tree, + TreePath path, + @Nullable Type typeFromAssignmentContext, + boolean calledFromDataflow) { + Type cachedType = inferredPolyExpressionTypes.get(tree); + if (cachedType != null) { + return cachedType; + } + boolean hasTargetType = typeFromAssignmentContext != null; + Type condExprType = typeFromAssignmentContext; + if (condExprType == null) { + TargetTypeAndAssignmentKind targetTypeAndAssignmentKind = + getTargetTypeForConditionalExpression(tree, state.withPath(path), calledFromDataflow); + condExprType = targetTypeAndAssignmentKind.typeFromAssignmentContext(); + hasTargetType = condExprType != null; + } + if (condExprType == null) { + condExprType = typeOrNullIfRaw(ASTHelpers.getType(tree)); + } + if (condExprType == null || condExprType.isRaw()) { + return null; + } + if (hasTargetType && !calledFromDataflow) { + inferredPolyExpressionTypes.put(tree, condExprType); + } + return condExprType; + } + + private record TargetTypeAndAssignmentKind( + @Nullable Type typeFromAssignmentContext, boolean assignedToLocal) {} + + private TargetTypeAndAssignmentKind getTargetTypeForConditionalExpression( + ConditionalExpressionTree tree, VisitorState state, boolean calledFromDataflow) { + TreePath conditionalPath = + Objects.equals(state.getPath().getLeaf(), tree) + ? state.getPath() + : pathWithLeaf(state.getPath(), tree); + return getTargetTypeFromParentContext(tree, conditionalPath, state, calledFromDataflow); + } + + /** + * Returns the target type supplied by the parent context for an expression, if one is available. + * + *

Handles assignment and variable declaration contexts, method and constructor arguments, + * return expressions, and expressions nested inside conditional expressions. If {@code path} is + * inside a nested conditional expression, this method first climbs to the outermost conditional + * expression, since the surrounding target type applies to that expression as a whole. + * + *

A {@code var}-declared local variable does not provide a target type; its type is inferred + * from the initializer. + */ + private TargetTypeAndAssignmentKind getTargetTypeFromParentContext( + Tree tree, TreePath path, VisitorState state, boolean calledFromDataflow) { + Tree expressionTree = tree; + TreePath expressionPath = path; + TreePath parentPath = expressionPath.getParentPath(); + if (parentPath == null) { + return new TargetTypeAndAssignmentKind(null, false); + } Tree parent = parentPath.getLeaf(); while (parent instanceof ParenthesizedTree) { parentPath = parentPath.getParentPath(); parent = parentPath.getLeaf(); } + if (parent instanceof ConditionalExpressionTree) { + expressionPath = getOutermostConditionalExpressionPath(parentPath); + expressionTree = expressionPath.getLeaf(); + parentPath = expressionPath.getParentPath(); + parent = parentPath.getLeaf(); + while (parent instanceof ParenthesizedTree) { + parentPath = parentPath.getParentPath(); + parent = parentPath.getLeaf(); + } + } + VisitorState parentState = state.withPath(parentPath); if (parent instanceof AssignmentTree || parent instanceof VariableTree) { - return getTreeType(parent, state.withPath(parentPath)); + return getTargetTypeForAssignmentContext(parent, parentState, calledFromDataflow); + } + if (parent instanceof ReturnTree) { + TreePath enclosingMethodOrLambda = + NullabilityUtil.findEnclosingMethodOrLambdaOrInitializer(parentPath); + if (enclosingMethodOrLambda != null + && enclosingMethodOrLambda.getLeaf() instanceof MethodTree enclosingMethod) { + Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(enclosingMethod); + if (methodSymbol != null) { + return new TargetTypeAndAssignmentKind(methodSymbol.getReturnType(), false); + } + } + return new TargetTypeAndAssignmentKind(null, false); + } + if (parent instanceof MethodInvocationTree parentInvocation) { + if (isGenericCallNeedingInference(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. + return new TargetTypeAndAssignmentKind(null, false); + } + Type methodType = ASTHelpers.getType(parentInvocation.getMethodSelect()); + if (methodType == null) { + return new TargetTypeAndAssignmentKind(null, false); + } + return new TargetTypeAndAssignmentKind( + getFormalParameterTypeForArgument( + parentInvocation, methodType.asMethodType(), expressionTree), + false); + } + if (parent instanceof NewClassTree parentConstructorCall) { + Type parentClassType = getTreeType(parentConstructorCall, parentState, calledFromDataflow); + if (parentClassType != null) { + Symbol parentCtorSymbol = ASTHelpers.getSymbol(parentConstructorCall); + Type parentCtorType = + TypeSubstitutionUtils.memberType( + state.getTypes(), parentClassType, parentCtorSymbol, config); + return new TargetTypeAndAssignmentKind( + getFormalParameterTypeForArgument( + parentConstructorCall, parentCtorType.asMethodType(), expressionTree), + false); + } + } + return new TargetTypeAndAssignmentKind(null, false); + } + + /** + * Returns target-type information for an assignment or variable declaration context. + * + *

For explicit variable declarations and assignments, the assigned location's type is the + * target type. For {@code var}-declared locals, no target type is returned because the local's + * type is inferred from the initializer. + */ + private TargetTypeAndAssignmentKind getTargetTypeForAssignmentContext( + Tree assignmentOrVariable, VisitorState state, boolean calledFromDataflow) { + Preconditions.checkArgument( + assignmentOrVariable instanceof AssignmentTree + || assignmentOrVariable instanceof VariableTree); + Type targetType = + assignmentOrVariable instanceof VariableTree variableTree + && isVarLocalVariableDeclaration(variableTree) + ? null + : getTreeType(assignmentOrVariable, state, calledFromDataflow); + return new TargetTypeAndAssignmentKind( + targetType, isAssignmentToLocalVariable(assignmentOrVariable)); + } + + /** + * Returns the outermost conditional expression enclosing {@code path}, ignoring parentheses + * between nested conditionals. + * + *

If {@code path} is not enclosed by another conditional expression, returns {@code path} + * itself. + */ + private TreePath getOutermostConditionalExpressionPath(TreePath path) { + TreePath conditionalPath = path; + TreePath parentPath = conditionalPath.getParentPath(); + while (parentPath != null) { + Tree parent = parentPath.getLeaf(); + while (parent instanceof ParenthesizedTree) { + parentPath = parentPath.getParentPath(); + parent = parentPath.getLeaf(); + } + if (!(parent instanceof ConditionalExpressionTree)) { + return conditionalPath; + } + conditionalPath = parentPath; + parentPath = conditionalPath.getParentPath(); } - return getTreeType(tree, state); + return conditionalPath; } /** @@ -1968,6 +2181,18 @@ public void compareGenericTypeParameterNullabilityForCall( formalParameter, false, false); + } else if (currentActualParam + instanceof ConditionalExpressionTree conditionalExpressionTree) { + actualParameterType = + inferConditionalExpressionType( + state.withPath(pathToParam), + conditionalExpressionTree, + pathToParam, + formalParameter, + false); + } + if (actualParameterType == null) { + return; } if (!subtypeParameterNullability(formalParameter, actualParameterType, state)) { reportInvalidParametersNullabilityError( @@ -2337,8 +2562,12 @@ private InvocationAndContext getInvocationAndContextForInference( parent = parentPath.getLeaf(); } if (parent instanceof AssignmentTree || parent instanceof VariableTree) { - return getInvocationInferenceInfoForAssignment( - parent, invocation, state.withPath(parentPath), calledFromDataflow); + TargetTypeAndAssignmentKind targetTypeAndAssignmentKind = + getTargetTypeForAssignmentContext(parent, state.withPath(parentPath), calledFromDataflow); + return new InvocationAndContext( + invocation, + targetTypeAndAssignmentKind.typeFromAssignmentContext(), + targetTypeAndAssignmentKind.assignedToLocal()); } else if (parent instanceof ReturnTree) { // find the enclosing method and return its return type TreePath enclosingMethodOrLambda = @@ -2399,33 +2628,23 @@ private InvocationAndContext getInvocationAndContextForInference( } } return new InvocationAndContext(invocation, formalParamType, false); + } else if (exprParent instanceof ConditionalExpressionTree) { + TreePath conditionalPath = getOutermostConditionalExpressionPath(parentPath); + TargetTypeAndAssignmentKind targetTypeAndAssignmentKind = + getTargetTypeForConditionalExpression( + (ConditionalExpressionTree) conditionalPath.getLeaf(), + state.withPath(conditionalPath), + calledFromDataflow); + return new InvocationAndContext( + invocation, + targetTypeAndAssignmentKind.typeFromAssignmentContext(), + targetTypeAndAssignmentKind.assignedToLocal()); } } // an unhandled case; for now, give up and return no assignment context return new InvocationAndContext(invocation, null, false); } - private InvocationAndContext getInvocationInferenceInfoForAssignment( - Tree assignment, - MethodInvocationTree invocation, - VisitorState state, - boolean calledFromDataflow) { - Preconditions.checkArgument( - assignment instanceof AssignmentTree || assignment instanceof VariableTree); - TreePath path = state.getPath(); - @SuppressWarnings("ReferenceEquality") // deliberate reference equality check - boolean leafIsAssignment = path.getLeaf() != assignment; - if (leafIsAssignment) { - state = state.withPath(pathWithLeaf(path, assignment)); - } - Type treeType = - assignment instanceof VariableTree variableTree - && isVarLocalVariableDeclaration(variableTree) - ? null // no info from assignment context if declared with `var` - : getTreeType(assignment, state, calledFromDataflow); - return new InvocationAndContext(invocation, treeType, isAssignmentToLocalVariable(assignment)); - } - /** * 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 diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/ConditionalExprTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/ConditionalExprTests.java new file mode 100644 index 0000000000..b735b72dd4 --- /dev/null +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/ConditionalExprTests.java @@ -0,0 +1,425 @@ +package com.uber.nullaway.jspecify; + +import com.google.errorprone.CompilationTestHelper; +import com.uber.nullaway.NullAwayTestsBase; +import com.uber.nullaway.generics.JSpecifyJavacConfig; +import java.util.List; +import org.junit.Test; + +public class ConditionalExprTests extends NullAwayTestsBase { + + @Test + public void wildcard() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + package com.example; + import org.jspecify.annotations.*; + @NullMarked + final class Test { + interface ClassLike { + String name(); + } + static String guardedNullableWildcard(@Nullable ClassLike maybeClass) { + return (maybeClass != null ? maybeClass : fallback()).name(); + } + static ClassLike fallback() { + throw new RuntimeException(); + } + } + """) + .doTest(); + } + + @Test + public void genericMethodCallInOneArm() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + package com.example; + import org.jspecify.annotations.*; + @NullMarked + final class Test { + String nonNullField = "hello"; + @Nullable String nullableField; + static T id(T t) { + return t; + } + void test(boolean flag) { + nullableField = flag ? id(null) : "fallback"; + // BUG: Diagnostic contains: passing @Nullable parameter + nonNullField = flag ? id(null) : "fallback"; + nonNullField = flag ? id("value") : "fallback"; + } + } + """) + .doTest(); + } + + @Test + public void genericMethodCallInBothArms() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + package com.example; + import org.jspecify.annotations.*; + @NullMarked + final class Test { + String nonNullField = "hello"; + @Nullable String nullableField; + static T id(T t) { + return t; + } + void test(boolean flag) { + nullableField = flag ? id(null) : id("fallback"); + // BUG: Diagnostic contains: passing @Nullable parameter + nonNullField = flag ? id(null) : id("fallback"); + nonNullField = flag ? id("value") : id("fallback"); + } + } + """) + .doTest(); + } + + @Test + public void nestedConditionalExpressions() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + package com.example; + import org.jspecify.annotations.*; + @NullMarked + final class Test { + String nonNullField = "hello"; + @Nullable String nullableField; + static T id(T t) { + return t; + } + void test(boolean first, boolean second) { + nullableField = first ? id(null) : second ? id("value") : "fallback"; + // BUG: Diagnostic contains: passing @Nullable parameter + nonNullField = first ? id(null) : second ? id("value") : "fallback"; + nonNullField = first ? id("first") : second ? id("second") : "fallback"; + } + } + """) + .doTest(); + } + + @Test + public void conditionalAsMethodArgument() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + package com.example; + import org.jspecify.annotations.*; + @NullMarked + final class Test { + static T id(T t) { + return t; + } + static void takesNonNull(String s) {} + static void takesNullable(@Nullable String s) {} + void test(boolean flag) { + takesNullable(flag ? id(null) : "fallback"); + // BUG: Diagnostic contains: passing @Nullable parameter + takesNonNull(flag ? id(null) : "fallback"); + takesNonNull(flag ? id("value") : "fallback"); + } + } + """) + .doTest(); + } + + @Test + public void conditionalInfersGenericTypeArguments() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + package com.example; + import org.jspecify.annotations.*; + @NullMarked + final class Test { + static final class Box {} + static Box box(T t) { + return new Box(); + } + void test(boolean flag) { + Box<@Nullable String> nullableBox = flag ? box(null) : box("fallback"); + // BUG: Diagnostic contains: passing @Nullable parameter + Box nonNullBox = flag ? box(null) : box("fallback"); + Box nonNullBoxOk = flag ? box("value") : box("fallback"); + } + } + """) + .doTest(); + } + + @Test + public void varConditionalDoesNotProvideTargetTypeForGenericMethodInference() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + package com.example; + import org.jspecify.annotations.*; + @NullMarked + final class Test { + interface Box { + T get(); + } + static Box box(T t) { + throw new RuntimeException(); + } + void test(boolean flag) { + var inferredFromInitializer = flag ? box(null) : box("fallback"); + Box<@Nullable String> explicitNullableTarget = + flag ? box(null) : box("fallback"); + Box explicitNonNullTarget = + // BUG: Diagnostic contains: passing @Nullable parameter + flag ? box(null) : box("fallback"); + } + } + """) + .doTest(); + } + + @Test + public void nestedConditionalInfersGenericTypeArguments() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + package com.example; + import org.jspecify.annotations.*; + @NullMarked + final class Test { + static final class Box {} + static Box box(T t) { + return new Box(); + } + void test(boolean first, boolean second) { + Box<@Nullable String> nullableBox = + first ? box(null) : second ? box("value") : box("fallback"); + Box nonNullBox = + // BUG: Diagnostic contains: passing @Nullable parameter + first ? box(null) : second ? box("value") : box("fallback"); + Box nonNullBoxOk = + first ? box("first") : second ? box("second") : box("fallback"); + } + } + """) + .doTest(); + } + + @Test + public void conditionalInfersDiamondTypeArguments() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + package com.example; + import org.jspecify.annotations.*; + @NullMarked + final class Test { + static final class Box { + Box(T t) {} + } + void test(boolean flag) { + Box<@Nullable String> nullableBox = + flag ? new Box<>(null) : new Box<>("fallback"); + Box nonNullBox = + // BUG: Diagnostic contains: passing @Nullable parameter + flag ? new Box<>(null) : new Box<>("fallback"); + Box nonNullBoxOk = + flag ? new Box<>("value") : new Box<>("fallback"); + } + } + """) + .doTest(); + } + + @Test + public void varConditionalDoesNotProvideTargetTypeForDiamondInference() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + package com.example; + import org.jspecify.annotations.*; + @NullMarked + final class Test { + static final class Box { + private final T value; + Box(T value) { + this.value = value; + } + T get() { + return value; + } + } + void test(boolean flag) { + // 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"); + Box explicitNonNullTarget = + // BUG: Diagnostic contains: passing @Nullable parameter + flag ? new Box<>(null) : new Box<>("fallback"); + } + } + """) + .doTest(); + } + + @Test + public void nestedConditionalInfersDiamondTypeArguments() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + package com.example; + import org.jspecify.annotations.*; + @NullMarked + final class Test { + static final class Box { + Box(T t) {} + } + void test(boolean first, boolean second) { + Box<@Nullable String> nullableBox = + first + ? new Box<>(null) + : second ? new Box<>("value") : new Box<>("fallback"); + Box nonNullBox = + first + // BUG: Diagnostic contains: passing @Nullable parameter + ? new Box<>(null) + : second ? new Box<>("value") : new Box<>("fallback"); + Box nonNullBoxOk = + first + ? new Box<>("first") + : second ? new Box<>("second") : new Box<>("fallback"); + } + } + """) + .doTest(); + } + + @Test + public void conditionalGenericMethodInferenceWithDataflow() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + package com.example; + import org.jspecify.annotations.*; + @NullMarked + final class Test { + String nonNullField = "hello"; + @Nullable String nullableField; + static T id(T t) { + return t; + } + void test(boolean flag, @Nullable String s) { + if (s != null) { + nonNullField = flag ? id(s) : id("fallback"); + } + nullableField = flag ? id(s) : id("fallback"); + // BUG: Diagnostic contains: passing @Nullable parameter + nonNullField = flag ? id(s) : id("fallback"); + } + } + """) + .doTest(); + } + + @Test + public void conditionalGenericTypeArgumentInferenceWithDataflow() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + package com.example; + import org.jspecify.annotations.*; + @NullMarked + final class Test { + static final class Box {} + static Box box(T t) { + return new Box(); + } + void test(boolean flag, @Nullable String s) { + if (s != null) { + Box nonNullBox = flag ? box(s) : box("fallback"); + } + Box<@Nullable String> nullableBox = flag ? box(s) : box("fallback"); + // BUG: Diagnostic contains: passing @Nullable parameter + Box bad = flag ? box(s) : box("fallback"); + } + } + """) + .doTest(); + } + + @Test + public void conditionalGenericMethodInferenceDataflowAndLoops() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + package com.example; + import org.jspecify.annotations.*; + @NullMarked + final class Test { + static T id(T t) { + return t; + } + void testLoop1(boolean flag) { + String s = "hello"; + while (true) { + String t = flag ? id(s) : id("fallback"); + // BUG: Diagnostic contains: dereferenced expression 't' is @Nullable + t.hashCode(); + s = null; + } + } + void testLoop2(boolean flag) { + String t = "hello"; + while (true) { + // BUG: Diagnostic contains: dereferenced expression 't' is @Nullable + t.hashCode(); + String s = null; + t = flag ? id(s) : id("fallback"); + } + } + void testLoop3(boolean flag) { + String t = "hello"; + String s = "hello"; + t.hashCode(); + int i = 2; + while (i > 0) { + t = flag ? id(s) : id("fallback"); + s = null; + i--; + } + // BUG: Diagnostic contains: dereferenced expression 't' is @Nullable + t.hashCode(); + } + } + """) + .doTest(); + } + + private CompilationTestHelper makeHelperWithInferenceFailureWarning() { + return makeTestHelperWithArgs( + JSpecifyJavacConfig.withJSpecifyModeArgs( + List.of( + "-XepOpt:NullAway:OnlyNullMarked=true", + "-XepOpt:NullAway:WarnOnGenericInferenceFailure=true"))); + } +} 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 07c4ac7ed5..21d6240888 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java @@ -44,6 +44,36 @@ void testPositive() { .doTest(); } + @Test + public void varLocalDoesNotProvideTargetTypeForDiamondInference() { + makeHelper() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.*; + @NullMarked + public class Test { + static class Box { + private final T value; + Box(T value) { + this.value = value; + } + T get() { + return value; + } + } + void test() { + // BUG: Diagnostic contains: passing @Nullable parameter + var inferredFromInitializer = new Box<>(null); + // BUG: Diagnostic contains: passing @Nullable parameter + Box explicitNonNullTarget = new Box<>(null); + Box<@Nullable String> explicitNullableTarget = new Box<>(null); + } + } + """) + .doTest(); + } + @Test public void returnDiamond() { makeHelper() diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/VarDeclaredLocalTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/VarDeclaredLocalTests.java index 845ce123a2..b31b56e9b3 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/VarDeclaredLocalTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/VarDeclaredLocalTests.java @@ -40,6 +40,36 @@ void test() { .doTest(); } + @Test + public void varLocalDoesNotProvideTargetTypeForGenericMethodInference() { + makeHelper() + .addSourceLines( + "Test.java", + """ + package com.uber; + import org.jspecify.annotations.NullMarked; + import org.jspecify.annotations.Nullable; + @NullMarked + class Test { + interface Box { + T get(); + } + static Box box(U u) { + throw new RuntimeException(); + } + void test() { + var inferredFromInitializer = box(null); + // BUG: Diagnostic contains: dereferenced expression 'inferredFromInitializer.get()' is @Nullable + inferredFromInitializer.get().hashCode(); + // BUG: Diagnostic contains: inference failure + Box explicitNonNullTarget = box(null); + Box<@Nullable String> explicitNullableTarget = box(null); + } + } + """) + .doTest(); + } + @Test public void varLocalReassigned() { makeHelper()