diff --git a/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java b/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java index fc710c5958..422d1295b0 100644 --- a/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java +++ b/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java @@ -714,9 +714,29 @@ private static boolean isCatchVariable(VariableDeclarationNode node) { return variableElement != null && variableElement.getKind() == EXCEPTION_PARAMETER; } + /** + * In JSpecify mode, this method invokes {@link + * GenericsChecks#registerVarLocalDeclaration(VariableTree)} to register {@code var}-declared + * locals. This is required since sometimes during dataflow analysis, we require the full generic + * type of such a variable at a use, which may require running generic method inference on the + * right-hand side of the declaration. javac does not provide an efficient way to retrieve a local + * variable declaration given its {@code Symbol}. So, as an efficient alternative, we cache all + * such declarations when they are first visited by dataflow analysis, in case inference needs to + * be performed later at a use. + * + *
Note that the above assumes dataflow analysis will always see the declaration of a local
+ * before any use. This depends on the worklist ordering used by the dataflow solver, which is a
+ * fragile dependence. But, any alternative would require running a separate visitor over the
+ * whole method body (or a traversal of the full CFG) to find the declarations, which could be
+ * expensive. So, we go with this approach, and our tests should catch if there is some unexpected
+ * change in worklist orderings.
+ */
@Override
public TransferResult We should remove this method if / when CF issue 7726 is fixed in a suitable manner.
+ */
+ @Override
+ public @Nullable V getValue(Node n) {
+ if (isRunning) {
+ if (currentNode == null
+ || currentNode == n
+ || (currentTree != null && currentTree == n.getTree())) {
+ return null;
+ }
+ assert !n.isLValue() : "Did not expect an lvalue, but got " + n;
+ }
+ return nodeValues.get(n);
+ }
}
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 0931007d68..de4b8aadf4 100644
--- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java
+++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java
@@ -115,6 +115,12 @@ private static final class InferenceFailure implements MethodInferenceResult {
*/
private final Map This method is required because in some cases, the type returned by {@link
+ * com.google.errorprone.util.ASTHelpers#getType(Tree)} fails to preserve type use annotations,
+ * e.g., when dealing with {@link com.sun.source.tree.NewClassTree} (e.g., {@code new
+ * Foo<@Nullable A>}).
+ *
+ * @param tree A tree for which we need the type with preserved annotations.
+ * @param state the visitor state
+ * @param calledFromDataflow true if the type is being computed as part of dataflow analysis
+ * @return Type of the tree with preserved annotations.
+ */
+ private @Nullable Type getTreeType(Tree tree, VisitorState state, boolean calledFromDataflow) {
if (tree instanceof ExpressionTree exprTree) {
NullabilityUtil.ExprTreeAndState exprTreeAndState =
NullabilityUtil.stripParensAndUpdateTreePath(exprTree, state);
@@ -492,7 +516,8 @@ private void reportInvalidOverridingMethodParamTypeError(
// 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);
+ Type fromAssignmentContext =
+ getDiamondTypeFromContext(newClassTree, state, calledFromDataflow);
if (fromAssignmentContext != null) {
return fromAssignmentContext;
}
@@ -533,6 +558,11 @@ private void reportInvalidOverridingMethodParamTypeError(
return lambdaParameterType;
}
}
+ // If it's a local variable declared using `var`, get the inferred type
+ Type inferredVarLocalType = getInferredVarLocalType(symbol, state, calledFromDataflow);
+ if (inferredVarLocalType != null) {
+ return inferredVarLocalType;
+ }
result = ASTHelpers.getType(tree);
// type on the tree itself can be missing nested annotations in certain cases, so use the
// type on the symbol instead. for type variables, we've found that the type on the symbol
@@ -546,7 +576,8 @@ private void reportInvalidOverridingMethodParamTypeError(
// the symbol for the assigned location instead, if available
Symbol lhsSymbol = ASTHelpers.getSymbol(assignmentTree.getVariable());
if (lhsSymbol != null) {
- result = lhsSymbol.type;
+ Type inferredVarLocalType = getInferredVarLocalType(lhsSymbol, state, calledFromDataflow);
+ result = inferredVarLocalType != null ? inferredVarLocalType : lhsSymbol.type;
} else {
result = ASTHelpers.getType(assignmentTree);
}
@@ -566,7 +597,7 @@ private void reportInvalidOverridingMethodParamTypeError(
Type invokedMethodType = symbol.type;
Type enclosingType =
getEnclosingTypeForCallExpression(
- symbol, invocationTree, state.getPath(), state, false);
+ symbol, invocationTree, state.getPath(), state, calledFromDataflow);
if (enclosingType != null) {
invokedMethodType =
TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, symbol, config);
@@ -609,9 +640,10 @@ private void reportInvalidOverridingMethodParamTypeError(
* 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) {
+ private @Nullable Type getDiamondTypeFromContext(
+ NewClassTree tree, VisitorState state, boolean calledFromDataflow) {
return getDiamondTypeFromParentContext(
- tree, state, castToNonNull(state.getPath().getParentPath()));
+ tree, state, castToNonNull(state.getPath().getParentPath()), calledFromDataflow);
}
/**
@@ -619,7 +651,7 @@ private void reportInvalidOverridingMethodParamTypeError(
* parent context.
*/
private @Nullable Type getDiamondTypeFromParentContext(
- NewClassTree tree, VisitorState state, TreePath parentPath) {
+ NewClassTree tree, VisitorState state, TreePath parentPath, boolean calledFromDataflow) {
Tree parent = parentPath.getLeaf();
while (parent instanceof ParenthesizedTree) {
parentPath = parentPath.getParentPath();
@@ -629,7 +661,7 @@ private void reportInvalidOverridingMethodParamTypeError(
parent = parentPath.getLeaf();
}
if (parent instanceof VariableTree || parent instanceof AssignmentTree) {
- return getTreeType(parent, state.withPath(parentPath));
+ return getTreeType(parent, state.withPath(parentPath), calledFromDataflow);
}
if (parent instanceof ReturnTree) {
TreePath enclosingMethodOrLambda =
@@ -658,7 +690,8 @@ private void reportInvalidOverridingMethodParamTypeError(
}
if (parent instanceof NewClassTree parentConstructorCall) {
// get the type returned by the parent constructor call
- Type parentClassType = getTreeType(parentConstructorCall, state.withPath(parentPath));
+ 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
@@ -796,18 +829,17 @@ public void checkTypeParameterNullnessForAssignability(Tree tree, VisitorState s
&& isAssignmentToField(tree)) {
maybeStorePolyExpressionTypeFromTarget(rhsTree, lhsType);
}
+ boolean varLocalDeclaration =
+ tree instanceof VariableTree varTree && isVarLocalVariableDeclaration(varTree);
TreePath pathToRhs = new TreePath(state.getPath(), rhsTree);
- Type rhsType = getTreeType(rhsTree, state.withPath(pathToRhs));
+ Type rhsType =
+ varLocalDeclaration
+ ? getInferredTypeForVarLocalDeclaration(
+ (VariableTree) tree, rhsTree, pathToRhs, state, false)
+ : getTypeForRhsOfAssignment(rhsTree, pathToRhs, lhsType, assignedToLocal, state, false);
if (rhsType != null) {
- if (isGenericCallNeedingInference(rhsTree)) {
- rhsType =
- inferGenericMethodCallType(
- state.withPath(pathToRhs),
- (MethodInvocationTree) rhsTree,
- pathToRhs,
- lhsType,
- assignedToLocal,
- false);
+ if (varLocalDeclaration) {
+ lhsType = rhsType;
}
boolean isAssignmentValid = subtypeParameterNullability(lhsType, rhsType, state);
if (!isAssignmentValid) {
@@ -817,7 +849,104 @@ && isAssignmentToField(tree)) {
}
private static boolean isAssignmentToLocalVariable(Tree tree) {
- return isAssignmentToKind(tree, ElementKind.LOCAL_VARIABLE);
+ return isAssignmentToKind(tree, ElementKind.LOCAL_VARIABLE)
+ || isAssignmentToKind(tree, ElementKind.RESOURCE_VARIABLE);
+ }
+
+ private static boolean isVarLocalVariableDeclaration(VariableTree tree) {
+ return tree instanceof JCTree.JCVariableDecl variableDecl && variableDecl.declaredUsingVar();
+ }
+
+ /**
+ * Associates the {@code Symbol} for a {@code var}-declared local with the local's declaration.
+ * This is used in case we observe a use of the local before we have processed the declaration
+ * (e.g., due to dataflow analysis), and need to jump to the declaration to infer the variable's
+ * type.
+ */
+ public void registerVarLocalDeclaration(VariableTree tree) {
+ if (!isVarLocalVariableDeclaration(tree)) {
+ return;
+ }
+ Symbol symbol = ASTHelpers.getSymbol(tree);
+ if (symbol != null
+ && (symbol.getKind().equals(ElementKind.LOCAL_VARIABLE)
+ || symbol.getKind().equals(ElementKind.RESOURCE_VARIABLE))) {
+ varLocalDeclarations.put(symbol, (JCTree.JCVariableDecl) tree);
+ }
+ }
+
+ /**
+ * Gets the inferred type for a local variable declared with {@code var}.
+ *
+ * @param symbol symbol for the local
+ * @param state visitor state
+ * @param calledFromDataflow whether this method was called as part of dataflow analysis
+ * @return the inferred type, or {@code null} if the symbol is not for a var-declared local
+ */
+ private @Nullable Type getInferredVarLocalType(
+ Symbol symbol, VisitorState state, boolean calledFromDataflow) {
+ Type cachedType = inferredVarLocalTypes.get(symbol);
+ if (cachedType != null) {
+ return cachedType;
+ }
+ VariableTree variableDecl = varLocalDeclarations.get(symbol);
+ if (variableDecl == null) {
+ return null;
+ }
+ ExpressionTree initializer = variableDecl.getInitializer();
+ if (initializer == null) {
+ // this can happen for enhanced for loops
+ // TODO handle this properly; see https://github.com/uber/NullAway/issues/1581
+ return typeOrNullIfRaw(symbol.type);
+ }
+ TreePath pathToInitializer = pathWithLeaf(state.getPath(), initializer);
+ return getInferredTypeForVarLocalDeclaration(
+ variableDecl, initializer, pathToInitializer, state, calledFromDataflow);
+ }
+
+ private @Nullable Type getInferredTypeForVarLocalDeclaration(
+ VariableTree varTree,
+ ExpressionTree initializer,
+ TreePath pathToInitializer,
+ VisitorState state,
+ boolean calledFromDataflow) {
+ Type rhsType =
+ getTypeForRhsOfAssignment(
+ initializer,
+ pathToInitializer,
+ null,
+ isAssignmentToLocalVariable(varTree),
+ state,
+ calledFromDataflow);
+ // do _not_ cache the inferred type if called from dataflow, since it may rely on incomplete
+ // results from the dataflow analysis
+ if (rhsType != null && !calledFromDataflow) {
+ Symbol symbol = ASTHelpers.getSymbol(varTree);
+ if (symbol != null) {
+ inferredVarLocalTypes.put(symbol, rhsType);
+ }
+ }
+ return rhsType;
+ }
+
+ private @Nullable Type getTypeForRhsOfAssignment(
+ ExpressionTree rhsTree,
+ TreePath pathToRhs,
+ @Nullable Type typeFromAssignmentContext,
+ boolean assignedToLocal,
+ VisitorState state,
+ boolean calledFromDataflow) {
+ if (isGenericCallNeedingInference(rhsTree)) {
+ return inferGenericMethodCallType(
+ state.withPath(pathToRhs),
+ (MethodInvocationTree) rhsTree,
+ pathToRhs,
+ typeFromAssignmentContext,
+ assignedToLocal,
+ calledFromDataflow);
+ } else {
+ return getTreeType(rhsTree, state.withPath(pathToRhs), calledFromDataflow);
+ }
}
private static boolean isAssignmentToField(Tree tree) {
@@ -924,7 +1053,8 @@ private MethodInferenceResult runInferenceForCall(
solver,
methodSymbol,
invocationTree,
- allInvocations);
+ allInvocations,
+ 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
@@ -934,22 +1064,6 @@ private MethodInferenceResult runInferenceForCall(
typeVarNullability.putIfAbsent(typeVar, ConstraintSolver.InferredNullability.NONNULL);
}
- // Store inferred types for lambda arguments
- new InvocationArguments(invocationTree, methodSymbol.type.asMethodType())
- .forEach(
- (argument, argPos, formalParamType, unused) -> {
- if (argument instanceof LambdaExpressionTree
- || argument instanceof MemberReferenceTree) {
- Type polyExprTreeType = ASTHelpers.getType(argument);
- if (polyExprTreeType != null) {
- Type typeWithInferredNullability =
- TypeSubstitutionUtils.updateTypeWithInferredNullability(
- polyExprTreeType, formalParamType, typeVarNullability, state, config);
- inferredPolyExpressionTypes.put(argument, typeWithInferredNullability);
- }
- }
- });
-
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
@@ -957,6 +1071,21 @@ private MethodInferenceResult runInferenceForCall(
for (MethodInvocationTree invTree : allInvocations) {
inferredTypeVarNullabilityForGenericCalls.put(invTree, successResult);
}
+ // Store inferred types for lambda or method reference arguments
+ new InvocationArguments(invocationTree, methodSymbol.type.asMethodType())
+ .forEach(
+ (argument, argPos, formalParamType, unused) -> {
+ if (argument instanceof LambdaExpressionTree
+ || argument instanceof MemberReferenceTree) {
+ Type polyExprTreeType = ASTHelpers.getType(argument);
+ if (polyExprTreeType != null) {
+ Type typeWithInferredNullability =
+ TypeSubstitutionUtils.updateTypeWithInferredNullability(
+ polyExprTreeType, formalParamType, typeVarNullability, state, config);
+ inferredPolyExpressionTypes.put(argument, typeWithInferredNullability);
+ }
+ }
+ });
}
return successResult;
} catch (UnsatisfiableConstraintsException e) {
@@ -1004,6 +1133,7 @@ private String inferenceFailureMessage(UnsatisfiableConstraintsException e) {
* @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 calledFromDataflow whether this method is being called from dataflow analysis
* @throws UnsatisfiableConstraintsException if the constraints are determined to be unsatisfiable
*/
private void generateConstraintsForCall(
@@ -1014,7 +1144,8 @@ private void generateConstraintsForCall(
ConstraintSolver solver,
Symbol.MethodSymbol methodSymbol,
MethodInvocationTree methodInvocationTree,
- Set