package checkers.nullness;
import static checkers.util.Heuristics.Matchers.*;
import java.util.List;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import checkers.nullness.quals.KeyFor;
import checkers.types.AnnotatedTypeMirror;
import checkers.types.AnnotatedTypeMirror.AnnotatedDeclaredType;
import checkers.types.AnnotatedTypeMirror.AnnotatedExecutableType;
import checkers.types.AnnotatedTypeFactory;
import checkers.util.AnnotationUtils;
import checkers.util.Heuristics.Matcher;
import checkers.util.InternalUtils;
import checkers.util.TreeUtils;
import checkers.util.Heuristics.Matchers;
import com.sun.source.tree.*;
import com.sun.source.util.TreePath;
/**
* Utilities class for handling {@code Map.get()} invocations.
*
* The heuristics cover the following cases:
*
* <ol>
* <li value="1">Within the true condition of a map.containsKey() if statement:
* <pre><code>if (map.containsKey(key)) { Object v = map.get(key); }</code></pre>
* </li>
*
* <li value="2">Within an enhanced-for loop of the map.keySet():
* <pre><code>for (Object key: map.keySet()) { Object v = map.get(key); }</code></pre>
* </li>
*
* <li value="3">Preceded by an assertion of contains or nullness get check:
* <pre><code>assert map.containsKey(key);
* Object v = map.get(key);</code></pre>
*
* Or
*
* <pre><code>assert map.get(key) != null;
* Object v = map.get(key);</code></pre>
*
* <li value="4">Preceded by an check of contains or nullness if
* test that throws an exception, in the first line:
*
* <pre><code>if (!map.contains(key)) throw new Exception();
* Object v = map.get(key);
* </code></pre>
*
* <li value="5">Preceded by a put-if-absent pattern convention:
*
* <pre><code>if (!map.contains(key)) map.put(key, DEFAULT_VALUE);
* Object v = map.get(key);</code></pre>
*
* </ol>
*/
/*package-scope*/ class MapGetHeuristics {
private final ProcessingEnvironment env;
private final Elements elements;
private final NullnessAnnotatedTypeFactory factory;
private final AnnotatedTypeFactory keyForFactory;
private final ExecutableElement mapGet;
private final ExecutableElement mapPut;
private final ExecutableElement mapKeySet;
private final ExecutableElement mapContains;
public MapGetHeuristics(ProcessingEnvironment env,
NullnessAnnotatedTypeFactory factory,
AnnotatedTypeFactory keyForFactory) {
this.env = env;
this.elements = env.getElementUtils();
this.factory = factory;
this.keyForFactory = keyForFactory;
mapGet = getMethod("java.util.Map", "get", 1);
mapPut = getMethod("java.util.Map", "put", 2);
mapKeySet = getMethod("java.util.Map", "keySet", 0);
mapContains = getMethod("java.util.Map", "containsKey", 1);
}
public void handle(MethodInvocationTree tree, AnnotatedExecutableType method) {
if (isMethod(tree, mapGet)) {
AnnotatedTypeMirror type = method.getReturnType();
type.clearAnnotations();
if (!isSuppressable((MethodInvocationTree)tree)) {
type.addAnnotation(factory.NULLABLE);
} else {
type.addAnnotation(factory.NONNULL);
}
}
}
/**
* Checks whether the key passed to {@code Map.get(K key)} is known
* to be in the map.
*
* TODO: Document when this method returns true
*/
private boolean isSuppressable(MethodInvocationTree tree) {
Element elt = getSite(tree);
if (elt instanceof VariableElement
&& tree.getArguments().get(0) instanceof IdentifierTree
&& isKeyInMap((IdentifierTree)tree.getArguments().get(0),
(VariableElement)elt)) {
return true;
}
if (elt instanceof VariableElement) {
ExpressionTree arg = tree.getArguments().get(0);
return keyForInMap(arg, ((VariableElement)elt).getSimpleName().toString())
|| keyForInMap(arg, String.valueOf(TreeUtils.getReceiverTree(tree)));
}
return false;
}
/**
* Returns true if the key is a member of the specified map
*/
private boolean keyForInMap(ExpressionTree key,
String mapName) {
AnnotatedTypeMirror keyForType = keyForFactory.getAnnotatedType(key);
AnnotationMirror anno = keyForType.getAnnotation(KeyFor.class);
if (anno == null)
return false;
List<String> maps = AnnotationUtils.parseStringArrayValue(anno, "value");
return maps.contains(mapName);
}
/**
* Case 1: get() is within true clause of map.containsKey()
*/
public Matcher inContains(final Element key, final VariableElement map) {
return or(whenTrue(new Matcher() {
@Override public Boolean visitMethodInvocation(MethodInvocationTree node, Void p) {
return isInvocationOfContains(key, map, node);
}
}), withIn(ofKind(Tree.Kind.CONDITIONAL_EXPRESSION, new Matcher() {
@Override public Boolean visitConditionalExpression(ConditionalExpressionTree tree, Void p) {
return isInvocationOfContains(key, map, tree.getCondition());
}
})));
}
/**
* Case 2: get() is within enhanced for-loop over the keys
*/
private Matcher inForEnhanced(final Element key,
final VariableElement map) {
return withIn(ofKind(Tree.Kind.ENHANCED_FOR_LOOP, new Matcher() {
@Override public Boolean visitEnhancedForLoop(EnhancedForLoopTree tree, Void p) {
if (key.equals(TreeUtils.elementFromDeclaration(tree.getVariable())))
return visit(tree.getExpression(), p);
return false;
}
@Override public Boolean visitMethodInvocation(MethodInvocationTree tree, Void p) {
return (isMethod(tree, mapKeySet) && map.equals(getSite(tree)));
}
}));
}
/**
* Case 3: get() is preceded with an assert
*/
private Matcher preceededByAssert(final Element key, final VariableElement map) {
return preceededBy(ofKind(Tree.Kind.ASSERT, new Matcher() {
@Override public Boolean visitAssert(AssertTree tree, Void p) {
return isInvocationOfContains(key, map, tree.getCondition())
|| isCheckOfGet(key, map, tree.getCondition());
}
}));
}
private boolean isTerminating(StatementTree tree) {
StatementTree first = firstStatement(tree);
if (first instanceof ThrowTree)
return true;
if (first instanceof ReturnTree)
return true;
if (first instanceof IfTree) {
IfTree ifTree = (IfTree)first;
if (ifTree.getElseStatement() != null
&& isTerminating(ifTree.getThenStatement())
&& isTerminating(ifTree.getElseStatement()))
return true;
}
return false;
}
/**
* Case 4: get() is preceded with explicit assertion
*/
private Matcher preceededByExplicitAssert(final Element key,
final VariableElement map) {
return preceededBy(ofKind(Tree.Kind.IF, new Matcher() {
@Override public Boolean visitIf(IfTree tree, Void p) {
return (isNotContained(key, map, tree.getCondition())
&& isTerminating(tree.getThenStatement()));
}
}));
}
/**
* Case 5: get() is preceded by put-if-abset pattern
*/
private Matcher preceededByIfThenPut(final Element key, final VariableElement map) {
return preceededBy(ofKind(Tree.Kind.IF, new Matcher() {
@Override public Boolean visitIf(IfTree tree, Void p) {
if (isNotContained(key, map, tree.getCondition())) {
StatementTree first = firstStatement(tree.getThenStatement());
if (first != null
&& first.getKind() == Tree.Kind.EXPRESSION_STATEMENT
&& isInvocationOfPut(key, map, ((ExpressionStatementTree)first).getExpression())) {
return true;
}
}
return false;
}
}));
}
private Matcher keyInMatcher(Element key, VariableElement map) {
return or(inContains(key, map),
inForEnhanced(key, map),
preceededByAssert(key, map),
preceededByExplicitAssert(key, map),
preceededByIfThenPut(key, map)
);
}
/**
* Checks for the supported patterns, and determines if we can
* infer that the queried key exists in the map
*
* @param keyTree the argument passed to {@code Map.get()}
* @param map the symbol of map
* @return true if key is in the map
*/
private boolean isKeyInMap(IdentifierTree keyTree, VariableElement map) {
TreePath path = factory.getPath(keyTree);
Element key = TreeUtils.elementFromUse(keyTree);
return keyInMatcher(key, map).match(path);
}
private Element getSite(MethodInvocationTree tree) {
AnnotatedDeclaredType type =
(AnnotatedDeclaredType)factory.getReceiver(tree);
return type.getElement();
}
/**
* Returns true if the given element is an invocation of the method, or
* of any method that overrides that one.
*/
private boolean isMethod(Tree tree, ExecutableElement method) {
if (!(tree instanceof MethodInvocationTree))
return false;
MethodInvocationTree methInvok = (MethodInvocationTree)tree;
ExecutableElement invoked = TreeUtils.elementFromUse(methInvok);
return isMethod(invoked, method);
}
/**
* Returns true if the given element is an invocation of the method, or
* of any method that overrides that one.
*/
private boolean isMethod(ExecutableElement questioned, ExecutableElement method) {
return (questioned.equals(method)
|| env.getElementUtils().overrides(questioned, method,
(TypeElement)questioned.getEnclosingElement()));
}
private ExecutableElement getMethod(String typeName, String methodName, int params) {
TypeElement mapElt = env.getElementUtils().getTypeElement(typeName);
for (ExecutableElement exec : ElementFilter.methodsIn(mapElt.getEnclosedElements())) {
if (exec.getSimpleName().contentEquals(methodName)
&& exec.getParameters().size() == params)
return exec;
}
throw new RuntimeException("Shouldn't be here!");
}
private boolean isInvocationOfContains(Element key, VariableElement map, Tree tree) {
if (TreeUtils.skipParens(tree) instanceof MethodInvocationTree) {
MethodInvocationTree invok = (MethodInvocationTree)TreeUtils.skipParens(tree);
if (isMethod(invok, mapContains)) {
Element containsArgument = InternalUtils.symbol(invok.getArguments().get(0));
if (key.equals(containsArgument) && map.equals(getSite(invok)))
return true;
}
}
return false;
}
private boolean isInvocationOfPut(Element key, VariableElement map, Tree tree) {
if (TreeUtils.skipParens(tree) instanceof MethodInvocationTree) {
MethodInvocationTree invok = (MethodInvocationTree)TreeUtils.skipParens(tree);
if (isMethod(invok, mapPut)) {
Element containsArgument = InternalUtils.symbol(invok.getArguments().get(0));
if (key.equals(containsArgument) && map.equals(getSite(invok)))
return true;
}
}
return false;
}
private boolean isNotContained(Element key, VariableElement map, ExpressionTree tree) {
tree = TreeUtils.skipParens(tree);
return (tree.getKind() == Tree.Kind.LOGICAL_COMPLEMENT
&& isInvocationOfContains(key, map, ((UnaryTree)tree).getExpression()));
}
private StatementTree firstStatement(StatementTree tree) {
StatementTree first = tree;
while (first.getKind() == Tree.Kind.BLOCK) {
List<? extends StatementTree> trees = ((BlockTree)first).getStatements();
if (trees.isEmpty())
return null;
else
first = trees.iterator().next();
}
return first;
}
private boolean isCheckOfGet(Element key, VariableElement map, Tree tree) {
tree = TreeUtils.skipParens(tree);
if (tree.getKind() != Tree.Kind.NOT_EQUAL_TO
|| ((BinaryTree)tree).getRightOperand().getKind() != Tree.Kind.NULL_LITERAL)
return false;
Tree right = TreeUtils.skipParens(((BinaryTree)tree).getLeftOperand());
if (right instanceof MethodInvocationTree) {
MethodInvocationTree invok = (MethodInvocationTree)right;
if (isMethod(invok, mapGet)) {
Element containsArgument = InternalUtils.symbol(invok.getArguments().get(0));
if (key.equals(containsArgument) && map.equals(getSite(invok)))
return true;
}
}
return false;
}
}