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   * <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                  TokenTypes.PARAMETER_DEF,
161                  TokenTypes.PATTERN_VARIABLE_DEF,
162                  TokenTypes.RECORD_COMPONENT_DEF -> currentFrame.addField(ast);
163 
164             case TokenTypes.METHOD_CALL -> processMethodCall(ast);
165 
166             case TokenTypes.SLIST -> processSlist(ast);
167 
168             case TokenTypes.LITERAL_NEW -> processLiteralNew(ast);
169 
170             case TokenTypes.OBJBLOCK -> {
171                 final int parentType = ast.getParent().getType();
172                 if (!astTypeIsClassOrEnumOrRecordDef(parentType)) {
173                     processFrame(ast);
174                 }
175             }
176 
177             default -> processFrame(ast);
178         }
179     }
180 
181     @Override
182     public void leaveToken(DetailAST ast) {
183         switch (ast.getType()) {
184             case TokenTypes.SLIST -> leaveSlist(ast);
185 
186             case TokenTypes.LITERAL_NEW -> leaveLiteralNew(ast);
187 
188             case TokenTypes.OBJBLOCK -> {
189                 final int parentType = ast.getParent().getType();
190                 if (!astTypeIsClassOrEnumOrRecordDef(parentType)) {
191                     currentFrame = currentFrame.getParent();
192                 }
193             }
194 
195             case TokenTypes.VARIABLE_DEF,
196                  TokenTypes.PARAMETER_DEF,
197                  TokenTypes.RECORD_COMPONENT_DEF,
198                  TokenTypes.METHOD_CALL,
199                  TokenTypes.PATTERN_VARIABLE_DEF -> {
200                 // intentionally do nothing
201             }
202 
203             default -> currentFrame = currentFrame.getParent();
204         }
205     }
206 
207     @Override
208     public void finishTree(DetailAST ast) {
209         traverseFieldFrameTree(currentFrame);
210     }
211 
212     /**
213      * Determine whether SLIST begins a block, determined by braces, and add it as
214      * a frame in this case.
215      *
216      * @param ast SLIST ast.
217      */
218     private void processSlist(DetailAST ast) {
219         if (LEFT_CURLY.equals(ast.getText())) {
220             final FieldFrame frame = new FieldFrame(currentFrame);
221             currentFrame.addChild(frame);
222             currentFrame = frame;
223         }
224     }
225 
226     /**
227      * Determine whether SLIST begins a block, determined by braces.
228      *
229      * @param ast SLIST ast.
230      */
231     private void leaveSlist(DetailAST ast) {
232         if (LEFT_CURLY.equals(ast.getText())) {
233             currentFrame = currentFrame.getParent();
234         }
235     }
236 
237     /**
238      * Process CLASS_DEF, METHOD_DEF, LITERAL_IF, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO,
239      * LITERAL_CATCH, LITERAL_TRY, CTOR_DEF, ENUM_DEF, ENUM_CONSTANT_DEF.
240      *
241      * @param ast processed ast.
242      */
243     private void processFrame(DetailAST ast) {
244         final FieldFrame frame = new FieldFrame(currentFrame);
245         final int astType = ast.getType();
246         if (astTypeIsClassOrEnumOrRecordDef(astType)) {
247             frame.setClassOrEnumOrRecordDef(true);
248             frame.setFrameName(ast.findFirstToken(TokenTypes.IDENT).getText());
249         }
250         currentFrame.addChild(frame);
251         currentFrame = frame;
252     }
253 
254     /**
255      * Add the method call to the current frame if it should be processed.
256      *
257      * @param methodCall METHOD_CALL ast.
258      */
259     private void processMethodCall(DetailAST methodCall) {
260         final DetailAST dot = methodCall.getFirstChild();
261         if (dot.getType() == TokenTypes.DOT) {
262             final String methodName = dot.getLastChild().getText();
263             if (EQUALS.equals(methodName)
264                     || !ignoreEqualsIgnoreCase && "equalsIgnoreCase".equals(methodName)) {
265                 currentFrame.addMethodCall(methodCall);
266             }
267         }
268     }
269 
270     /**
271      * Determine whether LITERAL_NEW is an anonymous class definition and add it as
272      * a frame in this case.
273      *
274      * @param ast LITERAL_NEW ast.
275      */
276     private void processLiteralNew(DetailAST ast) {
277         if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
278             final FieldFrame frame = new FieldFrame(currentFrame);
279             currentFrame.addChild(frame);
280             currentFrame = frame;
281         }
282     }
283 
284     /**
285      * Determine whether LITERAL_NEW is an anonymous class definition and leave
286      * the frame it is in.
287      *
288      * @param ast LITERAL_NEW ast.
289      */
290     private void leaveLiteralNew(DetailAST ast) {
291         if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
292             currentFrame = currentFrame.getParent();
293         }
294     }
295 
296     /**
297      * Traverse the tree of the field frames to check all equals method calls.
298      *
299      * @param frame to check method calls in.
300      */
301     private void traverseFieldFrameTree(FieldFrame frame) {
302         for (FieldFrame child: frame.getChildren()) {
303             traverseFieldFrameTree(child);
304 
305             currentFrame = child;
306             child.getMethodCalls().forEach(this::checkMethodCall);
307         }
308     }
309 
310     /**
311      * Check whether the method call should be violated.
312      *
313      * @param methodCall method call to check.
314      */
315     private void checkMethodCall(DetailAST methodCall) {
316         DetailAST objCalledOn = methodCall.getFirstChild().getFirstChild();
317         if (objCalledOn.getType() == TokenTypes.DOT) {
318             objCalledOn = objCalledOn.getLastChild();
319         }
320         final DetailAST expr = methodCall.findFirstToken(TokenTypes.ELIST).getFirstChild();
321         if (containsOneArgument(methodCall)
322                 && containsAllSafeTokens(expr)
323                 && isCalledOnStringFieldOrVariable(objCalledOn)) {
324             final String methodName = methodCall.getFirstChild().getLastChild().getText();
325             if (EQUALS.equals(methodName)) {
326                 log(methodCall, MSG_EQUALS_AVOID_NULL);
327             }
328             else {
329                 log(methodCall, MSG_EQUALS_IGNORE_CASE_AVOID_NULL);
330             }
331         }
332     }
333 
334     /**
335      * Verify that method call has one argument.
336      *
337      * @param methodCall METHOD_CALL DetailAST
338      * @return true if method call has one argument.
339      */
340     private static boolean containsOneArgument(DetailAST methodCall) {
341         final DetailAST elist = methodCall.findFirstToken(TokenTypes.ELIST);
342         return elist.getChildCount() == 1;
343     }
344 
345     /**
346      * Looks for all "safe" Token combinations in the argument
347      * expression branch.
348      *
349      * @param expr the argument expression
350      * @return - true if any child matches the set of tokens, false if not
351      */
352     private static boolean containsAllSafeTokens(final DetailAST expr) {
353         DetailAST arg = expr.getFirstChild();
354         arg = skipVariableAssign(arg);
355 
356         boolean argIsNotNull = false;
357         if (arg.getType() == TokenTypes.PLUS) {
358             DetailAST child = arg.getFirstChild();
359             while (child != null
360                     && !argIsNotNull) {
361                 argIsNotNull = child.getType() == TokenTypes.STRING_LITERAL
362                         || child.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN
363                         || child.getType() == TokenTypes.IDENT;
364                 child = child.getNextSibling();
365             }
366         }
367         else {
368             argIsNotNull = arg.getType() == TokenTypes.STRING_LITERAL
369                     || arg.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN;
370         }
371 
372         return argIsNotNull;
373     }
374 
375     /**
376      * Skips over an inner assign portion of an argument expression.
377      *
378      * @param currentAST current token in the argument expression
379      * @return the next relevant token
380      */
381     private static DetailAST skipVariableAssign(final DetailAST currentAST) {
382         DetailAST result = currentAST;
383         while (result.getType() == TokenTypes.LPAREN) {
384             result = result.getNextSibling();
385         }
386         if (result.getType() == TokenTypes.ASSIGN) {
387             result = result.getFirstChild().getNextSibling();
388         }
389         return result;
390     }
391 
392     /**
393      * Determine, whether equals method is called on a field of String type.
394      *
395      * @param objCalledOn object ast.
396      * @return true if the object is of String type.
397      */
398     private boolean isCalledOnStringFieldOrVariable(DetailAST objCalledOn) {
399         final boolean result;
400         final DetailAST previousSiblingAst = objCalledOn.getPreviousSibling();
401         if (previousSiblingAst == null) {
402             result = isStringFieldOrVariable(objCalledOn);
403         }
404         else {
405             if (previousSiblingAst.getType() == TokenTypes.LITERAL_THIS) {
406                 result = isStringFieldOrVariableFromThisInstance(objCalledOn);
407             }
408             else {
409                 final String className = previousSiblingAst.getText();
410                 result = isStringFieldOrVariableFromClass(objCalledOn, className);
411             }
412         }
413         return result;
414     }
415 
416     /**
417      * Whether the field or the variable is of String type.
418      *
419      * @param objCalledOn the field or the variable to check.
420      * @return true if the field or the variable is of String type.
421      */
422     private boolean isStringFieldOrVariable(DetailAST objCalledOn) {
423         boolean result = false;
424         final String name = objCalledOn.getText();
425         FieldFrame frame = currentFrame;
426         while (frame != null) {
427             final DetailAST field = frame.findField(name);
428             if (field != null
429                     && (frame.isClassOrEnumOrRecordDef()
430                             || CheckUtil.isBeforeInSource(field, objCalledOn))) {
431                 result = STRING.equals(getFieldType(field));
432                 break;
433             }
434             frame = frame.getParent();
435         }
436         return result;
437     }
438 
439     /**
440      * Whether the field or the variable from THIS instance is of String type.
441      *
442      * @param objCalledOn the field or the variable from THIS instance to check.
443      * @return true if the field or the variable from THIS instance is of String type.
444      */
445     private boolean isStringFieldOrVariableFromThisInstance(DetailAST objCalledOn) {
446         final String name = objCalledOn.getText();
447         final DetailAST field = getObjectFrame(currentFrame).findField(name);
448         return field != null && STRING.equals(getFieldType(field));
449     }
450 
451     /**
452      * Whether the field or the variable from the specified class is of String type.
453      *
454      * @param objCalledOn the field or the variable from the specified class to check.
455      * @param className the name of the class to check in.
456      * @return true if the field or the variable from the specified class is of String type.
457      */
458     private boolean isStringFieldOrVariableFromClass(DetailAST objCalledOn,
459             final String className) {
460         boolean result = false;
461         final String name = objCalledOn.getText();
462         FieldFrame frame = currentFrame;
463         while (frame != null) {
464             if (className.equals(frame.getFrameName())) {
465                 final DetailAST field = frame.findField(name);
466                 result = STRING.equals(getFieldType(field));
467                 break;
468             }
469             frame = frame.getParent();
470         }
471         return result;
472     }
473 
474     /**
475      * Get the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF.
476      *
477      * @param frame to start the search from.
478      * @return the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF.
479      */
480     private static FieldFrame getObjectFrame(FieldFrame frame) {
481         FieldFrame objectFrame = frame;
482         while (!objectFrame.isClassOrEnumOrRecordDef()) {
483             objectFrame = objectFrame.getParent();
484         }
485         return objectFrame;
486     }
487 
488     /**
489      * Get field type.
490      *
491      * @param field to get the type from.
492      * @return type of the field.
493      */
494     private static String getFieldType(DetailAST field) {
495         String fieldType = null;
496         final DetailAST identAst = field.findFirstToken(TokenTypes.TYPE)
497                 .findFirstToken(TokenTypes.IDENT);
498         if (identAst != null) {
499             fieldType = identAst.getText();
500         }
501         return fieldType;
502     }
503 
504     /**
505      * Verify that a token is either CLASS_DEF, RECORD_DEF, or ENUM_DEF.
506      *
507      * @param tokenType the type of token
508      * @return true if token is of specified type.
509      */
510     private static boolean astTypeIsClassOrEnumOrRecordDef(int tokenType) {
511         return tokenType == TokenTypes.CLASS_DEF
512                 || tokenType == TokenTypes.RECORD_DEF
513                 || tokenType == TokenTypes.ENUM_DEF;
514     }
515 
516     /**
517      * Holds the names of fields of a type.
518      */
519     private static final class FieldFrame {
520 
521         /** Parent frame. */
522         private final FieldFrame parent;
523 
524         /** Set of frame's children. */
525         private final Set<FieldFrame> children = new HashSet<>();
526 
527         /** Map of field name to field DetailAst. */
528         private final Map<String, DetailAST> fieldNameToAst = new HashMap<>();
529 
530         /** Set of equals calls. */
531         private final Set<DetailAST> methodCalls = new HashSet<>();
532 
533         /** Name of the class, enum or enum constant declaration. */
534         private String frameName;
535 
536         /** Whether the frame is CLASS_DEF, ENUM_DEF, ENUM_CONST_DEF, or RECORD_DEF. */
537         private boolean classOrEnumOrRecordDef;
538 
539         /**
540          * Creates new frame.
541          *
542          * @param parent parent frame.
543          */
544         private FieldFrame(FieldFrame parent) {
545             this.parent = parent;
546         }
547 
548         /**
549          * Set the frame name.
550          *
551          * @param frameName value to set.
552          */
553         public void setFrameName(String frameName) {
554             this.frameName = frameName;
555         }
556 
557         /**
558          * Getter for the frame name.
559          *
560          * @return frame name.
561          */
562         public String getFrameName() {
563             return frameName;
564         }
565 
566         /**
567          * Getter for the parent frame.
568          *
569          * @return parent frame.
570          */
571         public FieldFrame getParent() {
572             return parent;
573         }
574 
575         /**
576          * Getter for frame's children.
577          *
578          * @return children of this frame.
579          */
580         public Set<FieldFrame> getChildren() {
581             return Collections.unmodifiableSet(children);
582         }
583 
584         /**
585          * Add child frame to this frame.
586          *
587          * @param child frame to add.
588          */
589         public void addChild(FieldFrame child) {
590             children.add(child);
591         }
592 
593         /**
594          * Add field to this FieldFrame.
595          *
596          * @param field the ast of the field.
597          */
598         public void addField(DetailAST field) {
599             if (field.findFirstToken(TokenTypes.IDENT) != null) {
600                 fieldNameToAst.put(getFieldName(field), field);
601             }
602         }
603 
604         /**
605          * Sets isClassOrEnumOrRecordDef.
606          *
607          * @param value value to set.
608          */
609         public void setClassOrEnumOrRecordDef(boolean value) {
610             classOrEnumOrRecordDef = value;
611         }
612 
613         /**
614          * Getter for classOrEnumOrRecordDef.
615          *
616          * @return classOrEnumOrRecordDef.
617          */
618         public boolean isClassOrEnumOrRecordDef() {
619             return classOrEnumOrRecordDef;
620         }
621 
622         /**
623          * Add method call to this frame.
624          *
625          * @param methodCall METHOD_CALL ast.
626          */
627         public void addMethodCall(DetailAST methodCall) {
628             methodCalls.add(methodCall);
629         }
630 
631         /**
632          * Determines whether this FieldFrame contains the field.
633          *
634          * @param name name of the field to check.
635          * @return DetailAST if this FieldFrame contains instance field.
636          */
637         public DetailAST findField(String name) {
638             return fieldNameToAst.get(name);
639         }
640 
641         /**
642          * Getter for frame's method calls.
643          *
644          * @return method calls of this frame.
645          */
646         public Set<DetailAST> getMethodCalls() {
647             return Collections.unmodifiableSet(methodCalls);
648         }
649 
650         /**
651          * Get the name of the field.
652          *
653          * @param field to get the name from.
654          * @return name of the field.
655          */
656         private static String getFieldName(DetailAST field) {
657             return field.findFirstToken(TokenTypes.IDENT).getText();
658         }
659 
660     }
661 
662 }