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