package checkers.interning; import java.util.*; import checkers.source.*; import checkers.basetype.*; import checkers.interning.quals.Interned; import checkers.types.*; import checkers.util.*; import com.sun.source.tree.*; import javax.lang.model.element.*; import javax.lang.model.type.*; import static javax.lang.model.util.ElementFilter.*; /** * A type-checking visitor for the {@link Interned} type * qualifier that uses the {@link BaseTypeVisitor} implementation. 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") * <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<Void, Void> { /** The interned annotation. */ private final AnnotationMirror INTERNED; private final DeclaredType typeToCheck; /** * Creates a new visitor for type-checking {@link Interned}. * * @param checker the checker to use * @param root the root of the input program's AST to check */ public InterningVisitor(InterningChecker checker, CompilationUnitTree root) { super(checker, root); this.INTERNED = annoFactory.fromClass(Interned.class); typeToCheck = checker.typeToCheck(); } private boolean shouldCheckFor(ExpressionTree tree) { if (typeToCheck == null) return true; TypeMirror type = InternalUtils.typeOf(tree); return types.isSubtype(type, typeToCheck) || types.isSubtype(typeToCheck, type); } @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(), 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); if (suppressByHeuristic(node)) return super.visitBinary(node, p); if (!(shouldCheckFor(leftOp) && shouldCheckFor(rightOp))) 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.hasAnnotation(INTERNED)) checker.report(Result.failure("not.interned", left), leftOp); if (!right.hasAnnotation(INTERNED)) checker.report(Result.failure("not.interned", right), rightOp); return super.visitBinary(node, p); } @Override public Void visitMethodInvocation(MethodInvocationTree node, Void p) { if (isInvocationOfEquals(node)) { AnnotatedTypeMirror recv = atypeFactory.getReceiver(node); AnnotatedTypeMirror comp = atypeFactory.getAnnotatedType(node.getArguments().get(0)); if (this.checker.getLintOption("dotequals", true) && recv.hasAnnotation(INTERNED) && comp.hasAnnotation(INTERNED)) checker.report(Result.warning("unnecessary.equals"), node); } return super.visitMethodInvocation(node, p); } // ********************************************************************** // Helper methods // ********************************************************************** /** * Tests whether a method invocation is an invocation of * {@link #equals} that overrides or hides {@link Object#equals(Object)}. * <p> * * Returns true even if a method does not override {@link Object.equals}, * 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}. * * @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)}. * * 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: * * 1. 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 * * 2. the method overrides {@link Object#equals(Object)} and the * comparison tests "this" against the method's parameter * * @param node the comparison to check * @return true if one of the supported heuristics is matched, false * otherwise */ // TODO: handle != comparisons too! private boolean suppressByHeuristic(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; // If we're not directly in an if statement in a method (ignoring // parens and blocks), terminate. // TODO: only if it's the first statement in the block if (!Heuristics.matchParents(getCurrentPath(), Tree.Kind.IF, Tree.Kind.METHOD)) return false; ExecutableElement enclosing = TreeUtils.elementFromDeclaration(visitorState.getMethodTree()); assert enclosing != null; Element lhs = TreeUtils.elementFromUse((IdentifierTree)left); Element rhs = TreeUtils.elementFromUse((IdentifierTree)right); // Determine whether or not the "then" statement of the if has a single // "return 0" statement (for the Comparator.compare heuristic). if (overrides(enclosing, Comparator.class, "compare")) { final boolean returnsZero = Heuristics.Matchers.withIn( Heuristics.Matchers.ofKind(Tree.Kind.IF, 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().size() > 0) return visit(tree.getStatements().get(0), p); return false; } @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)); } })).match(getCurrentPath()); if (!returnsZero) return false; assert enclosing.getParameters().size() == 2; Element p1 = enclosing.getParameters().get(0); Element p2 = enclosing.getParameters().get(1); return (p1.equals(lhs) && p2.equals(rhs)) || (p2.equals(lhs) && p1.equals(rhs)); } else if (overrides(enclosing, Object.class, "equals")) { assert enclosing.getParameters().size() == 1; Element param = enclosing.getParameters().get(0); Element thisElt = getThis(trees.getScope(getCurrentPath())); assert thisElt != null; return (thisElt.equals(lhs) && param.equals(rhs)) || (param.equals(lhs) && thisElt.equals(rhs)); } return false; } /** * Determines the element corresponding to "this" inside a scope. Returns * null within static methods. * * @param scope the scope with the * @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 : methodsIn(clazzElt.getEnclosedElements())) if (elt.getSimpleName().contentEquals(method) && elements.overrides(e, elt, clazzElt)) return true; return false; } }