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