001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2024 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 case TokenTypes.PARAMETER_DEF: 161 case TokenTypes.PATTERN_VARIABLE_DEF: 162 case TokenTypes.RECORD_COMPONENT_DEF: 163 currentFrame.addField(ast); 164 break; 165 case TokenTypes.METHOD_CALL: 166 processMethodCall(ast); 167 break; 168 case TokenTypes.SLIST: 169 processSlist(ast); 170 break; 171 case TokenTypes.LITERAL_NEW: 172 processLiteralNew(ast); 173 break; 174 case TokenTypes.OBJBLOCK: 175 final int parentType = ast.getParent().getType(); 176 if (!astTypeIsClassOrEnumOrRecordDef(parentType)) { 177 processFrame(ast); 178 } 179 break; 180 default: 181 processFrame(ast); 182 } 183 } 184 185 @Override 186 public void leaveToken(DetailAST ast) { 187 switch (ast.getType()) { 188 case TokenTypes.SLIST: 189 leaveSlist(ast); 190 break; 191 case TokenTypes.LITERAL_NEW: 192 leaveLiteralNew(ast); 193 break; 194 case TokenTypes.OBJBLOCK: 195 final int parentType = ast.getParent().getType(); 196 if (!astTypeIsClassOrEnumOrRecordDef(parentType)) { 197 currentFrame = currentFrame.getParent(); 198 } 199 break; 200 case TokenTypes.VARIABLE_DEF: 201 case TokenTypes.PARAMETER_DEF: 202 case TokenTypes.RECORD_COMPONENT_DEF: 203 case TokenTypes.METHOD_CALL: 204 case TokenTypes.PATTERN_VARIABLE_DEF: 205 break; 206 default: 207 currentFrame = currentFrame.getParent(); 208 break; 209 } 210 } 211 212 @Override 213 public void finishTree(DetailAST ast) { 214 traverseFieldFrameTree(currentFrame); 215 } 216 217 /** 218 * Determine whether SLIST begins a block, determined by braces, and add it as 219 * a frame in this case. 220 * 221 * @param ast SLIST ast. 222 */ 223 private void processSlist(DetailAST ast) { 224 if (LEFT_CURLY.equals(ast.getText())) { 225 final FieldFrame frame = new FieldFrame(currentFrame); 226 currentFrame.addChild(frame); 227 currentFrame = frame; 228 } 229 } 230 231 /** 232 * Determine whether SLIST begins a block, determined by braces. 233 * 234 * @param ast SLIST ast. 235 */ 236 private void leaveSlist(DetailAST ast) { 237 if (LEFT_CURLY.equals(ast.getText())) { 238 currentFrame = currentFrame.getParent(); 239 } 240 } 241 242 /** 243 * Process CLASS_DEF, METHOD_DEF, LITERAL_IF, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO, 244 * LITERAL_CATCH, LITERAL_TRY, CTOR_DEF, ENUM_DEF, ENUM_CONSTANT_DEF. 245 * 246 * @param ast processed ast. 247 */ 248 private void processFrame(DetailAST ast) { 249 final FieldFrame frame = new FieldFrame(currentFrame); 250 final int astType = ast.getType(); 251 if (astTypeIsClassOrEnumOrRecordDef(astType)) { 252 frame.setClassOrEnumOrRecordDef(true); 253 frame.setFrameName(ast.findFirstToken(TokenTypes.IDENT).getText()); 254 } 255 currentFrame.addChild(frame); 256 currentFrame = frame; 257 } 258 259 /** 260 * Add the method call to the current frame if it should be processed. 261 * 262 * @param methodCall METHOD_CALL ast. 263 */ 264 private void processMethodCall(DetailAST methodCall) { 265 final DetailAST dot = methodCall.getFirstChild(); 266 if (dot.getType() == TokenTypes.DOT) { 267 final String methodName = dot.getLastChild().getText(); 268 if (EQUALS.equals(methodName) 269 || !ignoreEqualsIgnoreCase && "equalsIgnoreCase".equals(methodName)) { 270 currentFrame.addMethodCall(methodCall); 271 } 272 } 273 } 274 275 /** 276 * Determine whether LITERAL_NEW is an anonymous class definition and add it as 277 * a frame in this case. 278 * 279 * @param ast LITERAL_NEW ast. 280 */ 281 private void processLiteralNew(DetailAST ast) { 282 if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) { 283 final FieldFrame frame = new FieldFrame(currentFrame); 284 currentFrame.addChild(frame); 285 currentFrame = frame; 286 } 287 } 288 289 /** 290 * Determine whether LITERAL_NEW is an anonymous class definition and leave 291 * the frame it is in. 292 * 293 * @param ast LITERAL_NEW ast. 294 */ 295 private void leaveLiteralNew(DetailAST ast) { 296 if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) { 297 currentFrame = currentFrame.getParent(); 298 } 299 } 300 301 /** 302 * Traverse the tree of the field frames to check all equals method calls. 303 * 304 * @param frame to check method calls in. 305 */ 306 private void traverseFieldFrameTree(FieldFrame frame) { 307 for (FieldFrame child: frame.getChildren()) { 308 traverseFieldFrameTree(child); 309 310 currentFrame = child; 311 child.getMethodCalls().forEach(this::checkMethodCall); 312 } 313 } 314 315 /** 316 * Check whether the method call should be violated. 317 * 318 * @param methodCall method call to check. 319 */ 320 private void checkMethodCall(DetailAST methodCall) { 321 DetailAST objCalledOn = methodCall.getFirstChild().getFirstChild(); 322 if (objCalledOn.getType() == TokenTypes.DOT) { 323 objCalledOn = objCalledOn.getLastChild(); 324 } 325 final DetailAST expr = methodCall.findFirstToken(TokenTypes.ELIST).getFirstChild(); 326 if (containsOneArgument(methodCall) 327 && containsAllSafeTokens(expr) 328 && isCalledOnStringFieldOrVariable(objCalledOn)) { 329 final String methodName = methodCall.getFirstChild().getLastChild().getText(); 330 if (EQUALS.equals(methodName)) { 331 log(methodCall, MSG_EQUALS_AVOID_NULL); 332 } 333 else { 334 log(methodCall, MSG_EQUALS_IGNORE_CASE_AVOID_NULL); 335 } 336 } 337 } 338 339 /** 340 * Verify that method call has one argument. 341 * 342 * @param methodCall METHOD_CALL DetailAST 343 * @return true if method call has one argument. 344 */ 345 private static boolean containsOneArgument(DetailAST methodCall) { 346 final DetailAST elist = methodCall.findFirstToken(TokenTypes.ELIST); 347 return elist.getChildCount() == 1; 348 } 349 350 /** 351 * Looks for all "safe" Token combinations in the argument 352 * expression branch. 353 * 354 * @param expr the argument expression 355 * @return - true if any child matches the set of tokens, false if not 356 */ 357 private static boolean containsAllSafeTokens(final DetailAST expr) { 358 DetailAST arg = expr.getFirstChild(); 359 arg = skipVariableAssign(arg); 360 361 boolean argIsNotNull = false; 362 if (arg.getType() == TokenTypes.PLUS) { 363 DetailAST child = arg.getFirstChild(); 364 while (child != null 365 && !argIsNotNull) { 366 argIsNotNull = child.getType() == TokenTypes.STRING_LITERAL 367 || child.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN 368 || child.getType() == TokenTypes.IDENT; 369 child = child.getNextSibling(); 370 } 371 } 372 else { 373 argIsNotNull = arg.getType() == TokenTypes.STRING_LITERAL 374 || arg.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN; 375 } 376 377 return argIsNotNull; 378 } 379 380 /** 381 * Skips over an inner assign portion of an argument expression. 382 * 383 * @param currentAST current token in the argument expression 384 * @return the next relevant token 385 */ 386 private static DetailAST skipVariableAssign(final DetailAST currentAST) { 387 DetailAST result = currentAST; 388 while (result.getType() == TokenTypes.LPAREN) { 389 result = result.getNextSibling(); 390 } 391 if (result.getType() == TokenTypes.ASSIGN) { 392 result = result.getFirstChild().getNextSibling(); 393 } 394 return result; 395 } 396 397 /** 398 * Determine, whether equals method is called on a field of String type. 399 * 400 * @param objCalledOn object ast. 401 * @return true if the object is of String type. 402 */ 403 private boolean isCalledOnStringFieldOrVariable(DetailAST objCalledOn) { 404 final boolean result; 405 final DetailAST previousSiblingAst = objCalledOn.getPreviousSibling(); 406 if (previousSiblingAst == null) { 407 result = isStringFieldOrVariable(objCalledOn); 408 } 409 else { 410 if (previousSiblingAst.getType() == TokenTypes.LITERAL_THIS) { 411 result = isStringFieldOrVariableFromThisInstance(objCalledOn); 412 } 413 else { 414 final String className = previousSiblingAst.getText(); 415 result = isStringFieldOrVariableFromClass(objCalledOn, className); 416 } 417 } 418 return result; 419 } 420 421 /** 422 * Whether the field or the variable is of String type. 423 * 424 * @param objCalledOn the field or the variable to check. 425 * @return true if the field or the variable is of String type. 426 */ 427 private boolean isStringFieldOrVariable(DetailAST objCalledOn) { 428 boolean result = false; 429 final String name = objCalledOn.getText(); 430 FieldFrame frame = currentFrame; 431 while (frame != null) { 432 final DetailAST field = frame.findField(name); 433 if (field != null 434 && (frame.isClassOrEnumOrRecordDef() 435 || CheckUtil.isBeforeInSource(field, objCalledOn))) { 436 result = STRING.equals(getFieldType(field)); 437 break; 438 } 439 frame = frame.getParent(); 440 } 441 return result; 442 } 443 444 /** 445 * Whether the field or the variable from THIS instance is of String type. 446 * 447 * @param objCalledOn the field or the variable from THIS instance to check. 448 * @return true if the field or the variable from THIS instance is of String type. 449 */ 450 private boolean isStringFieldOrVariableFromThisInstance(DetailAST objCalledOn) { 451 final String name = objCalledOn.getText(); 452 final DetailAST field = getObjectFrame(currentFrame).findField(name); 453 return field != null && STRING.equals(getFieldType(field)); 454 } 455 456 /** 457 * Whether the field or the variable from the specified class is of String type. 458 * 459 * @param objCalledOn the field or the variable from the specified class to check. 460 * @param className the name of the class to check in. 461 * @return true if the field or the variable from the specified class is of String type. 462 */ 463 private boolean isStringFieldOrVariableFromClass(DetailAST objCalledOn, 464 final String className) { 465 boolean result = false; 466 final String name = objCalledOn.getText(); 467 FieldFrame frame = currentFrame; 468 while (frame != null) { 469 if (className.equals(frame.getFrameName())) { 470 final DetailAST field = frame.findField(name); 471 result = STRING.equals(getFieldType(field)); 472 break; 473 } 474 frame = frame.getParent(); 475 } 476 return result; 477 } 478 479 /** 480 * Get the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF. 481 * 482 * @param frame to start the search from. 483 * @return the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF. 484 */ 485 private static FieldFrame getObjectFrame(FieldFrame frame) { 486 FieldFrame objectFrame = frame; 487 while (!objectFrame.isClassOrEnumOrRecordDef()) { 488 objectFrame = objectFrame.getParent(); 489 } 490 return objectFrame; 491 } 492 493 /** 494 * Get field type. 495 * 496 * @param field to get the type from. 497 * @return type of the field. 498 */ 499 private static String getFieldType(DetailAST field) { 500 String fieldType = null; 501 final DetailAST identAst = field.findFirstToken(TokenTypes.TYPE) 502 .findFirstToken(TokenTypes.IDENT); 503 if (identAst != null) { 504 fieldType = identAst.getText(); 505 } 506 return fieldType; 507 } 508 509 /** 510 * Verify that a token is either CLASS_DEF, RECORD_DEF, or ENUM_DEF. 511 * 512 * @param tokenType the type of token 513 * @return true if token is of specified type. 514 */ 515 private static boolean astTypeIsClassOrEnumOrRecordDef(int tokenType) { 516 return tokenType == TokenTypes.CLASS_DEF 517 || tokenType == TokenTypes.RECORD_DEF 518 || tokenType == TokenTypes.ENUM_DEF; 519 } 520 521 /** 522 * Holds the names of fields of a type. 523 */ 524 private static final class FieldFrame { 525 526 /** Parent frame. */ 527 private final FieldFrame parent; 528 529 /** Set of frame's children. */ 530 private final Set<FieldFrame> children = new HashSet<>(); 531 532 /** Map of field name to field DetailAst. */ 533 private final Map<String, DetailAST> fieldNameToAst = new HashMap<>(); 534 535 /** Set of equals calls. */ 536 private final Set<DetailAST> methodCalls = new HashSet<>(); 537 538 /** Name of the class, enum or enum constant declaration. */ 539 private String frameName; 540 541 /** Whether the frame is CLASS_DEF, ENUM_DEF, ENUM_CONST_DEF, or RECORD_DEF. */ 542 private boolean classOrEnumOrRecordDef; 543 544 /** 545 * Creates new frame. 546 * 547 * @param parent parent frame. 548 */ 549 private FieldFrame(FieldFrame parent) { 550 this.parent = parent; 551 } 552 553 /** 554 * Set the frame name. 555 * 556 * @param frameName value to set. 557 */ 558 public void setFrameName(String frameName) { 559 this.frameName = frameName; 560 } 561 562 /** 563 * Getter for the frame name. 564 * 565 * @return frame name. 566 */ 567 public String getFrameName() { 568 return frameName; 569 } 570 571 /** 572 * Getter for the parent frame. 573 * 574 * @return parent frame. 575 */ 576 public FieldFrame getParent() { 577 return parent; 578 } 579 580 /** 581 * Getter for frame's children. 582 * 583 * @return children of this frame. 584 */ 585 public Set<FieldFrame> getChildren() { 586 return Collections.unmodifiableSet(children); 587 } 588 589 /** 590 * Add child frame to this frame. 591 * 592 * @param child frame to add. 593 */ 594 public void addChild(FieldFrame child) { 595 children.add(child); 596 } 597 598 /** 599 * Add field to this FieldFrame. 600 * 601 * @param field the ast of the field. 602 */ 603 public void addField(DetailAST field) { 604 if (field.findFirstToken(TokenTypes.IDENT) != null) { 605 fieldNameToAst.put(getFieldName(field), field); 606 } 607 } 608 609 /** 610 * Sets isClassOrEnumOrRecordDef. 611 * 612 * @param value value to set. 613 */ 614 public void setClassOrEnumOrRecordDef(boolean value) { 615 classOrEnumOrRecordDef = value; 616 } 617 618 /** 619 * Getter for classOrEnumOrRecordDef. 620 * 621 * @return classOrEnumOrRecordDef. 622 */ 623 public boolean isClassOrEnumOrRecordDef() { 624 return classOrEnumOrRecordDef; 625 } 626 627 /** 628 * Add method call to this frame. 629 * 630 * @param methodCall METHOD_CALL ast. 631 */ 632 public void addMethodCall(DetailAST methodCall) { 633 methodCalls.add(methodCall); 634 } 635 636 /** 637 * Determines whether this FieldFrame contains the field. 638 * 639 * @param name name of the field to check. 640 * @return DetailAST if this FieldFrame contains instance field. 641 */ 642 public DetailAST findField(String name) { 643 return fieldNameToAst.get(name); 644 } 645 646 /** 647 * Getter for frame's method calls. 648 * 649 * @return method calls of this frame. 650 */ 651 public Set<DetailAST> getMethodCalls() { 652 return Collections.unmodifiableSet(methodCalls); 653 } 654 655 /** 656 * Get the name of the field. 657 * 658 * @param field to get the name from. 659 * @return name of the field. 660 */ 661 private static String getFieldName(DetailAST field) { 662 return field.findFirstToken(TokenTypes.IDENT).getText(); 663 } 664 665 } 666 667}