1   
2   
3   
4   
5   
6   
7   
8   
9   
10  
11  
12  
13  
14  
15  
16  
17  
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  
36  
37  
38  
39  
40  
41  
42  
43  
44  
45  
46  
47  
48  
49  
50  @FileStatefulCheck
51  public class EqualsAvoidNullCheck extends AbstractCheck {
52  
53      
54  
55  
56  
57      public static final String MSG_EQUALS_AVOID_NULL = "equals.avoid.null";
58  
59      
60  
61  
62  
63      public static final String MSG_EQUALS_IGNORE_CASE_AVOID_NULL = "equalsIgnoreCase.avoid.null";
64  
65      
66      private static final String EQUALS = "equals";
67  
68      
69      private static final String STRING = "String";
70  
71      
72      private static final String LEFT_CURLY = "{";
73  
74      
75      private boolean ignoreEqualsIgnoreCase;
76  
77      
78      private FieldFrame currentFrame;
79  
80      @Override
81      public int[] getDefaultTokens() {
82          return getRequiredTokens();
83      }
84  
85      @Override
86      public int[] getAcceptableTokens() {
87          return getRequiredTokens();
88      }
89  
90      @Override
91      public int[] getRequiredTokens() {
92          return new int[] {
93              TokenTypes.METHOD_CALL,
94              TokenTypes.CLASS_DEF,
95              TokenTypes.METHOD_DEF,
96              TokenTypes.LITERAL_FOR,
97              TokenTypes.LITERAL_CATCH,
98              TokenTypes.LITERAL_TRY,
99              TokenTypes.LITERAL_SWITCH,
100             TokenTypes.VARIABLE_DEF,
101             TokenTypes.PARAMETER_DEF,
102             TokenTypes.CTOR_DEF,
103             TokenTypes.SLIST,
104             TokenTypes.OBJBLOCK,
105             TokenTypes.ENUM_DEF,
106             TokenTypes.ENUM_CONSTANT_DEF,
107             TokenTypes.LITERAL_NEW,
108             TokenTypes.LAMBDA,
109             TokenTypes.PATTERN_VARIABLE_DEF,
110             TokenTypes.RECORD_DEF,
111             TokenTypes.COMPACT_CTOR_DEF,
112             TokenTypes.RECORD_COMPONENT_DEF,
113         };
114     }
115 
116     
117 
118 
119 
120 
121 
122 
123     public void setIgnoreEqualsIgnoreCase(boolean newValue) {
124         ignoreEqualsIgnoreCase = newValue;
125     }
126 
127     @Override
128     public void beginTree(DetailAST rootAST) {
129         currentFrame = new FieldFrame(null);
130     }
131 
132     @Override
133     public void visitToken(final DetailAST ast) {
134         switch (ast.getType()) {
135             case TokenTypes.VARIABLE_DEF,
136                  TokenTypes.PARAMETER_DEF,
137                  TokenTypes.PATTERN_VARIABLE_DEF,
138                  TokenTypes.RECORD_COMPONENT_DEF -> currentFrame.addField(ast);
139 
140             case TokenTypes.METHOD_CALL -> processMethodCall(ast);
141 
142             case TokenTypes.SLIST -> processSlist(ast);
143 
144             case TokenTypes.LITERAL_NEW -> processLiteralNew(ast);
145 
146             case TokenTypes.OBJBLOCK -> {
147                 final int parentType = ast.getParent().getType();
148                 if (!astTypeIsClassOrEnumOrRecordDef(parentType)) {
149                     processFrame(ast);
150                 }
151             }
152 
153             default -> processFrame(ast);
154         }
155     }
156 
157     @Override
158     public void leaveToken(DetailAST ast) {
159         switch (ast.getType()) {
160             case TokenTypes.SLIST -> leaveSlist(ast);
161 
162             case TokenTypes.LITERAL_NEW -> leaveLiteralNew(ast);
163 
164             case TokenTypes.OBJBLOCK -> {
165                 final int parentType = ast.getParent().getType();
166                 if (!astTypeIsClassOrEnumOrRecordDef(parentType)) {
167                     currentFrame = currentFrame.getParent();
168                 }
169             }
170 
171             case TokenTypes.VARIABLE_DEF,
172                  TokenTypes.PARAMETER_DEF,
173                  TokenTypes.RECORD_COMPONENT_DEF,
174                  TokenTypes.METHOD_CALL,
175                  TokenTypes.PATTERN_VARIABLE_DEF -> {
176                 
177             }
178 
179             default -> currentFrame = currentFrame.getParent();
180         }
181     }
182 
183     @Override
184     public void finishTree(DetailAST ast) {
185         traverseFieldFrameTree(currentFrame);
186     }
187 
188     
189 
190 
191 
192 
193 
194     private void processSlist(DetailAST ast) {
195         if (LEFT_CURLY.equals(ast.getText())) {
196             final FieldFrame frame = new FieldFrame(currentFrame);
197             currentFrame.addChild(frame);
198             currentFrame = frame;
199         }
200     }
201 
202     
203 
204 
205 
206 
207     private void leaveSlist(DetailAST ast) {
208         if (LEFT_CURLY.equals(ast.getText())) {
209             currentFrame = currentFrame.getParent();
210         }
211     }
212 
213     
214 
215 
216 
217 
218 
219     private void processFrame(DetailAST ast) {
220         final FieldFrame frame = new FieldFrame(currentFrame);
221         final int astType = ast.getType();
222         if (astTypeIsClassOrEnumOrRecordDef(astType)) {
223             frame.setClassOrEnumOrRecordDef(true);
224             frame.setFrameName(ast.findFirstToken(TokenTypes.IDENT).getText());
225         }
226         currentFrame.addChild(frame);
227         currentFrame = frame;
228     }
229 
230     
231 
232 
233 
234 
235     private void processMethodCall(DetailAST methodCall) {
236         final DetailAST dot = methodCall.getFirstChild();
237         if (dot.getType() == TokenTypes.DOT) {
238             final String methodName = dot.getLastChild().getText();
239             if (EQUALS.equals(methodName)
240                     || !ignoreEqualsIgnoreCase && "equalsIgnoreCase".equals(methodName)) {
241                 currentFrame.addMethodCall(methodCall);
242             }
243         }
244     }
245 
246     
247 
248 
249 
250 
251 
252     private void processLiteralNew(DetailAST ast) {
253         if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
254             final FieldFrame frame = new FieldFrame(currentFrame);
255             currentFrame.addChild(frame);
256             currentFrame = frame;
257         }
258     }
259 
260     
261 
262 
263 
264 
265 
266     private void leaveLiteralNew(DetailAST ast) {
267         if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
268             currentFrame = currentFrame.getParent();
269         }
270     }
271 
272     
273 
274 
275 
276 
277     private void traverseFieldFrameTree(FieldFrame frame) {
278         for (FieldFrame child: frame.getChildren()) {
279             traverseFieldFrameTree(child);
280 
281             currentFrame = child;
282             child.getMethodCalls().forEach(this::checkMethodCall);
283         }
284     }
285 
286     
287 
288 
289 
290 
291     private void checkMethodCall(DetailAST methodCall) {
292         DetailAST objCalledOn = methodCall.getFirstChild().getFirstChild();
293         if (objCalledOn.getType() == TokenTypes.DOT) {
294             objCalledOn = objCalledOn.getLastChild();
295         }
296         final DetailAST expr = methodCall.findFirstToken(TokenTypes.ELIST).getFirstChild();
297         if (containsOneArgument(methodCall)
298                 && containsAllSafeTokens(expr)
299                 && isCalledOnStringFieldOrVariable(objCalledOn)) {
300             final String methodName = methodCall.getFirstChild().getLastChild().getText();
301             if (EQUALS.equals(methodName)) {
302                 log(methodCall, MSG_EQUALS_AVOID_NULL);
303             }
304             else {
305                 log(methodCall, MSG_EQUALS_IGNORE_CASE_AVOID_NULL);
306             }
307         }
308     }
309 
310     
311 
312 
313 
314 
315 
316     private static boolean containsOneArgument(DetailAST methodCall) {
317         final DetailAST elist = methodCall.findFirstToken(TokenTypes.ELIST);
318         return elist.getChildCount() == 1;
319     }
320 
321     
322 
323 
324 
325 
326 
327 
328     private static boolean containsAllSafeTokens(final DetailAST expr) {
329         DetailAST arg = expr.getFirstChild();
330         arg = skipVariableAssign(arg);
331 
332         boolean argIsNotNull = false;
333         if (arg.getType() == TokenTypes.PLUS) {
334             DetailAST child = arg.getFirstChild();
335             while (child != null
336                     && !argIsNotNull) {
337                 argIsNotNull = child.getType() == TokenTypes.STRING_LITERAL
338                         || child.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN
339                         || child.getType() == TokenTypes.IDENT;
340                 child = child.getNextSibling();
341             }
342         }
343         else {
344             argIsNotNull = arg.getType() == TokenTypes.STRING_LITERAL
345                     || arg.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN;
346         }
347 
348         return argIsNotNull;
349     }
350 
351     
352 
353 
354 
355 
356 
357     private static DetailAST skipVariableAssign(final DetailAST currentAST) {
358         DetailAST result = currentAST;
359         while (result.getType() == TokenTypes.LPAREN) {
360             result = result.getNextSibling();
361         }
362         if (result.getType() == TokenTypes.ASSIGN) {
363             result = result.getFirstChild().getNextSibling();
364         }
365         return result;
366     }
367 
368     
369 
370 
371 
372 
373 
374     private boolean isCalledOnStringFieldOrVariable(DetailAST objCalledOn) {
375         final boolean result;
376         final DetailAST previousSiblingAst = objCalledOn.getPreviousSibling();
377         if (previousSiblingAst == null) {
378             result = isStringFieldOrVariable(objCalledOn);
379         }
380         else {
381             if (previousSiblingAst.getType() == TokenTypes.LITERAL_THIS) {
382                 result = isStringFieldOrVariableFromThisInstance(objCalledOn);
383             }
384             else {
385                 final String className = previousSiblingAst.getText();
386                 result = isStringFieldOrVariableFromClass(objCalledOn, className);
387             }
388         }
389         return result;
390     }
391 
392     
393 
394 
395 
396 
397 
398     private boolean isStringFieldOrVariable(DetailAST objCalledOn) {
399         boolean result = false;
400         final String name = objCalledOn.getText();
401         FieldFrame frame = currentFrame;
402         while (frame != null) {
403             final DetailAST field = frame.findField(name);
404             if (field != null
405                     && (frame.isClassOrEnumOrRecordDef()
406                             || CheckUtil.isBeforeInSource(field, objCalledOn))) {
407                 result = STRING.equals(getFieldType(field));
408                 break;
409             }
410             frame = frame.getParent();
411         }
412         return result;
413     }
414 
415     
416 
417 
418 
419 
420 
421     private boolean isStringFieldOrVariableFromThisInstance(DetailAST objCalledOn) {
422         final String name = objCalledOn.getText();
423         final DetailAST field = getObjectFrame(currentFrame).findField(name);
424         return field != null && STRING.equals(getFieldType(field));
425     }
426 
427     
428 
429 
430 
431 
432 
433 
434     private boolean isStringFieldOrVariableFromClass(DetailAST objCalledOn,
435             final String className) {
436         boolean result = false;
437         final String name = objCalledOn.getText();
438         FieldFrame frame = currentFrame;
439         while (frame != null) {
440             if (className.equals(frame.getFrameName())) {
441                 final DetailAST field = frame.findField(name);
442                 result = STRING.equals(getFieldType(field));
443                 break;
444             }
445             frame = frame.getParent();
446         }
447         return result;
448     }
449 
450     
451 
452 
453 
454 
455 
456     private static FieldFrame getObjectFrame(FieldFrame frame) {
457         FieldFrame objectFrame = frame;
458         while (!objectFrame.isClassOrEnumOrRecordDef()) {
459             objectFrame = objectFrame.getParent();
460         }
461         return objectFrame;
462     }
463 
464     
465 
466 
467 
468 
469 
470     private static String getFieldType(DetailAST field) {
471         String fieldType = null;
472         final DetailAST identAst = field.findFirstToken(TokenTypes.TYPE)
473                 .findFirstToken(TokenTypes.IDENT);
474         if (identAst != null) {
475             fieldType = identAst.getText();
476         }
477         return fieldType;
478     }
479 
480     
481 
482 
483 
484 
485 
486     private static boolean astTypeIsClassOrEnumOrRecordDef(int tokenType) {
487         return tokenType == TokenTypes.CLASS_DEF
488                 || tokenType == TokenTypes.RECORD_DEF
489                 || tokenType == TokenTypes.ENUM_DEF;
490     }
491 
492     
493 
494 
495     private static final class FieldFrame {
496 
497         
498         private final FieldFrame parent;
499 
500         
501         private final Set<FieldFrame> children = new HashSet<>();
502 
503         
504         private final Map<String, DetailAST> fieldNameToAst = new HashMap<>();
505 
506         
507         private final Set<DetailAST> methodCalls = new HashSet<>();
508 
509         
510         private String frameName;
511 
512         
513         private boolean classOrEnumOrRecordDef;
514 
515         
516 
517 
518 
519 
520         private FieldFrame(FieldFrame parent) {
521             this.parent = parent;
522         }
523 
524         
525 
526 
527 
528 
529         public void setFrameName(String frameName) {
530             this.frameName = frameName;
531         }
532 
533         
534 
535 
536 
537 
538         public String getFrameName() {
539             return frameName;
540         }
541 
542         
543 
544 
545 
546 
547         public FieldFrame getParent() {
548             return parent;
549         }
550 
551         
552 
553 
554 
555 
556         public Set<FieldFrame> getChildren() {
557             return Collections.unmodifiableSet(children);
558         }
559 
560         
561 
562 
563 
564 
565         public void addChild(FieldFrame child) {
566             children.add(child);
567         }
568 
569         
570 
571 
572 
573 
574         public void addField(DetailAST field) {
575             if (field.findFirstToken(TokenTypes.IDENT) != null) {
576                 fieldNameToAst.put(getFieldName(field), field);
577             }
578         }
579 
580         
581 
582 
583 
584 
585         public void setClassOrEnumOrRecordDef(boolean value) {
586             classOrEnumOrRecordDef = value;
587         }
588 
589         
590 
591 
592 
593 
594         public boolean isClassOrEnumOrRecordDef() {
595             return classOrEnumOrRecordDef;
596         }
597 
598         
599 
600 
601 
602 
603         public void addMethodCall(DetailAST methodCall) {
604             methodCalls.add(methodCall);
605         }
606 
607         
608 
609 
610 
611 
612 
613         public DetailAST findField(String name) {
614             return fieldNameToAst.get(name);
615         }
616 
617         
618 
619 
620 
621 
622         public Set<DetailAST> getMethodCalls() {
623             return Collections.unmodifiableSet(methodCalls);
624         }
625 
626         
627 
628 
629 
630 
631 
632         private static String getFieldName(DetailAST field) {
633             return field.findFirstToken(TokenTypes.IDENT).getText();
634         }
635 
636     }
637 
638 }