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