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 }