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