Skip to content

Commit

Permalink
Fix assertion check for structure of enhanced-for loop over a Map key…
Browse files Browse the repository at this point in the history
…Set (#868)

Fixes #866 

Before, we would check that an enhanced-for loop includes a call to
`Set.iterator()` on the result of calling `Map.keySet()`. However, it is
possible and legal that statically, the target of this call is instead
`Collection.iterator()`. So, we change our check to test the receiver
type passed into the call (which must still be a `Set`). Also,
opportunistically switch a couple of places we were throwing
`RuntimeException` around this check to throw the more meaningful
`VerifyException`. Unfortunately, we have not found a way to add a test
in open-source to reproduce the failure from #866 but we have confirmed
this change fixes the problem.
  • Loading branch information
msridhar committed Nov 27, 2023
1 parent 8f4f8a6 commit bf74867
Showing 1 changed file with 25 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import static org.checkerframework.nullaway.javacutil.TreeUtils.elementFromDeclaration;

import com.google.common.base.Preconditions;
import com.google.common.base.VerifyException;
import com.google.errorprone.VisitorState;
import com.google.errorprone.suppliers.Supplier;
import com.google.errorprone.suppliers.Suppliers;
Expand Down Expand Up @@ -578,7 +579,7 @@ private void handleEnhancedForOverKeySet(
if (mapWithIteratorContentsKey != null) {
// put sanity check here to minimize perf impact
if (!isCallToMethod(rhsInv, SET_TYPE_SUPPLIER, "iterator")) {
throw new RuntimeException(
throw new VerifyException(
"expected call to iterator(), instead saw "
+ state.getSourceForNode(rhsInv.getTree()));
}
Expand All @@ -603,7 +604,7 @@ && isEnhancedForIteratorVariable((LocalVariableNode) receiver)) {
if (mapGetPath != null) {
// put sanity check here to minimize perf impact
if (!isCallToMethod(methodInv, ITERATOR_TYPE_SUPPLIER, "next")) {
throw new RuntimeException(
throw new VerifyException(
"expected call to next(), instead saw "
+ state.getSourceForNode(methodInv.getTree()));
}
Expand Down Expand Up @@ -633,14 +634,32 @@ private Node getMapNodeForKeySetIteratorCall(MethodInvocationNode invocationNode
return null;
}

/**
* Checks if an invocation node represents a call to a method on a given type
*
* @param invocationNode the invocation node
* @param containingTypeSupplier supplier for the type containing the method
* @param methodName name of the method
* @return true if the invocation node represents a call to the method on the type
*/
private boolean isCallToMethod(
MethodInvocationNode invocationNode,
Supplier<Type> containingTypeSupplier,
String methodName) {
Symbol.MethodSymbol symbol = ASTHelpers.getSymbol(invocationNode.getTree());
return symbol != null
&& symbol.getSimpleName().contentEquals(methodName)
&& ASTHelpers.isSubtype(symbol.owner.type, containingTypeSupplier.get(state), state);
MethodInvocationTree invocationTree = invocationNode.getTree();
Symbol.MethodSymbol symbol = ASTHelpers.getSymbol(invocationTree);
if (symbol != null && symbol.getSimpleName().contentEquals(methodName)) {
// NOTE: previously we checked if symbol.owner.type was a subtype of the containing type.
// However, symbol.owner.type refers to the static type at the call site, in which the target
// class/interface might be a supertype of the containing type with some Java compilers.
// Instead, we now check if the static type of the receiver at the invocation is a subtype of
// the containing type (as this guarantees a method in the containing type or one of its
// subtypes will be invoked, assuming such a method exists). See
// https://github.com/uber/NullAway/issues/866.
return ASTHelpers.isSubtype(
ASTHelpers.getReceiverType(invocationTree), containingTypeSupplier.get(state), state);
}
return false;
}

/**
Expand Down

0 comments on commit bf74867

Please sign in to comment.