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