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