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      @Override
82      public int[] getDefaultTokens() {
83          return getRequiredTokens();
84      }
85  
86      @Override
87      public int[] getAcceptableTokens() {
88          return getRequiredTokens();
89      }
90  
91      @Override
92      public int[] getRequiredTokens() {
93          return new int[] {
94              TokenTypes.METHOD_CALL,
95              TokenTypes.CLASS_DEF,
96              TokenTypes.METHOD_DEF,
97              TokenTypes.LITERAL_FOR,
98              TokenTypes.LITERAL_CATCH,
99              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 }