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