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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71 @FileStatefulCheck
72 public class EqualsAvoidNullCheck extends AbstractCheck {
73
74
75
76
77
78 public static final String MSG_EQUALS_AVOID_NULL = "equals.avoid.null";
79
80
81
82
83
84 public static final String MSG_EQUALS_IGNORE_CASE_AVOID_NULL = "equalsIgnoreCase.avoid.null";
85
86
87 private static final String EQUALS = "equals";
88
89
90 private static final String STRING = "String";
91
92
93 private static final String LEFT_CURLY = "{";
94
95
96 private boolean ignoreEqualsIgnoreCase;
97
98
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
139
140
141
142
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
216
217
218
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
230
231
232
233 private void leaveSlist(DetailAST ast) {
234 if (LEFT_CURLY.equals(ast.getText())) {
235 currentFrame = currentFrame.getParent();
236 }
237 }
238
239
240
241
242
243
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
258
259
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
274
275
276
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
288
289
290
291
292 private void leaveLiteralNew(DetailAST ast) {
293 if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
294 currentFrame = currentFrame.getParent();
295 }
296 }
297
298
299
300
301
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
314
315
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
338
339
340
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
349
350
351
352
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
379
380
381
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
396
397
398
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
420
421
422
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
443
444
445
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
455
456
457
458
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
478
479
480
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
492
493
494
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
508
509
510
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
520
521 private static final class FieldFrame {
522
523
524 private final FieldFrame parent;
525
526
527 private final Set<FieldFrame> children = new HashSet<>();
528
529
530 private final Map<String, DetailAST> fieldNameToAst = new HashMap<>();
531
532
533 private final Set<DetailAST> methodCalls = new HashSet<>();
534
535
536 private String frameName;
537
538
539 private boolean classOrEnumOrRecordDef;
540
541
542
543
544
545
546 private FieldFrame(FieldFrame parent) {
547 this.parent = parent;
548 }
549
550
551
552
553
554
555 public void setFrameName(String frameName) {
556 this.frameName = frameName;
557 }
558
559
560
561
562
563
564 public String getFrameName() {
565 return frameName;
566 }
567
568
569
570
571
572
573 public FieldFrame getParent() {
574 return parent;
575 }
576
577
578
579
580
581
582 public Set<FieldFrame> getChildren() {
583 return Collections.unmodifiableSet(children);
584 }
585
586
587
588
589
590
591 public void addChild(FieldFrame child) {
592 children.add(child);
593 }
594
595
596
597
598
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
608
609
610
611 public void setClassOrEnumOrRecordDef(boolean value) {
612 classOrEnumOrRecordDef = value;
613 }
614
615
616
617
618
619
620 public boolean isClassOrEnumOrRecordDef() {
621 return classOrEnumOrRecordDef;
622 }
623
624
625
626
627
628
629 public void addMethodCall(DetailAST methodCall) {
630 methodCalls.add(methodCall);
631 }
632
633
634
635
636
637
638
639 public DetailAST findField(String name) {
640 return fieldNameToAst.get(name);
641 }
642
643
644
645
646
647
648 public Set<DetailAST> getMethodCalls() {
649 return Collections.unmodifiableSet(methodCalls);
650 }
651
652
653
654
655
656
657
658 private static String getFieldName(DetailAST field) {
659 return field.findFirstToken(TokenTypes.IDENT).getText();
660 }
661
662 }
663
664 }