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.Collections; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.Map; 026import java.util.Set; 027 028import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 029import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 030import com.puppycrawl.tools.checkstyle.api.DetailAST; 031import com.puppycrawl.tools.checkstyle.api.TokenTypes; 032import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 033import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 034 035/** 036 * <div> 037 * Checks that any combination of String literals 038 * is on the left side of an {@code equals()} comparison. 039 * Also checks for String literals assigned to some field 040 * (such as {@code someString.equals(anotherString = "text")}). 041 * </div> 042 * 043 * <p>Rationale: Calling the {@code equals()} method on String literals 044 * will avoid a potential {@code NullPointerException}. Also, it is 045 * pretty common to see null checks right before equals comparisons 046 * but following this rule such checks are not required. 047 * </p> 048 * 049 * @since 5.0 050 */ 051@FileStatefulCheck 052public class EqualsAvoidNullCheck extends AbstractCheck { 053 054 /** 055 * A key is pointing to the warning message text in "messages.properties" 056 * file. 057 */ 058 public static final String MSG_EQUALS_AVOID_NULL = "equals.avoid.null"; 059 060 /** 061 * A key is pointing to the warning message text in "messages.properties" 062 * file. 063 */ 064 public static final String MSG_EQUALS_IGNORE_CASE_AVOID_NULL = "equalsIgnoreCase.avoid.null"; 065 066 /** Method name for comparison. */ 067 private static final String EQUALS = "equals"; 068 069 /** Type name for comparison. */ 070 private static final String STRING = "String"; 071 072 /** Curly for comparison. */ 073 private static final String LEFT_CURLY = "{"; 074 075 /** Control whether to ignore {@code String.equalsIgnoreCase(String)} invocations. */ 076 private boolean ignoreEqualsIgnoreCase; 077 078 /** Stack of sets of field names, one for each class of a set of nested classes. */ 079 private FieldFrame currentFrame; 080 081 /** 082 * Creates a new {@code EqualsAvoidNullCheck} instance. 083 */ 084 public EqualsAvoidNullCheck() { 085 // no code by default 086 } 087 088 @Override 089 public int[] getDefaultTokens() { 090 return getRequiredTokens(); 091 } 092 093 @Override 094 public int[] getAcceptableTokens() { 095 return getRequiredTokens(); 096 } 097 098 @Override 099 public int[] getRequiredTokens() { 100 return new int[] { 101 TokenTypes.METHOD_CALL, 102 TokenTypes.CLASS_DEF, 103 TokenTypes.METHOD_DEF, 104 TokenTypes.LITERAL_FOR, 105 TokenTypes.LITERAL_CATCH, 106 TokenTypes.LITERAL_TRY, 107 TokenTypes.LITERAL_SWITCH, 108 TokenTypes.VARIABLE_DEF, 109 TokenTypes.PARAMETER_DEF, 110 TokenTypes.CTOR_DEF, 111 TokenTypes.SLIST, 112 TokenTypes.OBJBLOCK, 113 TokenTypes.ENUM_DEF, 114 TokenTypes.ENUM_CONSTANT_DEF, 115 TokenTypes.LITERAL_NEW, 116 TokenTypes.LAMBDA, 117 TokenTypes.PATTERN_VARIABLE_DEF, 118 TokenTypes.RECORD_DEF, 119 TokenTypes.COMPACT_CTOR_DEF, 120 TokenTypes.RECORD_COMPONENT_DEF, 121 }; 122 } 123 124 /** 125 * Setter to control whether to ignore {@code String.equalsIgnoreCase(String)} invocations. 126 * 127 * @param newValue whether to ignore checking 128 * {@code String.equalsIgnoreCase(String)}. 129 * @since 5.4 130 */ 131 public void setIgnoreEqualsIgnoreCase(boolean newValue) { 132 ignoreEqualsIgnoreCase = newValue; 133 } 134 135 @Override 136 public void beginTree(DetailAST rootAST) { 137 currentFrame = new FieldFrame(null); 138 } 139 140 @Override 141 public void visitToken(final DetailAST ast) { 142 switch (ast.getType()) { 143 case TokenTypes.VARIABLE_DEF, 144 TokenTypes.PARAMETER_DEF, 145 TokenTypes.PATTERN_VARIABLE_DEF, 146 TokenTypes.RECORD_COMPONENT_DEF -> currentFrame.addField(ast); 147 148 case TokenTypes.METHOD_CALL -> processMethodCall(ast); 149 150 case TokenTypes.SLIST -> processSlist(ast); 151 152 case TokenTypes.LITERAL_NEW -> processLiteralNew(ast); 153 154 case TokenTypes.OBJBLOCK -> { 155 final int parentType = ast.getParent().getType(); 156 if (!astTypeIsClassOrEnumOrRecordDef(parentType)) { 157 processFrame(ast); 158 } 159 } 160 161 default -> processFrame(ast); 162 } 163 } 164 165 @Override 166 public void leaveToken(DetailAST ast) { 167 switch (ast.getType()) { 168 case TokenTypes.SLIST -> leaveSlist(ast); 169 170 case TokenTypes.LITERAL_NEW -> leaveLiteralNew(ast); 171 172 case TokenTypes.OBJBLOCK -> { 173 final int parentType = ast.getParent().getType(); 174 if (!astTypeIsClassOrEnumOrRecordDef(parentType)) { 175 currentFrame = currentFrame.getParent(); 176 } 177 } 178 179 case TokenTypes.VARIABLE_DEF, 180 TokenTypes.PARAMETER_DEF, 181 TokenTypes.RECORD_COMPONENT_DEF, 182 TokenTypes.METHOD_CALL, 183 TokenTypes.PATTERN_VARIABLE_DEF -> { 184 // intentionally do nothing 185 } 186 187 default -> currentFrame = currentFrame.getParent(); 188 } 189 } 190 191 @Override 192 public void finishTree(DetailAST ast) { 193 traverseFieldFrameTree(currentFrame); 194 } 195 196 /** 197 * Determine whether SLIST begins a block, determined by braces, and add it as 198 * a frame in this case. 199 * 200 * @param ast SLIST ast. 201 */ 202 private void processSlist(DetailAST ast) { 203 if (LEFT_CURLY.equals(ast.getText())) { 204 final FieldFrame frame = new FieldFrame(currentFrame); 205 currentFrame.addChild(frame); 206 currentFrame = frame; 207 } 208 } 209 210 /** 211 * Determine whether SLIST begins a block, determined by braces. 212 * 213 * @param ast SLIST ast. 214 */ 215 private void leaveSlist(DetailAST ast) { 216 if (LEFT_CURLY.equals(ast.getText())) { 217 currentFrame = currentFrame.getParent(); 218 } 219 } 220 221 /** 222 * Process CLASS_DEF, METHOD_DEF, LITERAL_IF, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO, 223 * LITERAL_CATCH, LITERAL_TRY, CTOR_DEF, ENUM_DEF, ENUM_CONSTANT_DEF. 224 * 225 * @param ast processed ast. 226 */ 227 private void processFrame(DetailAST ast) { 228 final FieldFrame frame = new FieldFrame(currentFrame); 229 final int astType = ast.getType(); 230 if (astTypeIsClassOrEnumOrRecordDef(astType)) { 231 frame.setClassOrEnumOrRecordDef(true); 232 frame.setFrameName(TokenUtil.getIdent(ast).getText()); 233 } 234 currentFrame.addChild(frame); 235 currentFrame = frame; 236 } 237 238 /** 239 * Add the method call to the current frame if it should be processed. 240 * 241 * @param methodCall METHOD_CALL ast. 242 */ 243 private void processMethodCall(DetailAST methodCall) { 244 final DetailAST dot = methodCall.getFirstChild(); 245 if (dot.getType() == TokenTypes.DOT) { 246 final String methodName = dot.getLastChild().getText(); 247 if (EQUALS.equals(methodName) 248 || !ignoreEqualsIgnoreCase && "equalsIgnoreCase".equals(methodName)) { 249 currentFrame.addMethodCall(methodCall); 250 } 251 } 252 } 253 254 /** 255 * Determine whether LITERAL_NEW is an anonymous class definition and add it as 256 * a frame in this case. 257 * 258 * @param ast LITERAL_NEW ast. 259 */ 260 private void processLiteralNew(DetailAST ast) { 261 if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) { 262 final FieldFrame frame = new FieldFrame(currentFrame); 263 currentFrame.addChild(frame); 264 currentFrame = frame; 265 } 266 } 267 268 /** 269 * Determine whether LITERAL_NEW is an anonymous class definition and leave 270 * the frame it is in. 271 * 272 * @param ast LITERAL_NEW ast. 273 */ 274 private void leaveLiteralNew(DetailAST ast) { 275 if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) { 276 currentFrame = currentFrame.getParent(); 277 } 278 } 279 280 /** 281 * Traverse the tree of the field frames to check all equals method calls. 282 * 283 * @param frame to check method calls in. 284 */ 285 private void traverseFieldFrameTree(FieldFrame frame) { 286 for (FieldFrame child: frame.getChildren()) { 287 traverseFieldFrameTree(child); 288 289 currentFrame = child; 290 child.getMethodCalls().forEach(this::checkMethodCall); 291 } 292 } 293 294 /** 295 * Check whether the method call should be violated. 296 * 297 * @param methodCall method call to check. 298 */ 299 private void checkMethodCall(DetailAST methodCall) { 300 DetailAST objCalledOn = methodCall.getFirstChild().getFirstChild(); 301 if (objCalledOn.getType() == TokenTypes.DOT) { 302 objCalledOn = objCalledOn.getLastChild(); 303 } 304 final DetailAST expr = methodCall.findFirstToken(TokenTypes.ELIST).getFirstChild(); 305 if (containsOneArgument(methodCall) 306 && containsAllSafeTokens(expr) 307 && isCalledOnStringFieldOrVariable(objCalledOn)) { 308 final String methodName = methodCall.getFirstChild().getLastChild().getText(); 309 if (EQUALS.equals(methodName)) { 310 log(methodCall, MSG_EQUALS_AVOID_NULL); 311 } 312 else { 313 log(methodCall, MSG_EQUALS_IGNORE_CASE_AVOID_NULL); 314 } 315 } 316 } 317 318 /** 319 * Verify that method call has one argument. 320 * 321 * @param methodCall METHOD_CALL DetailAST 322 * @return true if method call has one argument. 323 */ 324 private static boolean containsOneArgument(DetailAST methodCall) { 325 final DetailAST elist = methodCall.findFirstToken(TokenTypes.ELIST); 326 return elist.getChildCount() == 1; 327 } 328 329 /** 330 * Looks for all "safe" Token combinations in the argument 331 * expression branch. 332 * 333 * @param expr the argument expression 334 * @return - true if any child matches the set of tokens, false if not 335 */ 336 private static boolean containsAllSafeTokens(final DetailAST expr) { 337 DetailAST arg = expr.getFirstChild(); 338 arg = skipVariableAssign(arg); 339 340 boolean argIsNotNull = false; 341 if (arg.getType() == TokenTypes.PLUS) { 342 DetailAST child = arg.getFirstChild(); 343 while (child != null 344 && !argIsNotNull) { 345 argIsNotNull = child.getType() == TokenTypes.STRING_LITERAL 346 || child.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN 347 || child.getType() == TokenTypes.IDENT; 348 child = child.getNextSibling(); 349 } 350 } 351 else { 352 argIsNotNull = arg.getType() == TokenTypes.STRING_LITERAL 353 || arg.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN; 354 } 355 356 return argIsNotNull; 357 } 358 359 /** 360 * Skips over an inner assign portion of an argument expression. 361 * 362 * @param currentAST current token in the argument expression 363 * @return the next relevant token 364 */ 365 private static DetailAST skipVariableAssign(final DetailAST currentAST) { 366 DetailAST result = currentAST; 367 while (result.getType() == TokenTypes.LPAREN) { 368 result = result.getNextSibling(); 369 } 370 if (result.getType() == TokenTypes.ASSIGN) { 371 result = result.getFirstChild().getNextSibling(); 372 } 373 return result; 374 } 375 376 /** 377 * Determine, whether equals method is called on a field of String type. 378 * 379 * @param objCalledOn object ast. 380 * @return true if the object is of String type. 381 */ 382 private boolean isCalledOnStringFieldOrVariable(DetailAST objCalledOn) { 383 final boolean result; 384 final DetailAST previousSiblingAst = objCalledOn.getPreviousSibling(); 385 if (previousSiblingAst == null) { 386 result = isStringFieldOrVariable(objCalledOn); 387 } 388 else { 389 if (previousSiblingAst.getType() == TokenTypes.LITERAL_THIS) { 390 result = isStringFieldOrVariableFromThisInstance(objCalledOn); 391 } 392 else { 393 final String className = previousSiblingAst.getText(); 394 result = isStringFieldOrVariableFromClass(objCalledOn, className); 395 } 396 } 397 return result; 398 } 399 400 /** 401 * Whether the field or the variable is of String type. 402 * 403 * @param objCalledOn the field or the variable to check. 404 * @return true if the field or the variable is of String type. 405 */ 406 private boolean isStringFieldOrVariable(DetailAST objCalledOn) { 407 boolean result = false; 408 final String name = objCalledOn.getText(); 409 FieldFrame frame = currentFrame; 410 while (frame != null) { 411 final DetailAST field = frame.findField(name); 412 if (field != null 413 && (frame.isClassOrEnumOrRecordDef() 414 || CheckUtil.isBeforeInSource(field, objCalledOn))) { 415 result = STRING.equals(getFieldType(field)); 416 break; 417 } 418 frame = frame.getParent(); 419 } 420 return result; 421 } 422 423 /** 424 * Whether the field or the variable from THIS instance is of String type. 425 * 426 * @param objCalledOn the field or the variable from THIS instance to check. 427 * @return true if the field or the variable from THIS instance is of String type. 428 */ 429 private boolean isStringFieldOrVariableFromThisInstance(DetailAST objCalledOn) { 430 final String name = objCalledOn.getText(); 431 final DetailAST field = getObjectFrame(currentFrame).findField(name); 432 return field != null && STRING.equals(getFieldType(field)); 433 } 434 435 /** 436 * Whether the field or the variable from the specified class is of String type. 437 * 438 * @param objCalledOn the field or the variable from the specified class to check. 439 * @param className the name of the class to check in. 440 * @return true if the field or the variable from the specified class is of String type. 441 */ 442 private boolean isStringFieldOrVariableFromClass(DetailAST objCalledOn, 443 final String className) { 444 boolean result = false; 445 final String name = objCalledOn.getText(); 446 FieldFrame frame = currentFrame; 447 while (frame != null) { 448 if (className.equals(frame.getFrameName())) { 449 final DetailAST field = frame.findField(name); 450 result = STRING.equals(getFieldType(field)); 451 break; 452 } 453 frame = frame.getParent(); 454 } 455 return result; 456 } 457 458 /** 459 * Get the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF. 460 * 461 * @param frame to start the search from. 462 * @return the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF. 463 */ 464 private static FieldFrame getObjectFrame(FieldFrame frame) { 465 FieldFrame objectFrame = frame; 466 while (!objectFrame.isClassOrEnumOrRecordDef()) { 467 objectFrame = objectFrame.getParent(); 468 } 469 return objectFrame; 470 } 471 472 /** 473 * Get field type. 474 * 475 * @param field to get the type from. 476 * @return type of the field. 477 */ 478 private static String getFieldType(DetailAST field) { 479 String fieldType = null; 480 final DetailAST identAst = field.findFirstToken(TokenTypes.TYPE) 481 .findFirstToken(TokenTypes.IDENT); 482 if (identAst != null) { 483 fieldType = identAst.getText(); 484 } 485 return fieldType; 486 } 487 488 /** 489 * Verify that a token is either CLASS_DEF, RECORD_DEF, or ENUM_DEF. 490 * 491 * @param tokenType the type of token 492 * @return true if token is of specified type. 493 */ 494 private static boolean astTypeIsClassOrEnumOrRecordDef(int tokenType) { 495 return tokenType == TokenTypes.CLASS_DEF 496 || tokenType == TokenTypes.RECORD_DEF 497 || tokenType == TokenTypes.ENUM_DEF; 498 } 499 500 /** 501 * Holds the names of fields of a type. 502 */ 503 private static final class FieldFrame { 504 505 /** Parent frame. */ 506 private final FieldFrame parent; 507 508 /** Set of frame's children. */ 509 private final Set<FieldFrame> children = new HashSet<>(); 510 511 /** Map of field name to field DetailAst. */ 512 private final Map<String, DetailAST> fieldNameToAst = new HashMap<>(); 513 514 /** Set of equals calls. */ 515 private final Set<DetailAST> methodCalls = new HashSet<>(); 516 517 /** Name of the class, enum or enum constant declaration. */ 518 private String frameName; 519 520 /** Whether the frame is CLASS_DEF, ENUM_DEF, ENUM_CONST_DEF, or RECORD_DEF. */ 521 private boolean classOrEnumOrRecordDef; 522 523 /** 524 * Creates new frame. 525 * 526 * @param parent parent frame. 527 */ 528 private FieldFrame(FieldFrame parent) { 529 this.parent = parent; 530 } 531 532 /** 533 * Set the frame name. 534 * 535 * @param frameName value to set. 536 */ 537 /* package */ void setFrameName(String frameName) { 538 this.frameName = frameName; 539 } 540 541 /** 542 * Getter for the frame name. 543 * 544 * @return frame name. 545 */ 546 /* package */ String getFrameName() { 547 return frameName; 548 } 549 550 /** 551 * Getter for the parent frame. 552 * 553 * @return parent frame. 554 */ 555 /* package */ FieldFrame getParent() { 556 return parent; 557 } 558 559 /** 560 * Getter for frame's children. 561 * 562 * @return children of this frame. 563 */ 564 /* package */ Set<FieldFrame> getChildren() { 565 return Collections.unmodifiableSet(children); 566 } 567 568 /** 569 * Add child frame to this frame. 570 * 571 * @param child frame to add. 572 */ 573 /* package */ void addChild(FieldFrame child) { 574 children.add(child); 575 } 576 577 /** 578 * Add field to this FieldFrame. 579 * 580 * @param field the ast of the field. 581 */ 582 /* package */ void addField(DetailAST field) { 583 if (field.findFirstToken(TokenTypes.IDENT) != null) { 584 fieldNameToAst.put(getFieldName(field), field); 585 } 586 } 587 588 /** 589 * Sets isClassOrEnumOrRecordDef. 590 * 591 * @param value value to set. 592 */ 593 /* package */ void setClassOrEnumOrRecordDef(boolean value) { 594 classOrEnumOrRecordDef = value; 595 } 596 597 /** 598 * Getter for classOrEnumOrRecordDef. 599 * 600 * @return classOrEnumOrRecordDef. 601 */ 602 /* package */ boolean isClassOrEnumOrRecordDef() { 603 return classOrEnumOrRecordDef; 604 } 605 606 /** 607 * Add method call to this frame. 608 * 609 * @param methodCall METHOD_CALL ast. 610 */ 611 /* package */ void addMethodCall(DetailAST methodCall) { 612 methodCalls.add(methodCall); 613 } 614 615 /** 616 * Determines whether this FieldFrame contains the field. 617 * 618 * @param name name of the field to check. 619 * @return DetailAST if this FieldFrame contains instance field. 620 */ 621 /* package */ DetailAST findField(String name) { 622 return fieldNameToAst.get(name); 623 } 624 625 /** 626 * Getter for frame's method calls. 627 * 628 * @return method calls of this frame. 629 */ 630 /* package */ Set<DetailAST> getMethodCalls() { 631 return Collections.unmodifiableSet(methodCalls); 632 } 633 634 /** 635 * Get the name of the field. 636 * 637 * @param field to get the name from. 638 * @return name of the field. 639 */ 640 private static String getFieldName(DetailAST field) { 641 return TokenUtil.getIdent(field).getText(); 642 } 643 644 } 645 646}