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