/* * Copyright 2015 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.errorprone.bugpatterns; import static com.google.errorprone.dataflow.nullnesspropagation.Nullness.NONNULL; import static com.google.errorprone.dataflow.nullnesspropagation.Nullness.NULL; import static com.google.errorprone.matchers.Matchers.anyOf; import static com.google.errorprone.matchers.Matchers.instanceMethod; import static com.google.errorprone.matchers.Matchers.staticMethod; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.BinaryTreeMatcher; import com.google.errorprone.dataflow.nullnesspropagation.Nullness; import com.google.errorprone.fixes.Fix; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.Description; import com.google.errorprone.matchers.Matcher; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.BinaryTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.ParenthesizedTree; import com.sun.source.tree.Tree; import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.TreeVisitor; import com.sun.source.util.SimpleTreeVisitor; import com.sun.source.util.TreePath; import java.util.List; import java.util.Optional; /** * Abstract implementation of a BugPattern that detects the use of reference equality to compare * classes with value semantics. * * <p>See e.g. {@link NumericEquality}, {@link OptionalEquality}, * {@link ProtoStringFieldReferenceEquality}, and {@link StringEquality}. * * @author cushon@google.com (Liam Miller-Cushon) */ public abstract class AbstractReferenceEquality extends BugChecker implements BinaryTreeMatcher { private static final Matcher<ExpressionTree> EQUALS_STATIC_METHODS = anyOf( staticMethod().onClass("com.google.common.base.Objects").named("equal"), staticMethod().onClass("java.util.Objects").named("equals")); private static final Matcher<ExpressionTree> OBJECT_INSTANCE_EQUALS = instanceMethod() .onDescendantOf("java.lang.Object") .named("equals") .withParameters("java.lang.Object"); protected abstract boolean matchArgument(ExpressionTree tree, VisitorState state); @Override public final Description matchBinary(BinaryTree tree, VisitorState state) { switch (tree.getKind()) { case EQUAL_TO: case NOT_EQUAL_TO: break; default: return Description.NO_MATCH; } if (tree.getLeftOperand().getKind() == Kind.NULL_LITERAL || !matchArgument(tree.getLeftOperand(), state)) { return Description.NO_MATCH; } if (tree.getRightOperand().getKind() == Kind.NULL_LITERAL || !matchArgument(tree.getRightOperand(), state)) { return Description.NO_MATCH; } Description.Builder builder = buildDescription(tree); addFixes(builder, tree, state); return builder.build(); } protected void addFixes(Description.Builder builder, BinaryTree tree, VisitorState state) { ExpressionTree lhs = tree.getLeftOperand(); ExpressionTree rhs = tree.getRightOperand(); Optional<Fix> fixToReplaceOrStatement = inOrStatementWithEqualsCheck(state, tree); if (fixToReplaceOrStatement.isPresent()) { builder.addFix(fixToReplaceOrStatement.get()); return; } // Swap the order (e.g. rhs.equals(lhs) if the rhs is a non-null constant, and the lhs is not if (ASTHelpers.constValue(lhs) == null && ASTHelpers.constValue(rhs) != null) { ExpressionTree tmp = lhs; lhs = rhs; rhs = tmp; } String prefix = tree.getKind() == Kind.NOT_EQUAL_TO ? "!" : ""; String lhsSource = state.getSourceForNode(lhs); String rhsSource = state.getSourceForNode(rhs); Nullness nullness = getNullness(lhs, state); // If the lhs is possibly-null, provide both options. if (nullness != NONNULL) { builder.addFix( SuggestedFix.builder() .replace( tree, String.format("%sObjects.equals(%s, %s)", prefix, lhsSource, rhsSource)) .addImport("java.util.Objects") .build()); } if (nullness != NULL) { builder.addFix( SuggestedFix.replace( tree, String.format( "%s%s.equals(%s)", prefix, lhs instanceof BinaryTree ? String.format("(%s)", lhsSource) : lhsSource, rhsSource))); } } private static Optional<Fix> inOrStatementWithEqualsCheck(VisitorState state, BinaryTree tree) { // Only attempt to handle a == b || a.equals(b); if (tree.getKind() == Kind.NOT_EQUAL_TO) { return Optional.empty(); } ExpressionTree lhs = tree.getLeftOperand(); ExpressionTree rhs = tree.getRightOperand(); Tree parent = state.getPath().getParentPath().getLeaf(); if (parent.getKind() != Kind.CONDITIONAL_OR) { return Optional.empty(); } BinaryTree p = (BinaryTree) parent; if (p.getLeftOperand() != tree) { // a == b is on the RHS, ignore this construction return Optional.empty(); } // If the other half of this or statement is foo.equals(bar) or Objects.equals(foo, bar) // replace the or statement with the other half as already written. ExpressionTree otherExpression = skipParens(p.getRightOperand()); if (!(otherExpression instanceof MethodInvocationTree)) { return Optional.empty(); } MethodInvocationTree other = (MethodInvocationTree) otherExpression; // a == b || Objects.equals(a, b) => Objects.equals(a, b) if (EQUALS_STATIC_METHODS.matches(otherExpression, state)) { List<? extends ExpressionTree> arguments = other.getArguments(); if (treesMatch(arguments.get(0), arguments.get(1), lhs, rhs)) { return Optional.of(SuggestedFix.replace(parent, state.getSourceForNode(otherExpression))); } } // a == b || a.equals(b) => a.equals(b) if (OBJECT_INSTANCE_EQUALS.matches(otherExpression, state)) { if (treesMatch(ASTHelpers.getReceiver(other), other.getArguments().get(0), lhs, rhs)) { return Optional.of(SuggestedFix.replace(parent, state.getSourceForNode(otherExpression))); } } return Optional.empty(); } private Nullness getNullness(ExpressionTree expr, VisitorState state) { TreePath pathToExpr = new TreePath(state.getPath(), expr); return state.getNullnessAnalysis().getNullness(pathToExpr, state.context); } private static boolean treesMatch( ExpressionTree lhs1, ExpressionTree rhs1, ExpressionTree lhs2, ExpressionTree rhs2) { return (ASTHelpers.sameVariable(lhs1, lhs2) && ASTHelpers.sameVariable(rhs1, rhs2)) || (ASTHelpers.sameVariable(lhs1, rhs2) && ASTHelpers.sameVariable(rhs1, lhs2)); } private static final TreeVisitor<ExpressionTree, Void> SKIP_PARENS = new SimpleTreeVisitor<ExpressionTree, Void>() { @Override protected ExpressionTree defaultAction(Tree node, Void v) { return node instanceof ExpressionTree ? (ExpressionTree) node : null; } @Override public ExpressionTree visitParenthesized(ParenthesizedTree node, Void v) { return node.getExpression().accept(this, null); } }; private static ExpressionTree skipParens(ExpressionTree tree) { return tree.accept(SKIP_PARENS, null); } }