001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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 * <div> 034 * Checks that classes and records which define a covariant {@code equals()} method 035 * also override method {@code equals(Object)}. 036 * </div> 037 * 038 * <p> 039 * Covariant {@code equals()} - method that is similar to {@code equals(Object)}, 040 * but with a covariant parameter type (any subtype of Object). 041 * </p> 042 * 043 * <p> 044 * <strong>Notice</strong>: the enums are also checked, 045 * even though they cannot override {@code equals(Object)}. 046 * The reason is to point out that implementing {@code equals()} in enums 047 * is considered an awful practice: it may cause having two different enum values 048 * that are equal using covariant enum method, and not equal when compared normally. 049 * </p> 050 * 051 * <p> 052 * Note: Compact source files 053 * (<a href="https://openjdk.org/jeps/512">JEP 512</a>) 054 * are skipped by design. Implicit classes in compact source files are not 055 * reusable types and cannot be referenced by name, so they cannot participate 056 * in the polymorphic contexts and collections where covariant {@code equals()} 057 * silently falls back to identity comparison. The rationale for this check 058 * does not extend to compact source files. 059 * </p> 060 * 061 * <p> 062 * Inspired by <a href="https://www.cs.jhu.edu/~daveho/pubs/oopsla2004.pdf"> 063 * Finding Bugs is Easy, chapter '4.5 Bad Covariant Definition of Equals (Eq)'</a>: 064 * </p> 065 * 066 * <p> 067 * Java classes and records may override the {@code equals(Object)} method to define 068 * a predicate for object equality. This method is used by many of the Java 069 * runtime library classes; for example, to implement generic containers. 070 * </p> 071 * 072 * <p> 073 * Programmers sometimes mistakenly use the type of their class {@code Foo} 074 * as the type of the parameter to {@code equals()}: 075 * </p> 076 * {@snippet lang="text" : 077 * public boolean equals(Foo obj) { } 078 * } 079 * 080 * <p> 081 * This covariant version of {@code equals()} does not override the version in 082 * the {@code Object} class, and it may lead to unexpected behavior at runtime, 083 * especially if the class is used with one of the standard collection classes 084 * which expect that the standard {@code equals(Object)} method is overridden. 085 * </p> 086 * 087 * <p> 088 * This kind of bug is not obvious because it looks correct, and in circumstances 089 * where the class is accessed through the references of the class type (rather 090 * than a supertype), it will work correctly. However, the first time it is used 091 * in a container, the behavior might be mysterious. For these reasons, this type 092 * of bug can elude testing and code inspections. 093 * </p> 094 * 095 * @since 3.2 096 */ 097@FileStatefulCheck 098public class CovariantEqualsCheck extends AbstractCheck { 099 100 /** 101 * A key is pointing to the warning message text in "messages.properties" file. 102 */ 103 public static final String MSG_KEY = "covariant.equals"; 104 105 /** Set of equals method definitions. */ 106 private final Set<DetailAST> equalsMethods = new HashSet<>(); 107 108 /** 109 * Creates a new {@code CovariantEqualsCheck} instance. 110 */ 111 public CovariantEqualsCheck() { 112 // no code by default 113 } 114 115 @Override 116 public int[] getDefaultTokens() { 117 return getRequiredTokens(); 118 } 119 120 @Override 121 public int[] getRequiredTokens() { 122 return new int[] { 123 TokenTypes.CLASS_DEF, 124 TokenTypes.LITERAL_NEW, 125 TokenTypes.ENUM_DEF, 126 TokenTypes.RECORD_DEF, 127 }; 128 } 129 130 @Override 131 public int[] getAcceptableTokens() { 132 return getRequiredTokens(); 133 } 134 135 @Override 136 public void beginTree(DetailAST rootAST) { 137 equalsMethods.clear(); 138 } 139 140 @Override 141 public void visitToken(DetailAST ast) { 142 equalsMethods.clear(); 143 144 // examine method definitions for equals methods 145 final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK); 146 if (objBlock != null) { 147 DetailAST child = objBlock.getFirstChild(); 148 boolean hasEqualsObject = false; 149 while (child != null) { 150 if (CheckUtil.isEqualsMethod(child)) { 151 if (isFirstParameterObject(child)) { 152 hasEqualsObject = true; 153 } 154 else { 155 equalsMethods.add(child); 156 } 157 } 158 child = child.getNextSibling(); 159 } 160 161 // report equals method definitions 162 if (!hasEqualsObject) { 163 for (DetailAST equalsAST : equalsMethods) { 164 final DetailAST nameNode = equalsAST 165 .findFirstToken(TokenTypes.IDENT); 166 log(nameNode, MSG_KEY); 167 } 168 } 169 } 170 } 171 172 /** 173 * Tests whether a method's first parameter is an Object. 174 * 175 * @param methodDefAst the method definition AST to test. 176 * Precondition: ast is a TokenTypes.METHOD_DEF node. 177 * @return true if ast has first parameter of type Object. 178 */ 179 private static boolean isFirstParameterObject(DetailAST methodDefAst) { 180 final DetailAST paramsNode = methodDefAst.findFirstToken(TokenTypes.PARAMETERS); 181 182 // parameter type "Object"? 183 final DetailAST paramNode = 184 paramsNode.findFirstToken(TokenTypes.PARAMETER_DEF); 185 final DetailAST typeNode = paramNode.findFirstToken(TokenTypes.TYPE); 186 final FullIdent fullIdent = FullIdent.createFullIdentBelow(typeNode); 187 final String name = fullIdent.getText(); 188 return "Object".equals(name) || "java.lang.Object".equals(name); 189 } 190 191}