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