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