/* * 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.BugPattern.Category.JDK; import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.matchers.Matchers.allOf; import static com.google.errorprone.matchers.Matchers.anyOf; import static com.google.errorprone.matchers.Matchers.instanceMethod; import static com.google.errorprone.matchers.Matchers.isSameType; import static com.google.errorprone.matchers.Matchers.staticMethod; import static com.google.errorprone.matchers.Matchers.toType; import static com.google.errorprone.suppliers.Suppliers.BOOLEAN_TYPE; import com.google.common.base.Predicate; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; import com.google.errorprone.matchers.Description; import com.google.errorprone.matchers.Matcher; import com.google.errorprone.util.ASTHelpers; import com.google.errorprone.util.Signatures; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.MethodSymbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Types; import com.sun.tools.javac.util.Name; import java.util.Set; /** @author avenet@google.com (Arnaud J. Venet) */ @BugPattern( name = "EqualsIncompatibleType", summary = "An equality test between objects with incompatible types always returns false", category = JDK, severity = WARNING ) public class EqualsIncompatibleType extends BugChecker implements MethodInvocationTreeMatcher { private static final Matcher<MethodInvocationTree> STATIC_EQUALS_INVOCATION_MATCHER = anyOf( allOf( staticMethod() .onClass("java.util.Objects") .named("equals") .withParameters("java.lang.Object", "java.lang.Object"), isSameType(BOOLEAN_TYPE)), allOf( staticMethod() .onClass("com.google.common.base.Objects") .named("equal") .withParameters("java.lang.Object", "java.lang.Object"), isSameType(BOOLEAN_TYPE))); private static final Matcher<MethodInvocationTree> INSTANCE_EQUALS_INVOCATION_MATCHER = allOf( instanceMethod().anyClass().named("equals").withParameters("java.lang.Object"), isSameType(BOOLEAN_TYPE)); private static final Matcher<Tree> ASSERT_FALSE_MATCHER = toType( MethodInvocationTree.class, anyOf( instanceMethod().anyClass().named("assertFalse"), staticMethod().anyClass().named("assertFalse"))); @Override public Description matchMethodInvocation( MethodInvocationTree invocationTree, final VisitorState state) { if (!STATIC_EQUALS_INVOCATION_MATCHER.matches(invocationTree, state) && !INSTANCE_EQUALS_INVOCATION_MATCHER.matches(invocationTree, state)) { return Description.NO_MATCH; } // This is the type of the object on which the java.lang.Object.equals() method // is called, either directly or indirectly via a static utility method. In the latter, // it is the type of the first argument to the static method. Type receiverType; // This is the type of the argument to the java.lang.Object.equals() method. // In case a static utility method is used, it is the type of the second argument // to this method. Type argumentType; if (STATIC_EQUALS_INVOCATION_MATCHER.matches(invocationTree, state)) { receiverType = ASTHelpers.getType(invocationTree.getArguments().get(0)); argumentType = ASTHelpers.getType(invocationTree.getArguments().get(1)); } else { receiverType = ASTHelpers.getReceiverType(invocationTree); argumentType = ASTHelpers.getType(invocationTree.getArguments().get(0)); } if (!incompatibleTypes(receiverType, argumentType, state)) { return Description.NO_MATCH; } // Ignore callsites wrapped inside assertFalse: // assertFalse(objOfReceiverType.equals(objOfArgumentType)) if (ASSERT_FALSE_MATCHER.matches(state.getPath().getParentPath().getLeaf(), state)) { return Description.NO_MATCH; } // When we reach this point, we know that the two following facts hold: // (1) The types of the receiver and the argument to the eventual invocation of // java.lang.Object.equals() are incompatible. // (2) No common superclass (other than java.lang.Object) or interface of the receiver and the // argument defines an override of java.lang.Object.equals(). // This equality test almost certainly evaluates to false, which is very unlikely to be the // programmer's intent. Hence, this is reported as an error. There is no sensible fix to suggest // in this situation. return buildDescription(invocationTree) .setMessage(getMessage(invocationTree, receiverType, argumentType)) .build(); } static boolean incompatibleTypes(Type receiverType, Type argumentType, final VisitorState state) { if (receiverType == null || argumentType == null) { return false; } // If one type can be cast into the other, we don't flag the equality test. // Note: we do this precisely in this order to allow primitive values to be checked pre-1.7: // 1.6: java.lang.Object can't be cast to primitives // 1.7: java.lang.Object can be cast to primitives (implicitly through the boxed primitive type) if (ASTHelpers.isCastable(argumentType, receiverType, state)) { return false; } // Otherwise, we explore the superclasses of the receiver type as well as the interfaces it // implements and we collect all overrides of java.lang.Object.equals(). If one of those // overrides is inherited by the argument, then we don't flag the equality test. final Types types = state.getTypes(); Predicate<MethodSymbol> equalsPredicate = new Predicate<MethodSymbol>() { @Override public boolean apply(MethodSymbol methodSymbol) { return !methodSymbol.isStatic() && ((methodSymbol.flags() & Flags.SYNTHETIC) == 0) && types.isSameType(methodSymbol.getReturnType(), state.getSymtab().booleanType) && methodSymbol.getParameters().size() == 1 && types.isSameType( methodSymbol.getParameters().get(0).type, state.getSymtab().objectType); } }; Name equalsName = state.getName("equals"); Set<MethodSymbol> overridesOfEquals = ASTHelpers.findMatchingMethods(equalsName, equalsPredicate, receiverType, types); ClassSymbol argumentClass = (ClassSymbol) argumentType.tsym; for (MethodSymbol method : overridesOfEquals) { ClassSymbol methodClass = method.enclClass(); if (argumentClass.isSubClass(methodClass, types) && !methodClass.equals(state.getSymtab().objectType.tsym) && !methodClass.equals(state.getSymtab().enumSym)) { // The type of the argument shares a superclass // (other then java.lang.Object or java.lang.Enum) or interface // with the receiver that implements an override of java.lang.Object.equals(). return false; } } return true; } private static String getMessage( MethodInvocationTree invocationTree, Type receiverType, Type argumentType) { String receiverTypeString = Signatures.prettyType(receiverType); String argumentTypeString = Signatures.prettyType(argumentType); if (argumentTypeString.equals(receiverTypeString)) { receiverTypeString = receiverType.toString(); argumentTypeString = argumentType.toString(); } return "Calling " + ASTHelpers.getSymbol(invocationTree).getSimpleName() + " on incompatible types " + receiverTypeString + " and " + argumentTypeString; } }