/*
* 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.common.collect.Iterables.getOnlyElement;
import static com.google.errorprone.BugPattern.Category.JDK;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.tools.javac.code.Scope.WriteableScope;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.util.Name;
/** @author cushon@google.com (Liam Miller-Cushon) */
@BugPattern(
name = "ReferenceEquality",
summary = "Comparison using reference equality instead of value equality",
category = JDK,
severity = WARNING
)
public class ReferenceEquality extends AbstractReferenceEquality {
@Override
protected boolean matchArgument(ExpressionTree tree, VisitorState state) {
Type type = ASTHelpers.getType(tree);
if (!type.isReference()) {
return false;
}
ClassTree classTree = ASTHelpers.findEnclosingNode(state.getPath(), ClassTree.class);
if (classTree == null) {
return false;
}
Type classType = ASTHelpers.getType(classTree);
if (classType == null) {
return false;
}
if (inEqualsOrCompareTo(classType, type, state)) {
return false;
}
if (ASTHelpers.isSubtype(type, state.getSymtab().enumSym.type, state)) {
return false;
}
if (ASTHelpers.isSubtype(type, state.getSymtab().classType, state)) {
return false;
}
if (!implementsEquals(type, state)) {
return false;
}
return true;
}
private boolean inEqualsOrCompareTo(Type classType, Type type, VisitorState state) {
MethodTree methodTree = ASTHelpers.findEnclosingNode(state.getPath(), MethodTree.class);
if (methodTree == null) {
return false;
}
MethodSymbol sym = ASTHelpers.getSymbol(methodTree);
if (sym == null || sym.isStatic()) {
return false;
}
Symbol compareTo = getOnlyMember(state, state.getSymtab().comparableType, "compareTo");
Symbol equals = getOnlyMember(state, state.getSymtab().objectType, "equals");
if (!sym.overrides(compareTo, classType.tsym, state.getTypes(), false)
&& !sym.overrides(equals, classType.tsym, state.getTypes(), false)) {
return false;
}
if (!ASTHelpers.isSameType(type, classType, state)) {
return false;
}
return true;
}
private static Symbol getOnlyMember(VisitorState state, Type type, String name) {
return getOnlyElement(type.tsym.members().getSymbolsByName(state.getName(name)));
}
/** Check if the method declares or inherits an implementation of .equals() */
public static boolean implementsEquals(Type type, VisitorState state) {
Name equalsName = state.getName("equals");
Symbol objectEquals = getOnlyMember(state, state.getSymtab().objectType, "equals");
for (Type sup : state.getTypes().closure(type)) {
if (sup.tsym.isInterface()) {
continue;
}
if (ASTHelpers.isSameType(sup, state.getSymtab().objectType, state)) {
return false;
}
WriteableScope scope = sup.tsym.members();
if (scope == null) {
continue;
}
for (Symbol sym : scope.getSymbolsByName(equalsName)) {
if (sym.overrides(objectEquals, type.tsym, state.getTypes(), false)) {
return true;
}
}
}
return false;
}
}