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