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