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