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