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