1 /////////////////////////////////////////////////////////////////////////////////////////////// 2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules. 3 // Copyright (C) 2001-2025 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 * <div> 34 * Checks that classes and records which define a covariant {@code equals()} method 35 * also override method {@code equals(Object)}. 36 * </div> 37 * 38 * <p> 39 * Covariant {@code equals()} - method that is similar to {@code equals(Object)}, 40 * but with a covariant parameter type (any subtype of Object). 41 * </p> 42 * 43 * <p> 44 * <strong>Notice</strong>: the enums are also checked, 45 * even though they cannot override {@code equals(Object)}. 46 * The reason is to point out that implementing {@code equals()} in enums 47 * is considered an awful practice: it may cause having two different enum values 48 * that are equal using covariant enum method, and not equal when compared normally. 49 * </p> 50 * 51 * <p> 52 * Inspired by <a href="https://www.cs.jhu.edu/~daveho/pubs/oopsla2004.pdf"> 53 * Finding Bugs is Easy, chapter '4.5 Bad Covariant Definition of Equals (Eq)'</a>: 54 * </p> 55 * 56 * <p> 57 * Java classes and records may override the {@code equals(Object)} method to define 58 * a predicate for object equality. This method is used by many of the Java 59 * runtime library classes; for example, to implement generic containers. 60 * </p> 61 * 62 * <p> 63 * Programmers sometimes mistakenly use the type of their class {@code Foo} 64 * as the type of the parameter to {@code equals()}: 65 * </p> 66 * <pre> 67 * public boolean equals(Foo obj) {...} 68 * </pre> 69 * 70 * <p> 71 * This covariant version of {@code equals()} does not override the version in 72 * the {@code Object} class, and it may lead to unexpected behavior at runtime, 73 * especially if the class is used with one of the standard collection classes 74 * which expect that the standard {@code equals(Object)} method is overridden. 75 * </p> 76 * 77 * <p> 78 * This kind of bug is not obvious because it looks correct, and in circumstances 79 * where the class is accessed through the references of the class type (rather 80 * than a supertype), it will work correctly. However, the first time it is used 81 * in a container, the behavior might be mysterious. For these reasons, this type 82 * of bug can elude testing and code inspections. 83 * </p> 84 * 85 * <p> 86 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 87 * </p> 88 * 89 * <p> 90 * Violation Message Keys: 91 * </p> 92 * <ul> 93 * <li> 94 * {@code covariant.equals} 95 * </li> 96 * </ul> 97 * 98 * @since 3.2 99 */ 100 @FileStatefulCheck 101 public class CovariantEqualsCheck extends AbstractCheck { 102 103 /** 104 * A key is pointing to the warning message text in "messages.properties" 105 * file. 106 */ 107 public static final String MSG_KEY = "covariant.equals"; 108 109 /** Set of equals method definitions. */ 110 private final Set<DetailAST> equalsMethods = new HashSet<>(); 111 112 @Override 113 public int[] getDefaultTokens() { 114 return getRequiredTokens(); 115 } 116 117 @Override 118 public int[] getRequiredTokens() { 119 return new int[] { 120 TokenTypes.CLASS_DEF, 121 TokenTypes.LITERAL_NEW, 122 TokenTypes.ENUM_DEF, 123 TokenTypes.RECORD_DEF, 124 }; 125 } 126 127 @Override 128 public int[] getAcceptableTokens() { 129 return getRequiredTokens(); 130 } 131 132 @Override 133 public void visitToken(DetailAST ast) { 134 equalsMethods.clear(); 135 136 // examine method definitions for equals methods 137 final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK); 138 if (objBlock != null) { 139 DetailAST child = objBlock.getFirstChild(); 140 boolean hasEqualsObject = false; 141 while (child != null) { 142 if (CheckUtil.isEqualsMethod(child)) { 143 if (isFirstParameterObject(child)) { 144 hasEqualsObject = true; 145 } 146 else { 147 equalsMethods.add(child); 148 } 149 } 150 child = child.getNextSibling(); 151 } 152 153 // report equals method definitions 154 if (!hasEqualsObject) { 155 for (DetailAST equalsAST : equalsMethods) { 156 final DetailAST nameNode = equalsAST 157 .findFirstToken(TokenTypes.IDENT); 158 log(nameNode, MSG_KEY); 159 } 160 } 161 } 162 } 163 164 /** 165 * Tests whether a method's first parameter is an Object. 166 * 167 * @param methodDefAst the method definition AST to test. 168 * Precondition: ast is a TokenTypes.METHOD_DEF node. 169 * @return true if ast has first parameter of type Object. 170 */ 171 private static boolean isFirstParameterObject(DetailAST methodDefAst) { 172 final DetailAST paramsNode = methodDefAst.findFirstToken(TokenTypes.PARAMETERS); 173 174 // parameter type "Object"? 175 final DetailAST paramNode = 176 paramsNode.findFirstToken(TokenTypes.PARAMETER_DEF); 177 final DetailAST typeNode = paramNode.findFirstToken(TokenTypes.TYPE); 178 final FullIdent fullIdent = FullIdent.createFullIdentBelow(typeNode); 179 final String name = fullIdent.getText(); 180 return "Object".equals(name) || "java.lang.Object".equals(name); 181 } 182 183 }