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