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