View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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  
34  /**
35   * <div>
36   * Checks that any combination of String literals
37   * is on the left side of an {@code equals()} comparison.
38   * Also checks for String literals assigned to some field
39   * (such as {@code someString.equals(anotherString = "text")}).
40   * </div>
41   *
42   * <p>Rationale: Calling the {@code equals()} method on String literals
43   * will avoid a potential {@code NullPointerException}. Also, it is
44   * pretty common to see null checks right before equals comparisons
45   * but following this rule such checks are not required.
46   * </p>
47   *
48   * @since 5.0
49   */
50  @FileStatefulCheck
51  public class EqualsAvoidNullCheck extends AbstractCheck {
52  
53      /**
54       * A key is pointing to the warning message text in "messages.properties"
55       * file.
56       */
57      public static final String MSG_EQUALS_AVOID_NULL = "equals.avoid.null";
58  
59      /**
60       * A key is pointing to the warning message text in "messages.properties"
61       * file.
62       */
63      public static final String MSG_EQUALS_IGNORE_CASE_AVOID_NULL = "equalsIgnoreCase.avoid.null";
64  
65      /** Method name for comparison. */
66      private static final String EQUALS = "equals";
67  
68      /** Type name for comparison. */
69      private static final String STRING = "String";
70  
71      /** Curly for comparison. */
72      private static final String LEFT_CURLY = "{";
73  
74      /** Control whether to ignore {@code String.equalsIgnoreCase(String)} invocations. */
75      private boolean ignoreEqualsIgnoreCase;
76  
77      /** Stack of sets of field names, one for each class of a set of nested classes. */
78      private FieldFrame currentFrame;
79  
80      @Override
81      public int[] getDefaultTokens() {
82          return getRequiredTokens();
83      }
84  
85      @Override
86      public int[] getAcceptableTokens() {
87          return getRequiredTokens();
88      }
89  
90      @Override
91      public int[] getRequiredTokens() {
92          return new int[] {
93              TokenTypes.METHOD_CALL,
94              TokenTypes.CLASS_DEF,
95              TokenTypes.METHOD_DEF,
96              TokenTypes.LITERAL_FOR,
97              TokenTypes.LITERAL_CATCH,
98              TokenTypes.LITERAL_TRY,
99              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 }