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