1 //////////////////////////////////////////////////////////////////////////////// 2 // checkstyle: Checks Java source code for adherence to a set of rules. 3 // Copyright (C) 2001-2019 the original author or authors. 4 // 5 // This library is free software; you can redistribute it and/or 6 // modify it under the terms of the GNU Lesser General Public 7 // License as published by the Free Software Foundation; either 8 // version 2.1 of the License, or (at your option) any later version. 9 // 10 // This library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 // Lesser General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public 16 // License along with this library; if not, write to the Free Software 17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 //////////////////////////////////////////////////////////////////////////////// 19 20 package com.puppycrawl.tools.checkstyle.checks.coding; 21 22 import java.util.HashSet; 23 import java.util.Set; 24 25 import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 26 import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 27 import com.puppycrawl.tools.checkstyle.api.DetailAST; 28 import com.puppycrawl.tools.checkstyle.api.FullIdent; 29 import com.puppycrawl.tools.checkstyle.api.TokenTypes; 30 import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 31 32 /** 33 * <p> 34 * Checks that classes which define a covariant {@code equals()} method 35 * also override method {@code equals(Object)}. 36 * </p> 37 * <p> 38 * Covariant {@code equals()}- method that is similar to {@code equals(Object)}, 39 * but with a covariant parameter type (any subtype of Object). 40 * </p> 41 * <p> 42 * <strong>Notice</strong>: the enums are also checked, 43 * even though they cannot override {@code equals(Object)}. 44 * The reason is to point out that implementing {@code equals()} in enums 45 * is considered an awful practice: it may cause having two different enum values 46 * that are equal using covariant enum method, and not equal when compared normally. 47 * </p> 48 * <p> 49 * Inspired by <a href="https://cs.nyu.edu/~lharris/papers/findbugsPaper.pdf"> 50 * Finding Bugs is Easy, chapter '2.3.1 Bad Covariant Definition of Equals (Eq)'</a>: 51 * </p> 52 * <p> 53 * Java classes may override the {@code equals(Object)} method to define 54 * a predicate for object equality. This method is used by many of the Java 55 * runtime library classes; for example, to implement generic containers. 56 * </p> 57 * <p> 58 * Programmers sometimes mistakenly use the type of their class {@code Foo} 59 * as the type of the parameter to {@code equals()}: 60 * </p> 61 * <pre> 62 * public boolean equals(Foo obj) {...} 63 * </pre> 64 * <p> 65 * This covariant version of {@code equals()} does not override the version in 66 * the {@code Object} class, and it may lead to unexpected behavior at runtime, 67 * especially if the class is used with one of the standard collection classes 68 * which expect that the standard {@code equals(Object)} method is overridden. 69 * </p> 70 * <p> 71 * This kind of bug is not obvious because it looks correct, and in circumstances 72 * where the class is accessed through the references of the class type (rather 73 * than a supertype), it will work correctly. However, the first time it is used 74 * in a container, the behavior might be mysterious. For these reasons, this type 75 * of bug can elude testing and code inspections. 76 * </p> 77 * <p> 78 * To configure the check: 79 * </p> 80 * <pre> 81 * <module name="CovariantEquals"/> 82 * </pre> 83 * <p> 84 * For example: 85 * </p> 86 * <pre> 87 * class Test { 88 * public boolean equals(Test i) { // violation 89 * return false; 90 * } 91 * } 92 * </pre> 93 * <p> 94 * The same class without violations: 95 * </p> 96 * <pre> 97 * class Test { 98 * public boolean equals(Test i) { // no violation 99 * return false; 100 * } 101 * 102 * public boolean equals(Object i) { 103 * return false; 104 * } 105 * } 106 * </pre> 107 * 108 * @since 3.2 109 */ 110 @FileStatefulCheck 111 public class CovariantEqualsCheck extends AbstractCheck { 112 113 /** 114 * A key is pointing to the warning message text in "messages.properties" 115 * file. 116 */ 117 public static final String MSG_KEY = "covariant.equals"; 118 119 /** Set of equals method definitions. */ 120 private final Set<DetailAST> equalsMethods = new HashSet<>(); 121 122 @Override 123 public int[] getDefaultTokens() { 124 return getRequiredTokens(); 125 } 126 127 @Override 128 public int[] getRequiredTokens() { 129 return new int[] {TokenTypes.CLASS_DEF, TokenTypes.LITERAL_NEW, TokenTypes.ENUM_DEF, }; 130 } 131 132 @Override 133 public int[] getAcceptableTokens() { 134 return getRequiredTokens(); 135 } 136 137 @Override 138 public void visitToken(DetailAST ast) { 139 equalsMethods.clear(); 140 141 // examine method definitions for equals methods 142 final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK); 143 if (objBlock != null) { 144 DetailAST child = objBlock.getFirstChild(); 145 boolean hasEqualsObject = false; 146 while (child != null) { 147 if (child.getType() == TokenTypes.METHOD_DEF 148 && CheckUtil.isEqualsMethod(child)) { 149 if (isFirstParameterObject(child)) { 150 hasEqualsObject = true; 151 } 152 else { 153 equalsMethods.add(child); 154 } 155 } 156 child = child.getNextSibling(); 157 } 158 159 // report equals method definitions 160 if (!hasEqualsObject) { 161 for (DetailAST equalsAST : equalsMethods) { 162 final DetailAST nameNode = equalsAST 163 .findFirstToken(TokenTypes.IDENT); 164 log(nameNode, MSG_KEY); 165 } 166 } 167 } 168 } 169 170 /** 171 * Tests whether a method's first parameter is an Object. 172 * @param methodDefAst the method definition AST to test. 173 * Precondition: ast is a TokenTypes.METHOD_DEF node. 174 * @return true if ast has first parameter of type Object. 175 */ 176 private static boolean isFirstParameterObject(DetailAST methodDefAst) { 177 final DetailAST paramsNode = methodDefAst.findFirstToken(TokenTypes.PARAMETERS); 178 179 // parameter type "Object"? 180 final DetailAST paramNode = 181 paramsNode.findFirstToken(TokenTypes.PARAMETER_DEF); 182 final DetailAST typeNode = paramNode.findFirstToken(TokenTypes.TYPE); 183 final FullIdent fullIdent = FullIdent.createFullIdentBelow(typeNode); 184 final String name = fullIdent.getText(); 185 return "Object".equals(name) || "java.lang.Object".equals(name); 186 } 187 188 }