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