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.ArrayDeque;
23 import java.util.BitSet;
24 import java.util.Deque;
25 import java.util.HashMap;
26 import java.util.Iterator;
27 import java.util.Map;
28 import java.util.Optional;
29
30 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
31 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
32 import com.puppycrawl.tools.checkstyle.api.DetailAST;
33 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
34 import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
35 import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
36 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
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
75
76
77
78 @FileStatefulCheck
79 public class FinalLocalVariableCheck extends AbstractCheck {
80
81
82
83
84
85 public static final String MSG_KEY = "final.variable";
86
87
88
89
90 private static final BitSet ASSIGN_OPERATOR_TYPES = TokenUtil.asBitSet(
91 TokenTypes.POST_INC,
92 TokenTypes.POST_DEC,
93 TokenTypes.ASSIGN,
94 TokenTypes.PLUS_ASSIGN,
95 TokenTypes.MINUS_ASSIGN,
96 TokenTypes.STAR_ASSIGN,
97 TokenTypes.DIV_ASSIGN,
98 TokenTypes.MOD_ASSIGN,
99 TokenTypes.SR_ASSIGN,
100 TokenTypes.BSR_ASSIGN,
101 TokenTypes.SL_ASSIGN,
102 TokenTypes.BAND_ASSIGN,
103 TokenTypes.BXOR_ASSIGN,
104 TokenTypes.BOR_ASSIGN,
105 TokenTypes.INC,
106 TokenTypes.DEC
107 );
108
109
110
111
112 private static final BitSet LOOP_TYPES = TokenUtil.asBitSet(
113 TokenTypes.LITERAL_FOR,
114 TokenTypes.LITERAL_WHILE,
115 TokenTypes.LITERAL_DO
116 );
117
118
119 private final Deque<ScopeData> scopeStack = new ArrayDeque<>();
120
121
122 private final Deque<Deque<DetailAST>> currentScopeAssignedVariables =
123 new ArrayDeque<>();
124
125
126
127
128
129
130 private boolean validateEnhancedForLoopVariable;
131
132
133
134
135
136
137
138
139
140 public final void setValidateEnhancedForLoopVariable(boolean validateEnhancedForLoopVariable) {
141 this.validateEnhancedForLoopVariable = validateEnhancedForLoopVariable;
142 }
143
144 @Override
145 public int[] getRequiredTokens() {
146 return new int[] {
147 TokenTypes.IDENT,
148 TokenTypes.CTOR_DEF,
149 TokenTypes.METHOD_DEF,
150 TokenTypes.SLIST,
151 TokenTypes.OBJBLOCK,
152 TokenTypes.LITERAL_BREAK,
153 TokenTypes.LITERAL_FOR,
154 TokenTypes.EXPR,
155 };
156 }
157
158 @Override
159 public int[] getDefaultTokens() {
160 return new int[] {
161 TokenTypes.IDENT,
162 TokenTypes.CTOR_DEF,
163 TokenTypes.METHOD_DEF,
164 TokenTypes.SLIST,
165 TokenTypes.OBJBLOCK,
166 TokenTypes.LITERAL_BREAK,
167 TokenTypes.LITERAL_FOR,
168 TokenTypes.VARIABLE_DEF,
169 TokenTypes.EXPR,
170 };
171 }
172
173 @Override
174 public int[] getAcceptableTokens() {
175 return new int[] {
176 TokenTypes.IDENT,
177 TokenTypes.CTOR_DEF,
178 TokenTypes.METHOD_DEF,
179 TokenTypes.SLIST,
180 TokenTypes.OBJBLOCK,
181 TokenTypes.LITERAL_BREAK,
182 TokenTypes.LITERAL_FOR,
183 TokenTypes.VARIABLE_DEF,
184 TokenTypes.PARAMETER_DEF,
185 TokenTypes.EXPR,
186 };
187 }
188
189
190
191 @Override
192 public void visitToken(DetailAST ast) {
193 switch (ast.getType()) {
194 case TokenTypes.OBJBLOCK:
195 case TokenTypes.METHOD_DEF:
196 case TokenTypes.CTOR_DEF:
197 case TokenTypes.LITERAL_FOR:
198 scopeStack.push(new ScopeData());
199 break;
200 case TokenTypes.SLIST:
201 currentScopeAssignedVariables.push(new ArrayDeque<>());
202 if (ast.getParent().getType() != TokenTypes.CASE_GROUP
203 || ast.getParent().getParent().findFirstToken(TokenTypes.CASE_GROUP)
204 == ast.getParent()) {
205 storePrevScopeUninitializedVariableData();
206 scopeStack.push(new ScopeData());
207 }
208 break;
209 case TokenTypes.PARAMETER_DEF:
210 if (!isInLambda(ast)
211 && ast.findFirstToken(TokenTypes.MODIFIERS)
212 .findFirstToken(TokenTypes.FINAL) == null
213 && !isInAbstractOrNativeMethod(ast)
214 && !ScopeUtil.isInInterfaceBlock(ast)
215 && !isMultipleTypeCatch(ast)
216 && !CheckUtil.isReceiverParameter(ast)) {
217 insertParameter(ast);
218 }
219 break;
220 case TokenTypes.VARIABLE_DEF:
221 if (ast.getParent().getType() != TokenTypes.OBJBLOCK
222 && ast.findFirstToken(TokenTypes.MODIFIERS)
223 .findFirstToken(TokenTypes.FINAL) == null
224 && !isVariableInForInit(ast)
225 && shouldCheckEnhancedForLoopVariable(ast)) {
226 insertVariable(ast);
227 }
228 break;
229 case TokenTypes.IDENT:
230 final int parentType = ast.getParent().getType();
231 if (isAssignOperator(parentType) && isFirstChild(ast)) {
232 final Optional<FinalVariableCandidate> candidate = getFinalCandidate(ast);
233 if (candidate.isPresent()) {
234 determineAssignmentConditions(ast, candidate.orElseThrow());
235 currentScopeAssignedVariables.peek().add(ast);
236 }
237 removeFinalVariableCandidateFromStack(ast);
238 }
239 break;
240 case TokenTypes.LITERAL_BREAK:
241 scopeStack.peek().containsBreak = true;
242 break;
243 case TokenTypes.EXPR:
244
245 if (ast.getParent().getType() == TokenTypes.SWITCH_RULE) {
246 storePrevScopeUninitializedVariableData();
247 }
248 break;
249 default:
250 throw new IllegalStateException("Incorrect token type");
251 }
252 }
253
254 @Override
255 public void leaveToken(DetailAST ast) {
256 Map<String, FinalVariableCandidate> scope = null;
257 final DetailAST parentAst = ast.getParent();
258 switch (ast.getType()) {
259 case TokenTypes.OBJBLOCK:
260 case TokenTypes.CTOR_DEF:
261 case TokenTypes.METHOD_DEF:
262 case TokenTypes.LITERAL_FOR:
263 scope = scopeStack.pop().scope;
264 break;
265 case TokenTypes.EXPR:
266
267 if (parentAst.getType() == TokenTypes.SWITCH_RULE
268 && shouldUpdateUninitializedVariables(parentAst)) {
269 updateAllUninitializedVariables();
270 }
271 break;
272 case TokenTypes.SLIST:
273 boolean containsBreak = false;
274 if (parentAst.getType() != TokenTypes.CASE_GROUP
275 || findLastCaseGroupWhichContainsSlist(parentAst.getParent()) == parentAst) {
276 containsBreak = scopeStack.peek().containsBreak;
277 scope = scopeStack.pop().scope;
278 }
279 if (containsBreak || shouldUpdateUninitializedVariables(parentAst)) {
280 updateAllUninitializedVariables();
281 }
282 updateCurrentScopeAssignedVariables();
283 break;
284 default:
285
286 }
287 if (scope != null) {
288 for (FinalVariableCandidate candidate : scope.values()) {
289 final DetailAST ident = candidate.variableIdent;
290 log(ident, MSG_KEY, ident.getText());
291 }
292 }
293 }
294
295
296
297
298 private void updateCurrentScopeAssignedVariables() {
299
300 final Deque<DetailAST> poppedScopeAssignedVariableData =
301 currentScopeAssignedVariables.pop();
302 final Deque<DetailAST> currentScopeAssignedVariableData =
303 currentScopeAssignedVariables.peek();
304 if (currentScopeAssignedVariableData != null) {
305 currentScopeAssignedVariableData.addAll(poppedScopeAssignedVariableData);
306 }
307 }
308
309
310
311
312
313
314
315 private static void determineAssignmentConditions(DetailAST ident,
316 FinalVariableCandidate candidate) {
317 if (candidate.assigned) {
318 final int[] blockTypes = {
319 TokenTypes.LITERAL_ELSE,
320 TokenTypes.CASE_GROUP,
321 TokenTypes.SWITCH_RULE,
322 };
323 if (!isInSpecificCodeBlocks(ident, blockTypes)) {
324 candidate.alreadyAssigned = true;
325 }
326 }
327 else {
328 candidate.assigned = true;
329 }
330 }
331
332
333
334
335
336
337
338
339 private static boolean isInSpecificCodeBlocks(DetailAST node, int... blockTypes) {
340 boolean returnValue = false;
341 for (int blockType : blockTypes) {
342 for (DetailAST token = node; token != null; token = token.getParent()) {
343 final int type = token.getType();
344 if (type == blockType) {
345 returnValue = true;
346 break;
347 }
348 }
349 }
350 return returnValue;
351 }
352
353
354
355
356
357
358
359 private Optional<FinalVariableCandidate> getFinalCandidate(DetailAST ast) {
360 Optional<FinalVariableCandidate> result = Optional.empty();
361 final Iterator<ScopeData> iterator = scopeStack.descendingIterator();
362 while (iterator.hasNext() && result.isEmpty()) {
363 final ScopeData scopeData = iterator.next();
364 result = scopeData.findFinalVariableCandidateForAst(ast);
365 }
366 return result;
367 }
368
369
370
371
372 private void storePrevScopeUninitializedVariableData() {
373 final ScopeData scopeData = scopeStack.peek();
374 final Deque<DetailAST> prevScopeUninitializedVariableData =
375 new ArrayDeque<>();
376 scopeData.uninitializedVariables.forEach(prevScopeUninitializedVariableData::push);
377 scopeData.prevScopeUninitializedVariables = prevScopeUninitializedVariableData;
378 }
379
380
381
382
383 private void updateAllUninitializedVariables() {
384 final boolean hasSomeScopes = !currentScopeAssignedVariables.isEmpty();
385 if (hasSomeScopes) {
386 scopeStack.forEach(scopeData -> {
387 updateUninitializedVariables(scopeData.prevScopeUninitializedVariables);
388 });
389 }
390 }
391
392
393
394
395
396
397 private void updateUninitializedVariables(Deque<DetailAST> scopeUninitializedVariableData) {
398 final Iterator<DetailAST> iterator = currentScopeAssignedVariables.peek().iterator();
399 while (iterator.hasNext()) {
400 final DetailAST assignedVariable = iterator.next();
401 boolean shouldRemove = false;
402 for (DetailAST variable : scopeUninitializedVariableData) {
403 for (ScopeData scopeData : scopeStack) {
404 final FinalVariableCandidate candidate =
405 scopeData.scope.get(variable.getText());
406 DetailAST storedVariable = null;
407 if (candidate != null) {
408 storedVariable = candidate.variableIdent;
409 }
410 if (storedVariable != null
411 && isSameVariables(assignedVariable, variable)) {
412 scopeData.uninitializedVariables.push(variable);
413 shouldRemove = true;
414 }
415 }
416 }
417 if (shouldRemove) {
418 iterator.remove();
419 }
420 }
421 }
422
423
424
425
426
427
428
429
430
431 private static boolean shouldUpdateUninitializedVariables(DetailAST ast) {
432 return ast.getLastChild().getType() == TokenTypes.LITERAL_ELSE
433 || isCaseTokenWithAnotherCaseFollowing(ast);
434 }
435
436
437
438
439
440
441
442
443 private static boolean isCaseTokenWithAnotherCaseFollowing(DetailAST ast) {
444 boolean result = false;
445 if (ast.getType() == TokenTypes.CASE_GROUP) {
446 result = findLastCaseGroupWhichContainsSlist(ast.getParent()) != ast;
447 }
448 else if (ast.getType() == TokenTypes.SWITCH_RULE) {
449 result = ast.getNextSibling().getType() == TokenTypes.SWITCH_RULE;
450 }
451 return result;
452 }
453
454
455
456
457
458
459
460
461 private static DetailAST findLastCaseGroupWhichContainsSlist(DetailAST literalSwitchAst) {
462 DetailAST returnValue = null;
463 for (DetailAST astIterator = literalSwitchAst.getFirstChild(); astIterator != null;
464 astIterator = astIterator.getNextSibling()) {
465 if (astIterator.findFirstToken(TokenTypes.SLIST) != null) {
466 returnValue = astIterator;
467 }
468 }
469 return returnValue;
470 }
471
472
473
474
475
476
477
478 private boolean shouldCheckEnhancedForLoopVariable(DetailAST ast) {
479 return validateEnhancedForLoopVariable
480 || ast.getParent().getType() != TokenTypes.FOR_EACH_CLAUSE;
481 }
482
483
484
485
486
487
488 private void insertParameter(DetailAST ast) {
489 final Map<String, FinalVariableCandidate> scope = scopeStack.peek().scope;
490 final DetailAST astNode = ast.findFirstToken(TokenTypes.IDENT);
491 scope.put(astNode.getText(), new FinalVariableCandidate(astNode));
492 }
493
494
495
496
497
498
499 private void insertVariable(DetailAST ast) {
500 final Map<String, FinalVariableCandidate> scope = scopeStack.peek().scope;
501 final DetailAST astNode = ast.findFirstToken(TokenTypes.IDENT);
502 final FinalVariableCandidate candidate = new FinalVariableCandidate(astNode);
503
504 candidate.assigned = ast.getParent().getType() == TokenTypes.FOR_EACH_CLAUSE;
505 scope.put(astNode.getText(), candidate);
506 if (!isInitialized(astNode)) {
507 scopeStack.peek().uninitializedVariables.add(astNode);
508 }
509 }
510
511
512
513
514
515
516
517 private static boolean isInitialized(DetailAST ast) {
518 return ast.getParent().getLastChild().getType() == TokenTypes.ASSIGN;
519 }
520
521
522
523
524
525
526
527 private static boolean isFirstChild(DetailAST ast) {
528 return ast.getPreviousSibling() == null;
529 }
530
531
532
533
534
535
536 private void removeFinalVariableCandidateFromStack(DetailAST ast) {
537 final Iterator<ScopeData> iterator = scopeStack.descendingIterator();
538 while (iterator.hasNext()) {
539 final ScopeData scopeData = iterator.next();
540 final Map<String, FinalVariableCandidate> scope = scopeData.scope;
541 final FinalVariableCandidate candidate = scope.get(ast.getText());
542 DetailAST storedVariable = null;
543 if (candidate != null) {
544 storedVariable = candidate.variableIdent;
545 }
546 if (storedVariable != null && isSameVariables(storedVariable, ast)) {
547 if (shouldRemoveFinalVariableCandidate(scopeData, ast)) {
548 scope.remove(ast.getText());
549 }
550 break;
551 }
552 }
553 }
554
555
556
557
558
559
560
561 private static boolean isMultipleTypeCatch(DetailAST parameterDefAst) {
562 final DetailAST typeAst = parameterDefAst.findFirstToken(TokenTypes.TYPE);
563 return typeAst.findFirstToken(TokenTypes.BOR) != null;
564 }
565
566
567
568
569
570
571
572
573
574 private static boolean shouldRemoveFinalVariableCandidate(ScopeData scopeData, DetailAST ast) {
575 boolean shouldRemove = true;
576 for (DetailAST variable : scopeData.uninitializedVariables) {
577 if (variable.getText().equals(ast.getText())) {
578
579
580
581 final DetailAST currAstLoopAstParent = getParentLoop(ast);
582 final DetailAST currVarLoopAstParent = getParentLoop(variable);
583 if (currAstLoopAstParent == currVarLoopAstParent) {
584 final FinalVariableCandidate candidate = scopeData.scope.get(ast.getText());
585 shouldRemove = candidate.alreadyAssigned;
586 }
587 scopeData.uninitializedVariables.remove(variable);
588 break;
589 }
590 }
591 return shouldRemove;
592 }
593
594
595
596
597
598
599
600
601
602 private static DetailAST getParentLoop(DetailAST ast) {
603 DetailAST parentLoop = ast;
604 while (parentLoop != null
605 && !isLoopAst(parentLoop.getType())) {
606 parentLoop = parentLoop.getParent();
607 }
608 return parentLoop;
609 }
610
611
612
613
614
615
616
617 private static boolean isAssignOperator(int parentType) {
618 return ASSIGN_OPERATOR_TYPES.get(parentType);
619 }
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634 private static boolean isVariableInForInit(DetailAST variableDef) {
635 return variableDef.getParent().getType() == TokenTypes.FOR_INIT;
636 }
637
638
639
640
641
642
643
644 private static boolean isInAbstractOrNativeMethod(DetailAST ast) {
645 boolean abstractOrNative = false;
646 DetailAST currentAst = ast;
647 while (currentAst != null && !abstractOrNative) {
648 if (currentAst.getType() == TokenTypes.METHOD_DEF) {
649 final DetailAST modifiers =
650 currentAst.findFirstToken(TokenTypes.MODIFIERS);
651 abstractOrNative = modifiers.findFirstToken(TokenTypes.ABSTRACT) != null
652 || modifiers.findFirstToken(TokenTypes.LITERAL_NATIVE) != null;
653 }
654 currentAst = currentAst.getParent();
655 }
656 return abstractOrNative;
657 }
658
659
660
661
662
663
664
665 private static boolean isInLambda(DetailAST paramDef) {
666 return paramDef.getParent().getParent().getType() == TokenTypes.LAMBDA;
667 }
668
669
670
671
672
673
674
675 private static DetailAST findFirstUpperNamedBlock(DetailAST ast) {
676 DetailAST astTraverse = ast;
677 while (!TokenUtil.isOfType(astTraverse, TokenTypes.METHOD_DEF, TokenTypes.CLASS_DEF,
678 TokenTypes.ENUM_DEF, TokenTypes.CTOR_DEF, TokenTypes.COMPACT_CTOR_DEF)
679 && !ScopeUtil.isClassFieldDef(astTraverse)) {
680 astTraverse = astTraverse.getParent();
681 }
682 return astTraverse;
683 }
684
685
686
687
688
689
690
691
692 private static boolean isSameVariables(DetailAST ast1, DetailAST ast2) {
693 final DetailAST classOrMethodOfAst1 =
694 findFirstUpperNamedBlock(ast1);
695 final DetailAST classOrMethodOfAst2 =
696 findFirstUpperNamedBlock(ast2);
697 return classOrMethodOfAst1 == classOrMethodOfAst2 && ast1.getText().equals(ast2.getText());
698 }
699
700
701
702
703
704
705
706 private static boolean isLoopAst(int ast) {
707 return LOOP_TYPES.get(ast);
708 }
709
710
711
712
713 private static final class ScopeData {
714
715
716 private final Map<String, FinalVariableCandidate> scope = new HashMap<>();
717
718
719 private final Deque<DetailAST> uninitializedVariables = new ArrayDeque<>();
720
721
722 private Deque<DetailAST> prevScopeUninitializedVariables = new ArrayDeque<>();
723
724
725 private boolean containsBreak;
726
727
728
729
730
731
732
733 public Optional<FinalVariableCandidate> findFinalVariableCandidateForAst(DetailAST ast) {
734 Optional<FinalVariableCandidate> result = Optional.empty();
735 DetailAST storedVariable = null;
736 final Optional<FinalVariableCandidate> candidate =
737 Optional.ofNullable(scope.get(ast.getText()));
738 if (candidate.isPresent()) {
739 storedVariable = candidate.orElseThrow().variableIdent;
740 }
741 if (storedVariable != null && isSameVariables(storedVariable, ast)) {
742 result = candidate;
743 }
744 return result;
745 }
746
747 }
748
749
750 private static final class FinalVariableCandidate {
751
752
753 private final DetailAST variableIdent;
754
755 private boolean assigned;
756
757 private boolean alreadyAssigned;
758
759
760
761
762
763
764 private FinalVariableCandidate(DetailAST variableIdent) {
765 this.variableIdent = variableIdent;
766 }
767
768 }
769
770 }