/* * Copyright 2016 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.common.collect.Iterables.getOnlyElement; import static com.google.errorprone.BugPattern.Category.TRUTH; import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; import static com.google.errorprone.matchers.Matchers.allOf; import static com.google.errorprone.matchers.Matchers.anyOf; import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod; import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; 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.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import java.util.regex.Pattern; /** * Points out if an object is tested for equality/inequality to itself using Truth Libraries. * * @author bhagwani@google.com (Sumit Bhagwani) */ @BugPattern( name = "TruthSelfEquals", summary = "isEqualTo should not be used to test an object for equality with itself; the" + " assertion will never fail.", category = TRUTH, severity = ERROR ) public class TruthSelfEquals extends BugChecker implements MethodInvocationTreeMatcher { /** * Matches calls to any Truth method called "isEqualTo"/"isNotEqualTo" with exactly one argument * in which the receiver is the same reference as the argument. * * <p>Examples: * * <ul> * <li> assertThat(a).isEqualTo(a) * <li> assertThat(a).isNotEqualTo(a) * <li> assertThat(a).isNotSameAs(a) * <li> assertThat(a).isSameAs(a) * <li> assertWithMessage(msg).that(a).isEqualTo(a) * <li> assertWithMessage(msg).that(a).isNotEqualTo(a) * <li> assertWithMessage(msg).that(a).isNotSameAs(a) * <li> assertWithMessage(msg).that(a).isSameAs(a) * </ul> */ private static final Pattern EQUALS_SAME = Pattern.compile("(isEqualTo|isSameAs)"); private static final Pattern NOT_EQUALS_NOT_SAME = Pattern.compile("(isNotEqualTo|isNotSameAs)"); private static final Matcher<MethodInvocationTree> EQUALS_MATCHER = allOf( instanceMethod() .onDescendantOf("com.google.common.truth.Subject") .withNameMatching(EQUALS_SAME) .withParameters("java.lang.Object"), receiverSameAsParentsArgument()); private static final Matcher<MethodInvocationTree> NOT_EQUALS_MATCHER = allOf( instanceMethod() .onDescendantOf("com.google.common.truth.Subject") .withNameMatching(NOT_EQUALS_NOT_SAME) .withParameters("java.lang.Object"), receiverSameAsParentsArgument()); private static final Matcher<ExpressionTree> ASSERT_THAT = anyOf( staticMethod().onClass("com.google.common.truth.Truth").named("assertThat"), instanceMethod().onDescendantOf("com.google.common.truth.TestVerb").named("that")); @Override public Description matchMethodInvocation( MethodInvocationTree methodInvocationTree, VisitorState state) { if (methodInvocationTree.getArguments().isEmpty()) { return Description.NO_MATCH; } Description.Builder description = buildDescription(methodInvocationTree); ExpressionTree toReplace = methodInvocationTree.getArguments().get(0); if (EQUALS_MATCHER.matches(methodInvocationTree, state)) { description .setMessage( generateSummary( ASTHelpers.getSymbol(methodInvocationTree).getSimpleName().toString(), "passes")) .addFix(suggestEqualsTesterFix(methodInvocationTree, toReplace)); } else if (NOT_EQUALS_MATCHER.matches(methodInvocationTree, state)) { description.setMessage( generateSummary( ASTHelpers.getSymbol(methodInvocationTree).getSimpleName().toString(), "fails")); } else { return Description.NO_MATCH; } Fix fix = SelfEquals.fieldFix(toReplace, state); if (fix != null) { description.addFix(fix); } return description.build(); } private static String generateSummary(String methodName, String constantOutput) { return "The arguments to the " + methodName + " method are the same object, so it always " + constantOutput + ". Please change the arguments to point to different objects or " + "consider using EqualsTester."; } private static Matcher<? super MethodInvocationTree> receiverSameAsParentsArgument() { return new Matcher<MethodInvocationTree>() { @Override public boolean matches(MethodInvocationTree t, VisitorState state) { ExpressionTree rec = ASTHelpers.getReceiver(t); if (rec == null) { return false; } if (!ASSERT_THAT.matches(rec, state)) { return false; } if (!ASTHelpers.sameVariable( getOnlyElement(((MethodInvocationTree) rec).getArguments()), getOnlyElement(t.getArguments()))) { return false; } return true; } }; } private static Fix suggestEqualsTesterFix( MethodInvocationTree methodInvocationTree, ExpressionTree toReplace) { String equalsTesterSuggest = "new EqualsTester().addEqualityGroup(" + toReplace + ").testEquals()"; return SuggestedFix.builder() .replace(methodInvocationTree, equalsTesterSuggest) .addImport("com.google.common.testing.EqualsTester") .build(); } }