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 @FileStatefulCheck
53 public class FinalLocalVariableCheck extends AbstractCheck {
54
55
56
57
58
59 public static final String MSG_KEY = "final.variable";
60
61
62
63
64 private static final BitSet ASSIGN_OPERATOR_TYPES = TokenUtil.asBitSet(
65 TokenTypes.POST_INC,
66 TokenTypes.POST_DEC,
67 TokenTypes.ASSIGN,
68 TokenTypes.PLUS_ASSIGN,
69 TokenTypes.MINUS_ASSIGN,
70 TokenTypes.STAR_ASSIGN,
71 TokenTypes.DIV_ASSIGN,
72 TokenTypes.MOD_ASSIGN,
73 TokenTypes.SR_ASSIGN,
74 TokenTypes.BSR_ASSIGN,
75 TokenTypes.SL_ASSIGN,
76 TokenTypes.BAND_ASSIGN,
77 TokenTypes.BXOR_ASSIGN,
78 TokenTypes.BOR_ASSIGN,
79 TokenTypes.INC,
80 TokenTypes.DEC
81 );
82
83
84
85
86 private static final BitSet LOOP_TYPES = TokenUtil.asBitSet(
87 TokenTypes.LITERAL_FOR,
88 TokenTypes.LITERAL_WHILE,
89 TokenTypes.LITERAL_DO
90 );
91
92
93 private final Deque<ScopeData> scopeStack = new ArrayDeque<>();
94
95
96 private final Deque<Deque<DetailAST>> currentScopeAssignedVariables =
97 new ArrayDeque<>();
98
99
100
101
102
103
104 private boolean validateEnhancedForLoopVariable;
105
106
107
108
109
110
111 private boolean validateUnnamedVariables;
112
113
114
115
116
117
118
119
120
121 public final void setValidateEnhancedForLoopVariable(boolean validateEnhancedForLoopVariable) {
122 this.validateEnhancedForLoopVariable = validateEnhancedForLoopVariable;
123 }
124
125
126
127
128
129
130
131
132
133 public final void setValidateUnnamedVariables(boolean validateUnnamedVariables) {
134 this.validateUnnamedVariables = validateUnnamedVariables;
135 }
136
137 @Override
138 public int[] getRequiredTokens() {
139 return new int[] {
140 TokenTypes.IDENT,
141 TokenTypes.CTOR_DEF,
142 TokenTypes.METHOD_DEF,
143 TokenTypes.SLIST,
144 TokenTypes.OBJBLOCK,
145 TokenTypes.COMPACT_COMPILATION_UNIT,
146 TokenTypes.LITERAL_BREAK,
147 TokenTypes.LITERAL_FOR,
148 TokenTypes.EXPR,
149 };
150 }
151
152 @Override
153 public int[] getDefaultTokens() {
154 return new int[] {
155 TokenTypes.IDENT,
156 TokenTypes.CTOR_DEF,
157 TokenTypes.METHOD_DEF,
158 TokenTypes.SLIST,
159 TokenTypes.OBJBLOCK,
160 TokenTypes.COMPACT_COMPILATION_UNIT,
161 TokenTypes.LITERAL_BREAK,
162 TokenTypes.LITERAL_FOR,
163 TokenTypes.VARIABLE_DEF,
164 TokenTypes.EXPR,
165 };
166 }
167
168 @Override
169 public int[] getAcceptableTokens() {
170 return new int[] {
171 TokenTypes.IDENT,
172 TokenTypes.CTOR_DEF,
173 TokenTypes.METHOD_DEF,
174 TokenTypes.SLIST,
175 TokenTypes.OBJBLOCK,
176 TokenTypes.COMPACT_COMPILATION_UNIT,
177 TokenTypes.LITERAL_BREAK,
178 TokenTypes.LITERAL_FOR,
179 TokenTypes.VARIABLE_DEF,
180 TokenTypes.PARAMETER_DEF,
181 TokenTypes.EXPR,
182 };
183 }
184
185
186
187 @Override
188 public void visitToken(DetailAST ast) {
189 switch (ast.getType()) {
190 case TokenTypes.COMPACT_COMPILATION_UNIT, TokenTypes.OBJBLOCK,
191 TokenTypes.METHOD_DEF, TokenTypes.CTOR_DEF, TokenTypes.LITERAL_FOR ->
192 scopeStack.push(new ScopeData());
193
194 case TokenTypes.SLIST -> {
195 currentScopeAssignedVariables.push(new ArrayDeque<>());
196 if (ast.getParent().getType() != TokenTypes.CASE_GROUP
197 || ast.getParent().getParent()
198 .findFirstToken(TokenTypes.CASE_GROUP) == ast.getParent()) {
199 storePrevScopeUninitializedVariableData();
200 scopeStack.push(new ScopeData());
201 }
202 }
203
204 case TokenTypes.PARAMETER_DEF -> {
205 if (!isInLambda(ast)
206 && ast.findFirstToken(TokenTypes.MODIFIERS)
207 .findFirstToken(TokenTypes.FINAL) == null
208 && !isInMethodWithoutBody(ast)
209 && !isMultipleTypeCatch(ast)
210 && !CheckUtil.isReceiverParameter(ast)) {
211 insertParameter(ast);
212 }
213 }
214
215 case TokenTypes.VARIABLE_DEF -> {
216 if (ast.getParent().getType() != TokenTypes.OBJBLOCK
217 && ast.findFirstToken(TokenTypes.MODIFIERS)
218 .findFirstToken(TokenTypes.FINAL) == null
219 && !isVariableInForInit(ast)
220 && shouldCheckEnhancedForLoopVariable(ast)
221 && shouldCheckUnnamedVariable(ast)) {
222 insertVariable(ast);
223 }
224 }
225
226 case TokenTypes.IDENT -> {
227 final int parentType = ast.getParent().getType();
228 if (isAssignOperator(parentType) && isFirstChild(ast)) {
229 final Optional<FinalVariableCandidate> candidate = getFinalCandidate(ast);
230 if (candidate.isPresent()) {
231 determineAssignmentConditions(ast, candidate.orElseThrow());
232 currentScopeAssignedVariables.peek().add(ast);
233 }
234 removeFinalVariableCandidateFromStack(ast);
235 }
236 }
237
238 case TokenTypes.LITERAL_BREAK -> scopeStack.peek().containsBreak = true;
239
240 case TokenTypes.EXPR -> {
241
242 if (ast.getParent().getType() == TokenTypes.SWITCH_RULE) {
243 storePrevScopeUninitializedVariableData();
244 }
245 }
246
247 default -> throw new IllegalStateException("Incorrect token type");
248 }
249 }
250
251 @Override
252 public void leaveToken(DetailAST ast) {
253 Map<String, FinalVariableCandidate> scope = null;
254 final DetailAST parentAst = ast.getParent();
255 switch (ast.getType()) {
256 case TokenTypes.OBJBLOCK, TokenTypes.CTOR_DEF, TokenTypes.METHOD_DEF,
257 TokenTypes.LITERAL_FOR ->
258 scope = scopeStack.pop().scope;
259
260 case TokenTypes.EXPR -> {
261
262 if (parentAst.getType() == TokenTypes.SWITCH_RULE
263 && shouldUpdateUninitializedVariables(parentAst)) {
264 updateAllUninitializedVariables();
265 }
266 }
267
268 case TokenTypes.SLIST -> {
269 boolean containsBreak = false;
270 if (parentAst.getType() != TokenTypes.CASE_GROUP
271 || findLastCaseGroupWhichContainsSlist(parentAst.getParent())
272 == parentAst) {
273 containsBreak = scopeStack.peek().containsBreak;
274 scope = scopeStack.pop().scope;
275 }
276 if (containsBreak || shouldUpdateUninitializedVariables(parentAst)) {
277 updateAllUninitializedVariables();
278 }
279 updateCurrentScopeAssignedVariables();
280 }
281
282 default -> {
283
284 }
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
489 private boolean shouldCheckUnnamedVariable(DetailAST ast) {
490 return validateUnnamedVariables
491 || !"_".equals(TokenUtil.getIdent(ast).getText());
492 }
493
494
495
496
497
498
499 private void insertParameter(DetailAST ast) {
500 final Map<String, FinalVariableCandidate> scope = scopeStack.peek().scope;
501 final DetailAST astNode = TokenUtil.getIdent(ast);
502 scope.put(astNode.getText(), new FinalVariableCandidate(astNode));
503 }
504
505
506
507
508
509
510 private void insertVariable(DetailAST variableAst) {
511 final Map<String, FinalVariableCandidate> scope = scopeStack.peek().scope;
512 final DetailAST astNode = TokenUtil.getIdent(variableAst);
513 final FinalVariableCandidate candidate = new FinalVariableCandidate(astNode);
514
515 candidate.assigned = variableAst.getParent().getType() == TokenTypes.FOR_EACH_CLAUSE;
516 scope.put(astNode.getText(), candidate);
517 if (!isInitialized(variableAst)) {
518 scopeStack.peek().uninitializedVariables.add(astNode);
519 }
520 }
521
522
523
524
525
526
527
528 private static boolean isInitialized(DetailAST ast) {
529 return ast.getLastChild().getType() == TokenTypes.ASSIGN;
530 }
531
532
533
534
535
536
537
538 private static boolean isFirstChild(DetailAST ast) {
539 return ast.getPreviousSibling() == null;
540 }
541
542
543
544
545
546
547 private void removeFinalVariableCandidateFromStack(DetailAST ast) {
548 final Iterator<ScopeData> iterator = scopeStack.descendingIterator();
549 while (iterator.hasNext()) {
550 final ScopeData scopeData = iterator.next();
551 final Map<String, FinalVariableCandidate> scope = scopeData.scope;
552 final FinalVariableCandidate candidate = scope.get(ast.getText());
553 DetailAST storedVariable = null;
554 if (candidate != null) {
555 storedVariable = candidate.variableIdent;
556 }
557 if (storedVariable != null && isSameVariables(storedVariable, ast)) {
558 if (shouldRemoveFinalVariableCandidate(scopeData, ast)) {
559 scope.remove(ast.getText());
560 }
561 break;
562 }
563 }
564 }
565
566
567
568
569
570
571
572 private static boolean isMultipleTypeCatch(DetailAST parameterDefAst) {
573 final DetailAST typeAst = parameterDefAst.findFirstToken(TokenTypes.TYPE);
574 return typeAst.findFirstToken(TokenTypes.BOR) != null;
575 }
576
577
578
579
580
581
582
583
584
585 private static boolean shouldRemoveFinalVariableCandidate(ScopeData scopeData, DetailAST ast) {
586 boolean shouldRemove = true;
587 for (DetailAST variable : scopeData.uninitializedVariables) {
588 if (variable.getText().equals(ast.getText())) {
589
590
591
592 final DetailAST currAstLoopAstParent = getParentLoop(ast);
593 final DetailAST currVarLoopAstParent = getParentLoop(variable);
594 if (currAstLoopAstParent == currVarLoopAstParent) {
595 final FinalVariableCandidate candidate = scopeData.scope.get(ast.getText());
596 shouldRemove = candidate.alreadyAssigned;
597 }
598 scopeData.uninitializedVariables.remove(variable);
599 break;
600 }
601 }
602 return shouldRemove;
603 }
604
605
606
607
608
609
610
611
612
613 private static DetailAST getParentLoop(DetailAST ast) {
614 DetailAST parentLoop = ast;
615 while (parentLoop != null
616 && !isLoopAst(parentLoop.getType())) {
617 parentLoop = parentLoop.getParent();
618 }
619 return parentLoop;
620 }
621
622
623
624
625
626
627
628 private static boolean isAssignOperator(int parentType) {
629 return ASSIGN_OPERATOR_TYPES.get(parentType);
630 }
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646 private static boolean isVariableInForInit(DetailAST variableDef) {
647 return variableDef.getParent().getType() == TokenTypes.FOR_INIT;
648 }
649
650
651
652
653
654
655
656 private static boolean isInMethodWithoutBody(DetailAST parameterDefAst) {
657 final DetailAST methodDefAst = parameterDefAst.getParent().getParent();
658 return methodDefAst.findFirstToken(TokenTypes.SLIST) == null;
659 }
660
661
662
663
664
665
666
667 private static boolean isInLambda(DetailAST paramDef) {
668 return paramDef.getParent().getParent().getType() == TokenTypes.LAMBDA;
669 }
670
671
672
673
674
675
676
677 private static DetailAST findFirstUpperNamedBlock(DetailAST ast) {
678 DetailAST astTraverse = ast;
679 while (!TokenUtil.isOfType(astTraverse, TokenTypes.METHOD_DEF, TokenTypes.CLASS_DEF,
680 TokenTypes.ENUM_DEF, TokenTypes.CTOR_DEF, TokenTypes.COMPACT_CTOR_DEF)
681 && !ScopeUtil.isClassFieldDef(astTraverse)) {
682 astTraverse = astTraverse.getParent();
683 }
684 return astTraverse;
685 }
686
687
688
689
690
691
692
693
694 private static boolean isSameVariables(DetailAST ast1, DetailAST ast2) {
695 final DetailAST classOrMethodOfAst1 =
696 findFirstUpperNamedBlock(ast1);
697 final DetailAST classOrMethodOfAst2 =
698 findFirstUpperNamedBlock(ast2);
699 return classOrMethodOfAst1 == classOrMethodOfAst2 && ast1.getText().equals(ast2.getText());
700 }
701
702
703
704
705
706
707
708 private static boolean isLoopAst(int ast) {
709 return LOOP_TYPES.get(ast);
710 }
711
712
713
714
715 private static final class ScopeData {
716
717
718 private final Map<String, FinalVariableCandidate> scope = new HashMap<>();
719
720
721 private final Deque<DetailAST> uninitializedVariables = new ArrayDeque<>();
722
723
724 private Deque<DetailAST> prevScopeUninitializedVariables = new ArrayDeque<>();
725
726
727 private boolean containsBreak;
728
729
730
731
732
733
734
735 Optional<FinalVariableCandidate>
736 findFinalVariableCandidateForAst(DetailAST ast) {
737 Optional<FinalVariableCandidate> result = Optional.empty();
738 DetailAST storedVariable = null;
739 final Optional<FinalVariableCandidate> candidate =
740 Optional.ofNullable(scope.get(ast.getText()));
741 if (candidate.isPresent()) {
742 storedVariable = candidate.orElseThrow().variableIdent;
743 }
744 if (storedVariable != null && isSameVariables(storedVariable, ast)) {
745 result = candidate;
746 }
747 return result;
748 }
749
750 }
751
752
753 private static final class FinalVariableCandidate {
754
755
756 private final DetailAST variableIdent;
757
758 private boolean assigned;
759
760 private boolean alreadyAssigned;
761
762
763
764
765
766
767 private FinalVariableCandidate(DetailAST variableIdent) {
768 this.variableIdent = variableIdent;
769 }
770
771 }
772
773 }