001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2020 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018//////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.coding; 021 022import java.util.HashSet; 023import java.util.Set; 024 025import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 026import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 027import com.puppycrawl.tools.checkstyle.api.DetailAST; 028import com.puppycrawl.tools.checkstyle.api.FullIdent; 029import com.puppycrawl.tools.checkstyle.api.TokenTypes; 030import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 031 032/** 033 * <p> 034 * Checks that classes which define a covariant {@code equals()} method 035 * also override method {@code equals(Object)}. 036 * </p> 037 * <p> 038 * Covariant {@code equals()} - method that is similar to {@code equals(Object)}, 039 * but with a covariant parameter type (any subtype of Object). 040 * </p> 041 * <p> 042 * <strong>Notice</strong>: the enums are also checked, 043 * even though they cannot override {@code equals(Object)}. 044 * The reason is to point out that implementing {@code equals()} in enums 045 * is considered an awful practice: it may cause having two different enum values 046 * that are equal using covariant enum method, and not equal when compared normally. 047 * </p> 048 * <p> 049 * Inspired by <a href="https://www.cs.jhu.edu/~daveho/pubs/oopsla2004.pdf"> 050 * Finding Bugs is Easy, chapter '4.5 Bad Covariant Definition of Equals (Eq)'</a>: 051 * </p> 052 * <p> 053 * Java classes may override the {@code equals(Object)} method to define 054 * a predicate for object equality. This method is used by many of the Java 055 * runtime library classes; for example, to implement generic containers. 056 * </p> 057 * <p> 058 * Programmers sometimes mistakenly use the type of their class {@code Foo} 059 * as the type of the parameter to {@code equals()}: 060 * </p> 061 * <pre> 062 * public boolean equals(Foo obj) {...} 063 * </pre> 064 * <p> 065 * This covariant version of {@code equals()} does not override the version in 066 * the {@code Object} class, and it may lead to unexpected behavior at runtime, 067 * especially if the class is used with one of the standard collection classes 068 * which expect that the standard {@code equals(Object)} method is overridden. 069 * </p> 070 * <p> 071 * This kind of bug is not obvious because it looks correct, and in circumstances 072 * where the class is accessed through the references of the class type (rather 073 * than a supertype), it will work correctly. However, the first time it is used 074 * in a container, the behavior might be mysterious. For these reasons, this type 075 * of bug can elude testing and code inspections. 076 * </p> 077 * <p> 078 * To configure the check: 079 * </p> 080 * <pre> 081 * <module name="CovariantEquals"/> 082 * </pre> 083 * <p> 084 * For example: 085 * </p> 086 * <pre> 087 * class Test { 088 * public boolean equals(Test i) { // violation 089 * return false; 090 * } 091 * } 092 * </pre> 093 * <p> 094 * The same class without violations: 095 * </p> 096 * <pre> 097 * class Test { 098 * public boolean equals(Test i) { // no violation 099 * return false; 100 * } 101 * 102 * public boolean equals(Object i) { 103 * return false; 104 * } 105 * } 106 * </pre> 107 * <p> 108 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 109 * </p> 110 * <p> 111 * Violation Message Keys: 112 * </p> 113 * <ul> 114 * <li> 115 * {@code covariant.equals} 116 * </li> 117 * </ul> 118 * 119 * @since 3.2 120 */ 121@FileStatefulCheck 122public class CovariantEqualsCheck extends AbstractCheck { 123 124 /** 125 * A key is pointing to the warning message text in "messages.properties" 126 * file. 127 */ 128 public static final String MSG_KEY = "covariant.equals"; 129 130 /** Set of equals method definitions. */ 131 private final Set<DetailAST> equalsMethods = new HashSet<>(); 132 133 @Override 134 public int[] getDefaultTokens() { 135 return getRequiredTokens(); 136 } 137 138 @Override 139 public int[] getRequiredTokens() { 140 return new int[] {TokenTypes.CLASS_DEF, TokenTypes.LITERAL_NEW, TokenTypes.ENUM_DEF, }; 141 } 142 143 @Override 144 public int[] getAcceptableTokens() { 145 return getRequiredTokens(); 146 } 147 148 @Override 149 public void visitToken(DetailAST ast) { 150 equalsMethods.clear(); 151 152 // examine method definitions for equals methods 153 final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK); 154 if (objBlock != null) { 155 DetailAST child = objBlock.getFirstChild(); 156 boolean hasEqualsObject = false; 157 while (child != null) { 158 if (CheckUtil.isEqualsMethod(child)) { 159 if (isFirstParameterObject(child)) { 160 hasEqualsObject = true; 161 } 162 else { 163 equalsMethods.add(child); 164 } 165 } 166 child = child.getNextSibling(); 167 } 168 169 // report equals method definitions 170 if (!hasEqualsObject) { 171 for (DetailAST equalsAST : equalsMethods) { 172 final DetailAST nameNode = equalsAST 173 .findFirstToken(TokenTypes.IDENT); 174 log(nameNode, MSG_KEY); 175 } 176 } 177 } 178 } 179 180 /** 181 * Tests whether a method's first parameter is an Object. 182 * 183 * @param methodDefAst the method definition AST to test. 184 * Precondition: ast is a TokenTypes.METHOD_DEF node. 185 * @return true if ast has first parameter of type Object. 186 */ 187 private static boolean isFirstParameterObject(DetailAST methodDefAst) { 188 final DetailAST paramsNode = methodDefAst.findFirstToken(TokenTypes.PARAMETERS); 189 190 // parameter type "Object"? 191 final DetailAST paramNode = 192 paramsNode.findFirstToken(TokenTypes.PARAMETER_DEF); 193 final DetailAST typeNode = paramNode.findFirstToken(TokenTypes.TYPE); 194 final FullIdent fullIdent = FullIdent.createFullIdentBelow(typeNode); 195 final String name = fullIdent.getText(); 196 return "Object".equals(name) || "java.lang.Object".equals(name); 197 } 198 199}