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