View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.coding;
21  
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.Map;
26  import java.util.Set;
27  
28  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
29  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
30  import com.puppycrawl.tools.checkstyle.api.DetailAST;
31  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
32  import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
33  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
34  
35  /**
36   * <div>
37   * Checks that any combination of String literals
38   * is on the left side of an {@code equals()} comparison.
39   * Also checks for String literals assigned to some field
40   * (such as {@code someString.equals(anotherString = "text")}).
41   * </div>
42   *
43   * <p>Rationale: Calling the {@code equals()} method on String literals
44   * will avoid a potential {@code NullPointerException}. Also, it is
45   * pretty common to see null checks right before equals comparisons
46   * but following this rule such checks are not required.
47   * </p>
48   *
49   * @since 5.0
50   */
51  @FileStatefulCheck
52  public class EqualsAvoidNullCheck extends AbstractCheck {
53  
54      /**
55       * A key is pointing to the warning message text in "messages.properties"
56       * file.
57       */
58      public static final String MSG_EQUALS_AVOID_NULL = "equals.avoid.null";
59  
60      /**
61       * A key is pointing to the warning message text in "messages.properties"
62       * file.
63       */
64      public static final String MSG_EQUALS_IGNORE_CASE_AVOID_NULL = "equalsIgnoreCase.avoid.null";
65  
66      /** Method name for comparison. */
67      private static final String EQUALS = "equals";
68  
69      /** Type name for comparison. */
70      private static final String STRING = "String";
71  
72      /** Curly for comparison. */
73      private static final String LEFT_CURLY = "{";
74  
75      /** Control whether to ignore {@code String.equalsIgnoreCase(String)} invocations. */
76      private boolean ignoreEqualsIgnoreCase;
77  
78      /** Stack of sets of field names, one for each class of a set of nested classes. */
79      private FieldFrame currentFrame;
80  
81      /**
82       * Creates a new {@code EqualsAvoidNullCheck} instance.
83       */
84      public EqualsAvoidNullCheck() {
85          // no code by default
86      }
87  
88      @Override
89      public int[] getDefaultTokens() {
90          return getRequiredTokens();
91      }
92  
93      @Override
94      public int[] getAcceptableTokens() {
95          return getRequiredTokens();
96      }
97  
98      @Override
99      public int[] getRequiredTokens() {
100         return new int[] {
101             TokenTypes.METHOD_CALL,
102             TokenTypes.CLASS_DEF,
103             TokenTypes.METHOD_DEF,
104             TokenTypes.LITERAL_FOR,
105             TokenTypes.LITERAL_CATCH,
106             TokenTypes.LITERAL_TRY,
107             TokenTypes.LITERAL_SWITCH,
108             TokenTypes.VARIABLE_DEF,
109             TokenTypes.PARAMETER_DEF,
110             TokenTypes.CTOR_DEF,
111             TokenTypes.SLIST,
112             TokenTypes.OBJBLOCK,
113             TokenTypes.ENUM_DEF,
114             TokenTypes.ENUM_CONSTANT_DEF,
115             TokenTypes.LITERAL_NEW,
116             TokenTypes.LAMBDA,
117             TokenTypes.PATTERN_VARIABLE_DEF,
118             TokenTypes.RECORD_DEF,
119             TokenTypes.COMPACT_CTOR_DEF,
120             TokenTypes.RECORD_COMPONENT_DEF,
121         };
122     }
123 
124     /**
125      * Setter to control whether to ignore {@code String.equalsIgnoreCase(String)} invocations.
126      *
127      * @param newValue whether to ignore checking
128      *     {@code String.equalsIgnoreCase(String)}.
129      * @since 5.4
130      */
131     public void setIgnoreEqualsIgnoreCase(boolean newValue) {
132         ignoreEqualsIgnoreCase = newValue;
133     }
134 
135     @Override
136     public void beginTree(DetailAST rootAST) {
137         currentFrame = new FieldFrame(null);
138     }
139 
140     @Override
141     public void visitToken(final DetailAST ast) {
142         switch (ast.getType()) {
143             case TokenTypes.VARIABLE_DEF,
144                  TokenTypes.PARAMETER_DEF,
145                  TokenTypes.PATTERN_VARIABLE_DEF,
146                  TokenTypes.RECORD_COMPONENT_DEF -> currentFrame.addField(ast);
147 
148             case TokenTypes.METHOD_CALL -> processMethodCall(ast);
149 
150             case TokenTypes.SLIST -> processSlist(ast);
151 
152             case TokenTypes.LITERAL_NEW -> processLiteralNew(ast);
153 
154             case TokenTypes.OBJBLOCK -> {
155                 final int parentType = ast.getParent().getType();
156                 if (!astTypeIsClassOrEnumOrRecordDef(parentType)) {
157                     processFrame(ast);
158                 }
159             }
160 
161             default -> processFrame(ast);
162         }
163     }
164 
165     @Override
166     public void leaveToken(DetailAST ast) {
167         switch (ast.getType()) {
168             case TokenTypes.SLIST -> leaveSlist(ast);
169 
170             case TokenTypes.LITERAL_NEW -> leaveLiteralNew(ast);
171 
172             case TokenTypes.OBJBLOCK -> {
173                 final int parentType = ast.getParent().getType();
174                 if (!astTypeIsClassOrEnumOrRecordDef(parentType)) {
175                     currentFrame = currentFrame.getParent();
176                 }
177             }
178 
179             case TokenTypes.VARIABLE_DEF,
180                  TokenTypes.PARAMETER_DEF,
181                  TokenTypes.RECORD_COMPONENT_DEF,
182                  TokenTypes.METHOD_CALL,
183                  TokenTypes.PATTERN_VARIABLE_DEF -> {
184                 // intentionally do nothing
185             }
186 
187             default -> currentFrame = currentFrame.getParent();
188         }
189     }
190 
191     @Override
192     public void finishTree(DetailAST ast) {
193         traverseFieldFrameTree(currentFrame);
194     }
195 
196     /**
197      * Determine whether SLIST begins a block, determined by braces, and add it as
198      * a frame in this case.
199      *
200      * @param ast SLIST ast.
201      */
202     private void processSlist(DetailAST ast) {
203         if (LEFT_CURLY.equals(ast.getText())) {
204             final FieldFrame frame = new FieldFrame(currentFrame);
205             currentFrame.addChild(frame);
206             currentFrame = frame;
207         }
208     }
209 
210     /**
211      * Determine whether SLIST begins a block, determined by braces.
212      *
213      * @param ast SLIST ast.
214      */
215     private void leaveSlist(DetailAST ast) {
216         if (LEFT_CURLY.equals(ast.getText())) {
217             currentFrame = currentFrame.getParent();
218         }
219     }
220 
221     /**
222      * Process CLASS_DEF, METHOD_DEF, LITERAL_IF, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO,
223      * LITERAL_CATCH, LITERAL_TRY, CTOR_DEF, ENUM_DEF, ENUM_CONSTANT_DEF.
224      *
225      * @param ast processed ast.
226      */
227     private void processFrame(DetailAST ast) {
228         final FieldFrame frame = new FieldFrame(currentFrame);
229         final int astType = ast.getType();
230         if (astTypeIsClassOrEnumOrRecordDef(astType)) {
231             frame.setClassOrEnumOrRecordDef(true);
232             frame.setFrameName(TokenUtil.getIdent(ast).getText());
233         }
234         currentFrame.addChild(frame);
235         currentFrame = frame;
236     }
237 
238     /**
239      * Add the method call to the current frame if it should be processed.
240      *
241      * @param methodCall METHOD_CALL ast.
242      */
243     private void processMethodCall(DetailAST methodCall) {
244         final DetailAST dot = methodCall.getFirstChild();
245         if (dot.getType() == TokenTypes.DOT) {
246             final String methodName = dot.getLastChild().getText();
247             if (EQUALS.equals(methodName)
248                     || !ignoreEqualsIgnoreCase && "equalsIgnoreCase".equals(methodName)) {
249                 currentFrame.addMethodCall(methodCall);
250             }
251         }
252     }
253 
254     /**
255      * Determine whether LITERAL_NEW is an anonymous class definition and add it as
256      * a frame in this case.
257      *
258      * @param ast LITERAL_NEW ast.
259      */
260     private void processLiteralNew(DetailAST ast) {
261         if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
262             final FieldFrame frame = new FieldFrame(currentFrame);
263             currentFrame.addChild(frame);
264             currentFrame = frame;
265         }
266     }
267 
268     /**
269      * Determine whether LITERAL_NEW is an anonymous class definition and leave
270      * the frame it is in.
271      *
272      * @param ast LITERAL_NEW ast.
273      */
274     private void leaveLiteralNew(DetailAST ast) {
275         if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
276             currentFrame = currentFrame.getParent();
277         }
278     }
279 
280     /**
281      * Traverse the tree of the field frames to check all equals method calls.
282      *
283      * @param frame to check method calls in.
284      */
285     private void traverseFieldFrameTree(FieldFrame frame) {
286         for (FieldFrame child: frame.getChildren()) {
287             traverseFieldFrameTree(child);
288 
289             currentFrame = child;
290             child.getMethodCalls().forEach(this::checkMethodCall);
291         }
292     }
293 
294     /**
295      * Check whether the method call should be violated.
296      *
297      * @param methodCall method call to check.
298      */
299     private void checkMethodCall(DetailAST methodCall) {
300         DetailAST objCalledOn = methodCall.getFirstChild().getFirstChild();
301         if (objCalledOn.getType() == TokenTypes.DOT) {
302             objCalledOn = objCalledOn.getLastChild();
303         }
304         final DetailAST expr = methodCall.findFirstToken(TokenTypes.ELIST).getFirstChild();
305         if (containsOneArgument(methodCall)
306                 && containsAllSafeTokens(expr)
307                 && isCalledOnStringFieldOrVariable(objCalledOn)) {
308             final String methodName = methodCall.getFirstChild().getLastChild().getText();
309             if (EQUALS.equals(methodName)) {
310                 log(methodCall, MSG_EQUALS_AVOID_NULL);
311             }
312             else {
313                 log(methodCall, MSG_EQUALS_IGNORE_CASE_AVOID_NULL);
314             }
315         }
316     }
317 
318     /**
319      * Verify that method call has one argument.
320      *
321      * @param methodCall METHOD_CALL DetailAST
322      * @return true if method call has one argument.
323      */
324     private static boolean containsOneArgument(DetailAST methodCall) {
325         final DetailAST elist = methodCall.findFirstToken(TokenTypes.ELIST);
326         return elist.getChildCount() == 1;
327     }
328 
329     /**
330      * Looks for all "safe" Token combinations in the argument
331      * expression branch.
332      *
333      * @param expr the argument expression
334      * @return - true if any child matches the set of tokens, false if not
335      */
336     private static boolean containsAllSafeTokens(final DetailAST expr) {
337         DetailAST arg = expr.getFirstChild();
338         arg = skipVariableAssign(arg);
339 
340         boolean argIsNotNull = false;
341         if (arg.getType() == TokenTypes.PLUS) {
342             DetailAST child = arg.getFirstChild();
343             while (child != null
344                     && !argIsNotNull) {
345                 argIsNotNull = child.getType() == TokenTypes.STRING_LITERAL
346                         || child.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN
347                         || child.getType() == TokenTypes.IDENT;
348                 child = child.getNextSibling();
349             }
350         }
351         else {
352             argIsNotNull = arg.getType() == TokenTypes.STRING_LITERAL
353                     || arg.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN;
354         }
355 
356         return argIsNotNull;
357     }
358 
359     /**
360      * Skips over an inner assign portion of an argument expression.
361      *
362      * @param currentAST current token in the argument expression
363      * @return the next relevant token
364      */
365     private static DetailAST skipVariableAssign(final DetailAST currentAST) {
366         DetailAST result = currentAST;
367         while (result.getType() == TokenTypes.LPAREN) {
368             result = result.getNextSibling();
369         }
370         if (result.getType() == TokenTypes.ASSIGN) {
371             result = result.getFirstChild().getNextSibling();
372         }
373         return result;
374     }
375 
376     /**
377      * Determine, whether equals method is called on a field of String type.
378      *
379      * @param objCalledOn object ast.
380      * @return true if the object is of String type.
381      */
382     private boolean isCalledOnStringFieldOrVariable(DetailAST objCalledOn) {
383         final boolean result;
384         final DetailAST previousSiblingAst = objCalledOn.getPreviousSibling();
385         if (previousSiblingAst == null) {
386             result = isStringFieldOrVariable(objCalledOn);
387         }
388         else {
389             if (previousSiblingAst.getType() == TokenTypes.LITERAL_THIS) {
390                 result = isStringFieldOrVariableFromThisInstance(objCalledOn);
391             }
392             else {
393                 final String className = previousSiblingAst.getText();
394                 result = isStringFieldOrVariableFromClass(objCalledOn, className);
395             }
396         }
397         return result;
398     }
399 
400     /**
401      * Whether the field or the variable is of String type.
402      *
403      * @param objCalledOn the field or the variable to check.
404      * @return true if the field or the variable is of String type.
405      */
406     private boolean isStringFieldOrVariable(DetailAST objCalledOn) {
407         boolean result = false;
408         final String name = objCalledOn.getText();
409         FieldFrame frame = currentFrame;
410         while (frame != null) {
411             final DetailAST field = frame.findField(name);
412             if (field != null
413                     && (frame.isClassOrEnumOrRecordDef()
414                             || CheckUtil.isBeforeInSource(field, objCalledOn))) {
415                 result = STRING.equals(getFieldType(field));
416                 break;
417             }
418             frame = frame.getParent();
419         }
420         return result;
421     }
422 
423     /**
424      * Whether the field or the variable from THIS instance is of String type.
425      *
426      * @param objCalledOn the field or the variable from THIS instance to check.
427      * @return true if the field or the variable from THIS instance is of String type.
428      */
429     private boolean isStringFieldOrVariableFromThisInstance(DetailAST objCalledOn) {
430         final String name = objCalledOn.getText();
431         final DetailAST field = getObjectFrame(currentFrame).findField(name);
432         return field != null && STRING.equals(getFieldType(field));
433     }
434 
435     /**
436      * Whether the field or the variable from the specified class is of String type.
437      *
438      * @param objCalledOn the field or the variable from the specified class to check.
439      * @param className the name of the class to check in.
440      * @return true if the field or the variable from the specified class is of String type.
441      */
442     private boolean isStringFieldOrVariableFromClass(DetailAST objCalledOn,
443             final String className) {
444         boolean result = false;
445         final String name = objCalledOn.getText();
446         FieldFrame frame = currentFrame;
447         while (frame != null) {
448             if (className.equals(frame.getFrameName())) {
449                 final DetailAST field = frame.findField(name);
450                 result = STRING.equals(getFieldType(field));
451                 break;
452             }
453             frame = frame.getParent();
454         }
455         return result;
456     }
457 
458     /**
459      * Get the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF.
460      *
461      * @param frame to start the search from.
462      * @return the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF.
463      */
464     private static FieldFrame getObjectFrame(FieldFrame frame) {
465         FieldFrame objectFrame = frame;
466         while (!objectFrame.isClassOrEnumOrRecordDef()) {
467             objectFrame = objectFrame.getParent();
468         }
469         return objectFrame;
470     }
471 
472     /**
473      * Get field type.
474      *
475      * @param field to get the type from.
476      * @return type of the field.
477      */
478     private static String getFieldType(DetailAST field) {
479         String fieldType = null;
480         final DetailAST identAst = field.findFirstToken(TokenTypes.TYPE)
481                 .findFirstToken(TokenTypes.IDENT);
482         if (identAst != null) {
483             fieldType = identAst.getText();
484         }
485         return fieldType;
486     }
487 
488     /**
489      * Verify that a token is either CLASS_DEF, RECORD_DEF, or ENUM_DEF.
490      *
491      * @param tokenType the type of token
492      * @return true if token is of specified type.
493      */
494     private static boolean astTypeIsClassOrEnumOrRecordDef(int tokenType) {
495         return tokenType == TokenTypes.CLASS_DEF
496                 || tokenType == TokenTypes.RECORD_DEF
497                 || tokenType == TokenTypes.ENUM_DEF;
498     }
499 
500     /**
501      * Holds the names of fields of a type.
502      */
503     private static final class FieldFrame {
504 
505         /** Parent frame. */
506         private final FieldFrame parent;
507 
508         /** Set of frame's children. */
509         private final Set<FieldFrame> children = new HashSet<>();
510 
511         /** Map of field name to field DetailAst. */
512         private final Map<String, DetailAST> fieldNameToAst = new HashMap<>();
513 
514         /** Set of equals calls. */
515         private final Set<DetailAST> methodCalls = new HashSet<>();
516 
517         /** Name of the class, enum or enum constant declaration. */
518         private String frameName;
519 
520         /** Whether the frame is CLASS_DEF, ENUM_DEF, ENUM_CONST_DEF, or RECORD_DEF. */
521         private boolean classOrEnumOrRecordDef;
522 
523         /**
524          * Creates new frame.
525          *
526          * @param parent parent frame.
527          */
528         private FieldFrame(FieldFrame parent) {
529             this.parent = parent;
530         }
531 
532         /**
533          * Set the frame name.
534          *
535          * @param frameName value to set.
536          */
537         /* package */ void setFrameName(String frameName) {
538             this.frameName = frameName;
539         }
540 
541         /**
542          * Getter for the frame name.
543          *
544          * @return frame name.
545          */
546         /* package */ String getFrameName() {
547             return frameName;
548         }
549 
550         /**
551          * Getter for the parent frame.
552          *
553          * @return parent frame.
554          */
555         /* package */ FieldFrame getParent() {
556             return parent;
557         }
558 
559         /**
560          * Getter for frame's children.
561          *
562          * @return children of this frame.
563          */
564         /* package */ Set<FieldFrame> getChildren() {
565             return Collections.unmodifiableSet(children);
566         }
567 
568         /**
569          * Add child frame to this frame.
570          *
571          * @param child frame to add.
572          */
573         /* package */ void addChild(FieldFrame child) {
574             children.add(child);
575         }
576 
577         /**
578          * Add field to this FieldFrame.
579          *
580          * @param field the ast of the field.
581          */
582         /* package */ void addField(DetailAST field) {
583             if (field.findFirstToken(TokenTypes.IDENT) != null) {
584                 fieldNameToAst.put(getFieldName(field), field);
585             }
586         }
587 
588         /**
589          * Sets isClassOrEnumOrRecordDef.
590          *
591          * @param value value to set.
592          */
593         /* package */ void setClassOrEnumOrRecordDef(boolean value) {
594             classOrEnumOrRecordDef = value;
595         }
596 
597         /**
598          * Getter for classOrEnumOrRecordDef.
599          *
600          * @return classOrEnumOrRecordDef.
601          */
602         /* package */ boolean isClassOrEnumOrRecordDef() {
603             return classOrEnumOrRecordDef;
604         }
605 
606         /**
607          * Add method call to this frame.
608          *
609          * @param methodCall METHOD_CALL ast.
610          */
611         /* package */ void addMethodCall(DetailAST methodCall) {
612             methodCalls.add(methodCall);
613         }
614 
615         /**
616          * Determines whether this FieldFrame contains the field.
617          *
618          * @param name name of the field to check.
619          * @return DetailAST if this FieldFrame contains instance field.
620          */
621         /* package */ DetailAST findField(String name) {
622             return fieldNameToAst.get(name);
623         }
624 
625         /**
626          * Getter for frame's method calls.
627          *
628          * @return method calls of this frame.
629          */
630         /* package */ Set<DetailAST> getMethodCalls() {
631             return Collections.unmodifiableSet(methodCalls);
632         }
633 
634         /**
635          * Get the name of the field.
636          *
637          * @param field to get the name from.
638          * @return name of the field.
639          */
640         private static String getFieldName(DetailAST field) {
641             return TokenUtil.getIdent(field).getText();
642         }
643 
644     }
645 
646 }