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