/*
* Copyright 2012 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.enclosingClass;
import static com.google.errorprone.matchers.Matchers.hasMethod;
import static com.google.errorprone.matchers.Matchers.isSameType;
import static com.google.errorprone.matchers.Matchers.isStatic;
import static com.google.errorprone.matchers.Matchers.methodHasParameters;
import static com.google.errorprone.matchers.Matchers.methodHasVisibility;
import static com.google.errorprone.matchers.Matchers.methodIsNamed;
import static com.google.errorprone.matchers.Matchers.methodReturns;
import static com.google.errorprone.matchers.Matchers.not;
import static com.google.errorprone.matchers.Matchers.variableType;
import static com.google.errorprone.suppliers.Suppliers.BOOLEAN_TYPE;
import static com.google.errorprone.suppliers.Suppliers.JAVA_LANG_BOOLEAN_TYPE;
import static com.google.errorprone.suppliers.Suppliers.OBJECT_TYPE;
import static com.sun.tools.javac.code.Flags.ENUM;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.MethodVisibility.Visibility;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;
import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
import com.sun.tools.javac.util.Name;
/** Bug checker for equals methods that don't actually override equals. */
@BugPattern(
name = "NonOverridingEquals",
summary = "equals method doesn't override Object.equals",
category = JDK,
severity = WARNING
)
public class NonOverridingEquals extends BugChecker implements MethodTreeMatcher {
private static final String MESSAGE_BASE = "equals method doesn't override Object.equals";
/**
* Matches any method definition that:
* 1) is named `equals`
* 2) takes a single argument of a type other than Object
* 3) returns a boolean or Boolean
*/
private static final Matcher<MethodTree> MATCHER = allOf(
methodIsNamed("equals"),
methodHasParameters(variableType(not(isSameType("java.lang.Object")))),
anyOf(methodReturns(BOOLEAN_TYPE), methodReturns(JAVA_LANG_BOOLEAN_TYPE)));
/**
* Matches if the enclosing class overrides Object#equals.
*/
private static final Matcher<MethodTree> enclosingClassOverridesEquals =
enclosingClass(hasMethod(allOf(
methodIsNamed("equals"),
methodReturns(BOOLEAN_TYPE),
methodHasParameters(variableType(isSameType(OBJECT_TYPE))),
not(isStatic()))));
/**
* Matches method declarations for which we cannot provide a fix. Our default fix rewrites
* the equals method to override Object.equals. In these (uncommon) cases, our rewrite
* algorithm doesn't work:
* <ul>
* <li>the method is static
* <li>the method is not public
* <li>the method returns a boxed Boolean
* </ul>
*/
private static final Matcher<MethodTree> noFixMatcher = anyOf(
isStatic(),
not(methodHasVisibility(Visibility.PUBLIC)),
methodReturns(JAVA_LANG_BOOLEAN_TYPE));
@Override
public Description matchMethod(MethodTree methodTree, VisitorState state) {
if (!MATCHER.matches(methodTree, state)) {
return Description.NO_MATCH;
}
// If an overriding equals method has already been defined in the enclosing class, assume
// this is a type-specific helper method and give advice to either inline it or rename it.
if (enclosingClassOverridesEquals.matches(methodTree, state)) {
return buildDescription(methodTree)
.setMessage(
MESSAGE_BASE
+ "; if this is a type-specific helper for a method that does"
+ " override Object.equals, either inline it into the callers or rename it to"
+ " avoid ambiguity")
.build();
}
// Don't provide a fix if the method is static, non-public, or returns a boxed Boolean
if (noFixMatcher.matches(methodTree, state)) {
return describeMatch(methodTree);
}
JCClassDecl cls = (JCClassDecl) state.findEnclosing(ClassTree.class);
if ((cls.getModifiers().flags & ENUM) != 0) {
/* If the enclosing class is an enum, then just delete the equals method since enums
* should always be compared for reference equality. Enum defines a final equals method for
* just this reason. */
return buildDescription(methodTree)
.setMessage(MESSAGE_BASE + "; enum instances can safely be compared by reference "
+ "equality, so please delete this")
.addFix(SuggestedFix.delete(methodTree))
.build();
} else {
/* Otherwise, change the covariant equals method to override Object.equals. */
SuggestedFix.Builder fix = SuggestedFix.builder();
// Add @Override annotation if not present.
if (ASTHelpers.getAnnotation(methodTree, Override.class) == null) {
fix.prefixWith(methodTree, "@Override\n");
}
// Change method signature, substituting Object for parameter type.
JCTree parameterType = (JCTree) methodTree.getParameters().get(0).getType();
Name parameterName = ((JCVariableDecl) methodTree.getParameters().get(0)).getName();
fix.replace(parameterType, "Object");
// If there is a method body...
if (methodTree.getBody() != null) {
// Add type check at start
String typeCheckStmt = "if (!(" + parameterName + " instanceof " + parameterType + ")) {\n"
+ " return false;\n"
+ "}\n";
fix.prefixWith(methodTree.getBody().getStatements().get(0), typeCheckStmt);
// Cast all uses of the parameter name using a recursive TreeScanner.
new CastScanner().scan(methodTree.getBody(), new CastState(parameterName,
parameterType.toString(), fix));
}
return describeMatch(methodTree, fix.build());
}
}
private static class CastState {
final Name name;
final String castToType;
final SuggestedFix.Builder fix;
public CastState(Name name, String castToType, SuggestedFix.Builder fix) {
this.name = name;
this.castToType = castToType;
this.fix = fix;
}
}
/**
* A Scanner used to replace all references to a variable with
* a casted version.
*/
private static class CastScanner extends TreeScanner<Void, CastState> {
@Override
public Void visitIdentifier(IdentifierTree node, CastState state) {
if (state.name.equals(node.getName())) {
state.fix.replace(node, "((" + state.castToType + ") " + state.name + ")");
}
return super.visitIdentifier(node, state);
}
}
}