blob: 5a48866f3396e4a84c13bc808d3800fc0e6bacda [file] [log] [blame]
package org.checkerframework.checker.interning;
import com.sun.source.tree.BinaryTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ConditionalExpressionTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.IfTree;
import com.sun.source.tree.LiteralTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.Scope;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.TreePath;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic.Kind;
import org.checkerframework.checker.interning.qual.CompareToMethod;
import org.checkerframework.checker.interning.qual.EqualsMethod;
import org.checkerframework.checker.interning.qual.InternMethod;
import org.checkerframework.checker.interning.qual.Interned;
import org.checkerframework.checker.interning.qual.InternedDistinct;
import org.checkerframework.checker.interning.qual.UsesObjectEquals;
import org.checkerframework.checker.signature.qual.CanonicalName;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.common.basetype.BaseTypeVisitor;
import org.checkerframework.framework.type.AnnotatedTypeFactory.ParameterizedExecutableType;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
import org.checkerframework.framework.util.Heuristics;
import org.checkerframework.javacutil.AnnotationBuilder;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.TreeUtils;
import org.checkerframework.javacutil.TypesUtils;
/**
* Typechecks source code for interning violations. A type is considered interned if its primary
* annotation is {@link Interned} or {@link InternedDistinct}. This visitor reports errors or
* warnings for violations for the following cases:
*
* <ol>
* <li value="1">either argument to a "==" or "!=" comparison is not Interned (error
* "not.interned"). As a special case, the comparison is permitted if either arugment is
* InternedDistinct.
* <li value="2">the receiver and argument for a call to an equals method are both Interned
* (optional warning "unnecessary.equals")
* </ol>
*
* @see BaseTypeVisitor
*/
public final class InterningVisitor extends BaseTypeVisitor<InterningAnnotatedTypeFactory> {
/** The @Interned annotation. */
private final AnnotationMirror INTERNED = AnnotationBuilder.fromClass(elements, Interned.class);
/** The @InternedDistinct annotation. */
private final AnnotationMirror INTERNED_DISTINCT =
AnnotationBuilder.fromClass(elements, InternedDistinct.class);
/**
* The declared type of which the equality tests should be tested, if the user explicitly passed
* one. The user can pass the class name via the {@code -Acheckclass=...} option. Null if no class
* is specified, or the class specified isn't in the classpath.
*/
private final DeclaredType typeToCheck = typeToCheck();
/** The Comparable.compareTo method. */
private final ExecutableElement comparableCompareTo =
TreeUtils.getMethod(
"java.lang.Comparable", "compareTo", 1, checker.getProcessingEnvironment());
/** Create an InterningVisitor. */
public InterningVisitor(BaseTypeChecker checker) {
super(checker);
}
/**
* @return true if interning should be verified for the input expression. By default, all classes
* are checked for interning unless {@code -Acheckclass} is specified.
* @see <a href="https://checkerframework.org/manual/#interning-checks">What the Interning Checker
* checks</a>
*/
private boolean shouldCheckExpression(ExpressionTree tree) {
if (typeToCheck == null) {
return true;
}
TypeMirror type = TreeUtils.typeOf(tree);
return types.isSubtype(type, typeToCheck) || types.isSubtype(typeToCheck, type);
}
/** Checks comparison operators, == and !=, for INTERNING violations. */
@Override
public Void visitBinary(BinaryTree node, Void p) {
// No checking unless the operator is "==" or "!=".
if (!(node.getKind() == Tree.Kind.EQUAL_TO || node.getKind() == Tree.Kind.NOT_EQUAL_TO)) {
return super.visitBinary(node, p);
}
ExpressionTree leftOp = node.getLeftOperand();
ExpressionTree rightOp = node.getRightOperand();
// Check passes if either arg is null.
if (leftOp.getKind() == Tree.Kind.NULL_LITERAL || rightOp.getKind() == Tree.Kind.NULL_LITERAL) {
return super.visitBinary(node, p);
}
AnnotatedTypeMirror left = atypeFactory.getAnnotatedType(leftOp);
AnnotatedTypeMirror right = atypeFactory.getAnnotatedType(rightOp);
// If either argument is a primitive, check passes due to auto-unboxing
if (left.getKind().isPrimitive() || right.getKind().isPrimitive()) {
return super.visitBinary(node, p);
}
if (left.hasEffectiveAnnotation(INTERNED_DISTINCT)
|| right.hasEffectiveAnnotation(INTERNED_DISTINCT)) {
return super.visitBinary(node, p);
}
// If shouldCheckExpression returns true for either the LHS or RHS,
// this method proceeds with the interning check.
// Justification: Consider the following scenario:
// interface I { ... }
// class A { ... }
// class B extends A implements I { ... }
// ...
// I i;
// A a;
// ...
// if (a == i) { ... }
// The Java compiler does not issue a compilation error for the (a == i) comparison because,
// even though A does not implement I, 'a' could be assigned an instance of B, and B does
// implement I (note that the compiler does not need to know about the existence of B
// in order to assume this).
// Now suppose the user passes -AcheckClass=A on the command-line.
// I is not a subtype or supertype of A, so shouldCheckExpression will not return true for I.
// But the interning check must be performed, given the argument above. Therefore if
// shouldCheckExpression returns true for either the LHS or the RHS, this method proceeds
// with the interning check.
if (!shouldCheckExpression(leftOp) && !shouldCheckExpression(rightOp)) {
return super.visitBinary(node, p);
}
// Syntactic checks for legal uses of ==
if (suppressInsideComparison(node)) {
return super.visitBinary(node, p);
}
if (suppressEarlyEquals(node)) {
return super.visitBinary(node, p);
}
if (suppressEarlyCompareTo(node)) {
return super.visitBinary(node, p);
}
if (suppressEqualsIfClassIsAnnotated(left, right)) {
return super.visitBinary(node, p);
}
Element leftElt = TypesUtils.getTypeElement(left.getUnderlyingType());
// If neither @Interned or @UsesObjectEquals, report error.
if (!(left.hasEffectiveAnnotation(INTERNED)
|| (leftElt != null
&& atypeFactory.getDeclAnnotation(leftElt, UsesObjectEquals.class) != null))) {
checker.reportError(leftOp, "not.interned", left);
}
Element rightElt = TypesUtils.getTypeElement(right.getUnderlyingType());
if (!(right.hasEffectiveAnnotation(INTERNED)
|| (rightElt != null
&& atypeFactory.getDeclAnnotation(rightElt, UsesObjectEquals.class) != null))) {
checker.reportError(rightOp, "not.interned", right);
}
return super.visitBinary(node, p);
}
/**
* If lint option "dotequals" is specified, warn if the .equals method is used where reference
* equality is safe.
*/
@Override
public Void visitMethodInvocation(MethodInvocationTree node, Void p) {
if (isInvocationOfEquals(node)) {
AnnotatedTypeMirror receiverType = atypeFactory.getReceiverType(node);
AnnotatedTypeMirror comp = atypeFactory.getAnnotatedType(node.getArguments().get(0));
if (this.checker.getLintOption("dotequals", true)
&& receiverType.hasEffectiveAnnotation(INTERNED)
&& comp.hasEffectiveAnnotation(INTERNED)) {
checker.reportWarning(node, "unnecessary.equals");
}
}
return super.visitMethodInvocation(node, p);
}
// Ensure that method annotations are not written on methods they don't apply to.
@Override
public Void visitMethod(MethodTree node, Void p) {
ExecutableElement methElt = TreeUtils.elementFromDeclaration(node);
boolean hasCompareToMethodAnno =
atypeFactory.getDeclAnnotation(methElt, CompareToMethod.class) != null;
boolean hasEqualsMethodAnno =
atypeFactory.getDeclAnnotation(methElt, EqualsMethod.class) != null;
boolean hasInternMethodAnno =
atypeFactory.getDeclAnnotation(methElt, InternMethod.class) != null;
int params = methElt.getParameters().size();
if (hasCompareToMethodAnno && !(params == 1 || params == 2)) {
checker.reportError(
node, "invalid.method.annotation", "@CompareToMethod", "1 or 2", methElt, params);
} else if (hasEqualsMethodAnno && !(params == 1 || params == 2)) {
checker.reportError(
node, "invalid.method.annotation", "@EqualsMethod", "1 or 2", methElt, params);
} else if (hasInternMethodAnno && !(params == 0)) {
checker.reportError(node, "invalid.method.annotation", "@InternMethod", "0", methElt, params);
}
return super.visitMethod(node, p);
}
/**
* Method to implement the @UsesObjectEquals functionality. If a class is annotated
* with @UsesObjectEquals, it must:
*
* <ul>
* <li>not override .equals(Object) and be a subclass of a class annotated
* with @UsesObjectEquals, or
* <li>override equals(Object) with body "this == arg"
* </ul>
*
* If a class is not annotated with @UsesObjectEquals, it must:
*
* <ul>
* <li>not have a superclass annotated with @UsesObjectEquals
* </ul>
*
* @see
* org.checkerframework.common.basetype.BaseTypeVisitor#visitClass(com.sun.source.tree.ClassTree,
* java.lang.Object)
*/
@Override
public void processClassTree(ClassTree classTree) {
TypeElement elt = TreeUtils.elementFromDeclaration(classTree);
AnnotationMirror annotation = atypeFactory.getDeclAnnotation(elt, UsesObjectEquals.class);
// If @UsesObjectEquals is present, check to make sure the class does not override equals
// and its supertype is Object or is annotated with @UsesObjectEquals.
if (annotation != null) {
MethodTree equalsMethod = equalsImplementation(classTree);
if (equalsMethod != null) {
if (!isReferenceEqualityImplementation(equalsMethod)) {
checker.reportError(classTree, "overrides.equals");
}
} else {
// Does not override equals()
TypeMirror superClass = elt.getSuperclass();
if (superClass != null
// The super class of an interface is "none" rather than null.
&& superClass.getKind() == TypeKind.DECLARED) {
TypeElement superClassElement = TypesUtils.getTypeElement(superClass);
if (superClassElement != null
&& !ElementUtils.isObject(superClassElement)
&& atypeFactory.getDeclAnnotation(superClassElement, UsesObjectEquals.class)
== null) {
checker.reportError(classTree, "superclass.notannotated");
}
}
}
}
super.processClassTree(classTree);
}
/**
* Returns true if the given equals() method implements reference equality.
*
* @param equalsMethod an overriding implementation of Object.equals()
* @return true if the given equals() method implements reference equality
*/
private boolean isReferenceEqualityImplementation(MethodTree equalsMethod) {
BlockTree body = equalsMethod.getBody();
List<? extends StatementTree> bodyStatements = body.getStatements();
if (bodyStatements.size() == 1) {
StatementTree bodyStatement = bodyStatements.get(0);
if (bodyStatement.getKind() == Tree.Kind.RETURN) {
ExpressionTree returnExpr =
TreeUtils.withoutParens(((ReturnTree) bodyStatement).getExpression());
if (returnExpr.getKind() == Tree.Kind.EQUAL_TO) {
BinaryTree bt = (BinaryTree) returnExpr;
ExpressionTree lhsTree = bt.getLeftOperand();
ExpressionTree rhsTree = bt.getRightOperand();
if (lhsTree.getKind() == Tree.Kind.IDENTIFIER
&& rhsTree.getKind() == Tree.Kind.IDENTIFIER) {
Name leftName = ((IdentifierTree) lhsTree).getName();
Name rightName = ((IdentifierTree) rhsTree).getName();
Name paramName = equalsMethod.getParameters().get(0).getName();
if ((leftName.contentEquals("this") && rightName == paramName)
|| (leftName == paramName && rightName.contentEquals("this"))) {
return true;
}
}
}
}
}
return false;
}
@Override
protected void checkConstructorResult(
AnnotatedExecutableType constructorType, ExecutableElement constructorElement) {
if (constructorElement.getEnclosingElement().getKind() == ElementKind.ENUM) {
// Enums constructor are only called once per enum constant.
return;
}
super.checkConstructorResult(constructorType, constructorElement);
}
@Override
public boolean validateTypeOf(Tree tree) {
// Don't check the result type of a constructor, because it must be @UnknownInterned, even
// if the type on the class declaration is @Interned.
if (tree.getKind() == Tree.Kind.METHOD && TreeUtils.isConstructor((MethodTree) tree)) {
return true;
} else if (tree.getKind() == Tree.Kind.NEW_CLASS) {
NewClassTree newClassTree = (NewClassTree) tree;
TypeMirror typeMirror = TreeUtils.typeOf(newClassTree);
Set<AnnotationMirror> bounds = atypeFactory.getTypeDeclarationBounds(typeMirror);
// Don't issue an invalid type warning for creations of objects of interned classes;
// instead, issue an interned.object.creation if required.
if (atypeFactory.containsSameByClass(bounds, Interned.class)) {
ParameterizedExecutableType fromUse = atypeFactory.constructorFromUse(newClassTree);
AnnotatedExecutableType constructor = fromUse.executableType;
if (!checkCreationOfInternedObject(newClassTree, constructor)) {
return false;
}
}
}
return super.validateTypeOf(tree);
}
/**
* Issue an error if {@code newInternedObject} is not immediately interned.
*
* @param newInternedObject call to a constructor of an interned class
* @param constructor declared type of the constructor
* @return false unless {@code newInternedObject} is immediately interned
*/
private boolean checkCreationOfInternedObject(
NewClassTree newInternedObject, AnnotatedExecutableType constructor) {
if (constructor.getReturnType().hasAnnotation(Interned.class)) {
return true;
}
TreePath path = getCurrentPath();
if (path != null) {
TreePath parentPath = path.getParentPath();
while (parentPath != null && parentPath.getLeaf().getKind() == Tree.Kind.PARENTHESIZED) {
parentPath = parentPath.getParentPath();
}
if (parentPath != null && parentPath.getParentPath() != null) {
Tree parent = parentPath.getParentPath().getLeaf();
if (parent.getKind() == Tree.Kind.METHOD_INVOCATION) {
// Allow new MyInternType().intern(), where "intern" is any method marked @InternMethod.
ExecutableElement elt = TreeUtils.elementFromUse((MethodInvocationTree) parent);
if (atypeFactory.getDeclAnnotation(elt, InternMethod.class) != null) {
return true;
}
}
}
}
checker.reportError(newInternedObject, "interned.object.creation");
return false;
}
// **********************************************************************
// Helper methods
// **********************************************************************
/**
* Returns the method that overrides Object.equals, or null.
*
* @param node a class
* @return the class's implementation of equals, or null
*/
private MethodTree equalsImplementation(ClassTree node) {
List<? extends Tree> members = node.getMembers();
for (Tree member : members) {
if (member instanceof MethodTree) {
MethodTree mTree = (MethodTree) member;
ExecutableElement enclosing = TreeUtils.elementFromDeclaration(mTree);
if (overrides(enclosing, Object.class, "equals")) {
return mTree;
}
}
}
return null;
}
/**
* Tests whether a method invocation is an invocation of {@link #equals} with one argument.
*
* <p>Returns true even if a method overloads {@link Object#equals(Object)}, because of the common
* idiom of writing an equals method with a non-Object parameter, in addition to the equals method
* that overrides {@link Object#equals(Object)}.
*
* @param node a method invocation node
* @return true iff {@code node} is a invocation of {@code equals()}
*/
private boolean isInvocationOfEquals(MethodInvocationTree node) {
ExecutableElement method = TreeUtils.elementFromUse(node);
return (method.getParameters().size() == 1
&& method.getReturnType().getKind() == TypeKind.BOOLEAN
// method symbols only have simple names
&& method.getSimpleName().contentEquals("equals"));
}
/**
* Pattern matches particular comparisons to avoid common false positives in the {@link
* Comparable#compareTo(Object)} and {@link Object#equals(Object)}.
*
* <p>Specifically, this method tests if: the comparison is a == comparison, it is the test of an
* if statement that's the first statement in the method, and one of the following is true:
*
* <ol>
* <li>the method overrides {@link Comparator#compare}, the "then" branch of the if statement
* returns zero, and the comparison tests equality of the method's two parameters
* <li>the method overrides {@link Object#equals(Object)} and the comparison tests "this"
* against the method's parameter
* <li>the method overrides {@link Comparable#compareTo(Object)}, the "then" branch of the if
* statement returns zero, and the comparison tests "this" against the method's parameter
* </ol>
*
* @param node the comparison to check
* @return true if one of the supported heuristics is matched, false otherwise
*/
// TODO: handle != comparisons too!
// TODO: handle more methods, such as early return from addAll when this == arg
private boolean suppressInsideComparison(final BinaryTree node) {
// Only handle == binary trees
if (node.getKind() != Tree.Kind.EQUAL_TO) {
return false;
}
Tree left = node.getLeftOperand();
Tree right = node.getRightOperand();
// Only valid if we're comparing identifiers.
if (!(left.getKind() == Tree.Kind.IDENTIFIER && right.getKind() == Tree.Kind.IDENTIFIER)) {
return false;
}
TreePath path = getCurrentPath();
TreePath parentPath = path.getParentPath();
Tree parent = parentPath.getLeaf();
// Ensure the == is in a return or in an if, and that enclosing statement is the first
// statement in the method.
if (parent.getKind() == Tree.Kind.RETURN) {
// ensure the return statement is the first statement in the method
if (parentPath.getParentPath().getParentPath().getLeaf().getKind() != Tree.Kind.METHOD) {
return false;
}
// maybe set some variables??
} else if (Heuristics.matchParents(getCurrentPath(), Tree.Kind.IF, Tree.Kind.METHOD)) {
// Ensure the if statement is the first statement in the method
// Retrieve the enclosing if statement tree and method tree
Tree ifStatementTree = null;
MethodTree methodTree = null;
// Set ifStatementTree and methodTree
{
TreePath ppath = parentPath;
Tree tree;
while ((tree = ppath.getLeaf()) != null) {
if (tree.getKind() == Tree.Kind.IF) {
ifStatementTree = tree;
} else if (tree.getKind() == Tree.Kind.METHOD) {
methodTree = (MethodTree) tree;
break;
}
ppath = ppath.getParentPath();
}
}
assert ifStatementTree != null;
assert methodTree != null;
StatementTree firstStmnt = methodTree.getBody().getStatements().get(0);
assert firstStmnt != null;
@SuppressWarnings("interning:not.interned") // comparing AST nodes
boolean notSameNode = firstStmnt != ifStatementTree;
if (notSameNode) {
return false; // The if statement is not the first statement in the method.
}
} else {
return false;
}
ExecutableElement enclosingMethod =
TreeUtils.elementFromDeclaration(visitorState.getMethodTree());
assert enclosingMethod != null;
final Element lhs = TreeUtils.elementFromUse((IdentifierTree) left);
final Element rhs = TreeUtils.elementFromUse((IdentifierTree) right);
// Matcher to check for if statement that returns zero
Heuristics.Matcher matcherIfReturnsZero =
new Heuristics.Matcher() {
@Override
public Boolean visitIf(IfTree tree, Void p) {
return visit(tree.getThenStatement(), p);
}
@Override
public Boolean visitBlock(BlockTree tree, Void p) {
if (tree.getStatements().isEmpty()) {
return false;
}
return visit(tree.getStatements().get(0), p);
}
@Override
public Boolean visitReturn(ReturnTree tree, Void p) {
ExpressionTree expr = tree.getExpression();
return (expr != null
&& expr.getKind() == Tree.Kind.INT_LITERAL
&& ((LiteralTree) expr).getValue().equals(0));
}
};
boolean hasCompareToMethodAnno =
atypeFactory.getDeclAnnotation(enclosingMethod, CompareToMethod.class) != null;
boolean hasEqualsMethodAnno =
atypeFactory.getDeclAnnotation(enclosingMethod, EqualsMethod.class) != null;
int params = enclosingMethod.getParameters().size();
// Determine whether or not the "then" statement of the if has a single
// "return 0" statement (for the Comparator.compare heuristic).
if (overrides(enclosingMethod, Comparator.class, "compare")
|| (hasCompareToMethodAnno && params == 2)) {
final boolean returnsZero =
new Heuristics.Within(new Heuristics.OfKind(Tree.Kind.IF, matcherIfReturnsZero))
.match(getCurrentPath());
if (!returnsZero) {
return false;
}
assert params == 2;
Element p1 = enclosingMethod.getParameters().get(0);
Element p2 = enclosingMethod.getParameters().get(1);
return (p1.equals(lhs) && p2.equals(rhs)) || (p1.equals(rhs) && p2.equals(lhs));
} else if (overrides(enclosingMethod, Object.class, "equals")
|| (hasEqualsMethodAnno && params == 1)) {
assert params == 1;
Element param = enclosingMethod.getParameters().get(0);
Element thisElt = getThis(trees.getScope(getCurrentPath()));
assert thisElt != null;
return (thisElt.equals(lhs) && param.equals(rhs))
|| (thisElt.equals(rhs) && param.equals(lhs));
} else if (hasEqualsMethodAnno && params == 2) {
Element p1 = enclosingMethod.getParameters().get(0);
Element p2 = enclosingMethod.getParameters().get(1);
return (p1.equals(lhs) && p2.equals(rhs)) || (p1.equals(rhs) && p2.equals(lhs));
} else if (overrides(enclosingMethod, Comparable.class, "compareTo")
|| (hasCompareToMethodAnno && params == 1)) {
final boolean returnsZero =
new Heuristics.Within(new Heuristics.OfKind(Tree.Kind.IF, matcherIfReturnsZero))
.match(getCurrentPath());
if (!returnsZero) {
return false;
}
assert params == 1;
Element param = enclosingMethod.getParameters().get(0);
Element thisElt = getThis(trees.getScope(getCurrentPath()));
assert thisElt != null;
return (thisElt.equals(lhs) && param.equals(rhs))
|| (thisElt.equals(rhs) && param.equals(lhs));
}
return false;
}
/**
* Pattern matches to prevent false positives of the forms:
*
* <pre>{@code
* (a == b) || a.equals(b)
* (a == b) || (a != null ? a.equals(b) : false)
* (a == b) || (a != null && a.equals(b))
* }</pre>
*
* Returns true iff the given node fits this pattern.
*
* @return true iff the node fits a pattern such as (a == b || a.equals(b))
*/
private boolean suppressEarlyEquals(final BinaryTree node) {
// Only handle == binary trees
if (node.getKind() != Tree.Kind.EQUAL_TO) {
return false;
}
// should strip parens
final ExpressionTree left = TreeUtils.withoutParens(node.getLeftOperand());
final ExpressionTree right = TreeUtils.withoutParens(node.getRightOperand());
// looking for ((a == b || a.equals(b))
Heuristics.Matcher matcherEqOrEquals =
new Heuristics.Matcher() {
/** Returns true if e is either "e1 != null" or "e2 != null". */
private boolean isNeqNull(ExpressionTree e, ExpressionTree e1, ExpressionTree e2) {
e = TreeUtils.withoutParens(e);
if (e.getKind() != Tree.Kind.NOT_EQUAL_TO) {
return false;
}
ExpressionTree neqLeft = ((BinaryTree) e).getLeftOperand();
ExpressionTree neqRight = ((BinaryTree) e).getRightOperand();
return (((TreeUtils.sameTree(neqLeft, e1) || TreeUtils.sameTree(neqLeft, e2))
&& neqRight.getKind() == Tree.Kind.NULL_LITERAL)
// also check for "null != e1" and "null != e2"
|| ((TreeUtils.sameTree(neqRight, e1) || TreeUtils.sameTree(neqRight, e2))
&& neqLeft.getKind() == Tree.Kind.NULL_LITERAL));
}
@Override
public Boolean visitBinary(BinaryTree tree, Void p) {
ExpressionTree leftTree = tree.getLeftOperand();
ExpressionTree rightTree = tree.getRightOperand();
if (tree.getKind() == Tree.Kind.CONDITIONAL_OR) {
if (TreeUtils.sameTree(leftTree, node)) {
// left is "a==b"
// check right, which should be a.equals(b) or b.equals(a) or similar
return visit(rightTree, p);
} else {
return false;
}
}
if (tree.getKind() == Tree.Kind.CONDITIONAL_AND) {
// looking for: (a != null && a.equals(b)))
if (isNeqNull(leftTree, left, right)) {
return visit(rightTree, p);
}
return false;
}
return false;
}
@Override
public Boolean visitConditionalExpression(ConditionalExpressionTree tree, Void p) {
// looking for: (a != null ? a.equals(b) : false)
ExpressionTree cond = tree.getCondition();
ExpressionTree trueExp = tree.getTrueExpression();
ExpressionTree falseExp = tree.getFalseExpression();
if (isNeqNull(cond, left, right)
&& (falseExp.getKind() == Tree.Kind.BOOLEAN_LITERAL)
&& ((LiteralTree) falseExp).getValue().equals(false)) {
return visit(trueExp, p);
}
return false;
}
@Override
public Boolean visitMethodInvocation(MethodInvocationTree tree, Void p) {
if (!isInvocationOfEquals(tree)) {
return false;
}
List<? extends ExpressionTree> args = tree.getArguments();
if (args.size() != 1) {
return false;
}
ExpressionTree arg = args.get(0);
// if (arg.getKind() != Tree.Kind.IDENTIFIER) {
// return false;
// }
// Element argElt = TreeUtils.elementFromUse((IdentifierTree) arg);
ExpressionTree exp = tree.getMethodSelect();
if (exp.getKind() != Tree.Kind.MEMBER_SELECT) {
return false;
}
MemberSelectTree member = (MemberSelectTree) exp;
ExpressionTree receiver = member.getExpression();
// Element refElt = TreeUtils.elementFromUse(receiver);
// if (!((refElt.equals(lhs) && argElt.equals(rhs)) ||
// ((refElt.equals(rhs) && argElt.equals(lhs))))) {
// return false;
// }
if (TreeUtils.sameTree(receiver, left) && TreeUtils.sameTree(arg, right)) {
return true;
}
if (TreeUtils.sameTree(receiver, right) && TreeUtils.sameTree(arg, left)) {
return true;
}
return false;
}
};
boolean okay =
new Heuristics.Within(new Heuristics.OfKind(Tree.Kind.CONDITIONAL_OR, matcherEqOrEquals))
.match(getCurrentPath());
return okay;
}
/**
* Pattern matches to prevent false positives of the form {@code (a == b || a.compareTo(b) == 0)}.
* Returns true iff the given node fits this pattern.
*
* @return true iff the node fits the pattern (a == b || a.compareTo(b) == 0)
*/
private boolean suppressEarlyCompareTo(final BinaryTree node) {
// Only handle == binary trees
if (node.getKind() != Tree.Kind.EQUAL_TO) {
return false;
}
Tree left = TreeUtils.withoutParens(node.getLeftOperand());
Tree right = TreeUtils.withoutParens(node.getRightOperand());
// Only valid if we're comparing identifiers.
if (!(left.getKind() == Tree.Kind.IDENTIFIER && right.getKind() == Tree.Kind.IDENTIFIER)) {
return false;
}
final Element lhs = TreeUtils.elementFromUse((IdentifierTree) left);
final Element rhs = TreeUtils.elementFromUse((IdentifierTree) right);
// looking for ((a == b || a.compareTo(b) == 0)
Heuristics.Matcher matcherEqOrCompareTo =
new Heuristics.Matcher() {
@Override
public Boolean visitBinary(BinaryTree tree, Void p) {
if (tree.getKind() == Tree.Kind.EQUAL_TO) { // a.compareTo(b) == 0
ExpressionTree leftTree = tree.getLeftOperand(); // looking for a.compareTo(b) or
// b.compareTo(a)
ExpressionTree rightTree = tree.getRightOperand(); // looking for 0
if (rightTree.getKind() != Tree.Kind.INT_LITERAL) {
return false;
}
LiteralTree rightLiteral = (LiteralTree) rightTree;
if (!rightLiteral.getValue().equals(0)) {
return false;
}
return visit(leftTree, p);
} else {
// a == b || a.compareTo(b) == 0
@SuppressWarnings("interning:assignment" // AST node comparisons
)
@InternedDistinct ExpressionTree leftTree = tree.getLeftOperand(); // looking for a==b
ExpressionTree rightTree = tree.getRightOperand(); // looking for a.compareTo(b) == 0
// or b.compareTo(a) == 0
if (leftTree != node) {
return false;
}
if (rightTree.getKind() != Tree.Kind.EQUAL_TO) {
return false;
}
return visit(rightTree, p);
}
}
@Override
public Boolean visitMethodInvocation(MethodInvocationTree tree, Void p) {
if (!TreeUtils.isMethodInvocation(
tree, comparableCompareTo, checker.getProcessingEnvironment())) {
return false;
}
List<? extends ExpressionTree> args = tree.getArguments();
if (args.size() != 1) {
return false;
}
ExpressionTree arg = args.get(0);
if (arg.getKind() != Tree.Kind.IDENTIFIER) {
return false;
}
Element argElt = TreeUtils.elementFromUse(arg);
ExpressionTree exp = tree.getMethodSelect();
if (exp.getKind() != Tree.Kind.MEMBER_SELECT) {
return false;
}
MemberSelectTree member = (MemberSelectTree) exp;
if (member.getExpression().getKind() != Tree.Kind.IDENTIFIER) {
return false;
}
Element refElt = TreeUtils.elementFromUse(member.getExpression());
if (!((refElt.equals(lhs) && argElt.equals(rhs))
|| (refElt.equals(rhs) && argElt.equals(lhs)))) {
return false;
}
return true;
}
};
boolean okay =
new Heuristics.Within(new Heuristics.OfKind(Tree.Kind.CONDITIONAL_OR, matcherEqOrCompareTo))
.match(getCurrentPath());
return okay;
}
/**
* Given {@code a == b}, where a has type A and b has type B, don't issue a warning when either
* the declaration of A or that of B is annotated with @Interned because {@code a == b} will be
* true only if a's run-time type is B (or lower), in which case a is actually interned.
*/
private boolean suppressEqualsIfClassIsAnnotated(
AnnotatedTypeMirror left, AnnotatedTypeMirror right) {
// It would be better to just test their greatest lower bound.
// That could permit some comparisons that this forbids.
return classIsAnnotated(left) || classIsAnnotated(right);
}
/** Returns true if the type's declaration has an @Interned annotation. */
private boolean classIsAnnotated(AnnotatedTypeMirror type) {
TypeMirror tm = type.getUnderlyingType();
if (tm == null) {
// Maybe a type variable or wildcard had no upper bound
return false;
}
tm = TypesUtils.findConcreteUpperBound(tm);
if (tm == null || tm.getKind() == TypeKind.ARRAY) {
// Bound of a wildcard might be null
return false;
}
if (tm.getKind() != TypeKind.DECLARED) {
checker.message(
Kind.WARNING, "InterningVisitor.classIsAnnotated: tm = %s (%s)", tm, tm.getClass());
}
Element classElt = ((DeclaredType) tm).asElement();
if (classElt == null) {
checker.message(
Kind.WARNING,
"InterningVisitor.classIsAnnotated: classElt = null for tm = %s (%s)",
tm,
tm.getClass());
}
if (classElt != null) {
Set<AnnotationMirror> bound = atypeFactory.getTypeDeclarationBounds(tm);
return atypeFactory.containsSameByClass(bound, Interned.class);
}
return false;
}
/**
* Determines the element corresponding to "this" inside a scope. Returns null within static
* methods.
*
* @param scope the scope to search for the element corresponding to "this" in
* @return the element corresponding to "this" in the given scope, or null if not found
*/
private Element getThis(Scope scope) {
for (Element e : scope.getLocalElements()) {
if (e.getSimpleName().contentEquals("this")) {
return e;
}
}
return null;
}
/**
* Determines whether or not the given element overrides the named method in the named class.
*
* @param e an element for a method
* @param clazz the class
* @param method the name of a method
* @return true if the method given by {@code e} overrides the named method in the named class;
* false otherwise
*/
private boolean overrides(ExecutableElement e, Class<?> clazz, String method) {
// Get the element named by "clazz".
TypeElement clazzElt = elements.getTypeElement(clazz.getCanonicalName());
assert clazzElt != null;
// Check all of the methods in the class for name matches and overriding.
for (ExecutableElement elt : ElementFilter.methodsIn(clazzElt.getEnclosedElements())) {
if (elt.getSimpleName().contentEquals(method) && elements.overrides(e, elt, clazzElt)) {
return true;
}
}
return false;
}
/**
* Returns the type to check.
*
* @return the type to check
*/
DeclaredType typeToCheck() {
@SuppressWarnings("signature:assignment") // user input
@CanonicalName String className = checker.getOption("checkclass");
if (className == null) {
return null;
}
TypeElement classElt = elements.getTypeElement(className);
if (classElt == null) {
return null;
}
return types.getDeclaredType(classElt);
}
@Override
protected boolean isTypeCastSafe(AnnotatedTypeMirror castType, AnnotatedTypeMirror exprType) {
if (castType.getKind().isPrimitive()) {
return true;
}
return super.isTypeCastSafe(castType, exprType);
}
}