001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.coding; 021 022import java.util.ArrayDeque; 023import java.util.BitSet; 024import java.util.Deque; 025import java.util.HashMap; 026import java.util.Iterator; 027import java.util.Map; 028import java.util.Optional; 029 030import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 031import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 032import com.puppycrawl.tools.checkstyle.api.DetailAST; 033import com.puppycrawl.tools.checkstyle.api.TokenTypes; 034import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 035import com.puppycrawl.tools.checkstyle.utils.ScopeUtil; 036import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 037 038/** 039 * <div> 040 * Checks that local variables that never have their values changed are declared final. 041 * The check can be configured to also check that unchanged parameters are declared final. 042 * </div> 043 * 044 * <p> 045 * Notes: 046 * When configured to check parameters, the check ignores parameters of interface 047 * methods and abstract methods. 048 * </p> 049 * 050 * @since 3.2 051 */ 052@FileStatefulCheck 053public class FinalLocalVariableCheck extends AbstractCheck { 054 055 /** 056 * A key is pointing to the warning message text in "messages.properties" file. 057 */ 058 public static final String MSG_KEY = "final.variable"; 059 060 /** 061 * Assign operator types. 062 */ 063 private static final BitSet ASSIGN_OPERATOR_TYPES = TokenUtil.asBitSet( 064 TokenTypes.POST_INC, 065 TokenTypes.POST_DEC, 066 TokenTypes.ASSIGN, 067 TokenTypes.PLUS_ASSIGN, 068 TokenTypes.MINUS_ASSIGN, 069 TokenTypes.STAR_ASSIGN, 070 TokenTypes.DIV_ASSIGN, 071 TokenTypes.MOD_ASSIGN, 072 TokenTypes.SR_ASSIGN, 073 TokenTypes.BSR_ASSIGN, 074 TokenTypes.SL_ASSIGN, 075 TokenTypes.BAND_ASSIGN, 076 TokenTypes.BXOR_ASSIGN, 077 TokenTypes.BOR_ASSIGN, 078 TokenTypes.INC, 079 TokenTypes.DEC 080 ); 081 082 /** 083 * Loop types. 084 */ 085 private static final BitSet LOOP_TYPES = TokenUtil.asBitSet( 086 TokenTypes.LITERAL_FOR, 087 TokenTypes.LITERAL_WHILE, 088 TokenTypes.LITERAL_DO 089 ); 090 091 /** Scope Deque. */ 092 private final Deque<ScopeData> scopeStack = new ArrayDeque<>(); 093 094 /** Assigned variables of current scope. */ 095 private final Deque<Deque<DetailAST>> currentScopeAssignedVariables = 096 new ArrayDeque<>(); 097 098 /** Unassigned candidates on entry to conditional blocks that end with a throw. */ 099 private final Map<DetailAST, ThrowingBranch> throwingBranches = 100 new HashMap<>(); 101 102 /** 103 * Control whether to check 104 * <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-14.html#jls-14.14.2"> 105 * enhanced for-loop</a> variable. 106 */ 107 private boolean validateEnhancedForLoopVariable; 108 109 /** 110 * Control whether to check 111 * <a href="https://docs.oracle.com/javase/specs/jls/se21/preview/specs/unnamed-jls.html"> 112 * unnamed variables</a>. 113 */ 114 private boolean validateUnnamedVariables; 115 116 /** 117 * Creates a new {@code FinalLocalVariableCheck} instance. 118 */ 119 public FinalLocalVariableCheck() { 120 // no code by default 121 } 122 123 /** 124 * Setter to control whether to check 125 * <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-14.html#jls-14.14.2"> 126 * enhanced for-loop</a> variable. 127 * 128 * @param validateEnhancedForLoopVariable whether to check for-loop variable 129 * @since 6.5 130 */ 131 public final void setValidateEnhancedForLoopVariable(boolean validateEnhancedForLoopVariable) { 132 this.validateEnhancedForLoopVariable = validateEnhancedForLoopVariable; 133 } 134 135 /** 136 * Setter to control whether to check 137 * <a href="https://docs.oracle.com/javase/specs/jls/se21/preview/specs/unnamed-jls.html"> 138 * unnamed variables</a>. 139 * 140 * @param validateUnnamedVariables whether to check unnamed variables 141 * @since 10.18.0 142 */ 143 public final void setValidateUnnamedVariables(boolean validateUnnamedVariables) { 144 this.validateUnnamedVariables = validateUnnamedVariables; 145 } 146 147 @Override 148 public int[] getRequiredTokens() { 149 return new int[] { 150 TokenTypes.IDENT, 151 TokenTypes.CTOR_DEF, 152 TokenTypes.METHOD_DEF, 153 TokenTypes.SLIST, 154 TokenTypes.OBJBLOCK, 155 TokenTypes.COMPACT_COMPILATION_UNIT, 156 TokenTypes.LITERAL_BREAK, 157 TokenTypes.LITERAL_FOR, 158 TokenTypes.EXPR, 159 }; 160 } 161 162 @Override 163 public int[] getDefaultTokens() { 164 return new int[] { 165 TokenTypes.IDENT, 166 TokenTypes.CTOR_DEF, 167 TokenTypes.METHOD_DEF, 168 TokenTypes.SLIST, 169 TokenTypes.OBJBLOCK, 170 TokenTypes.COMPACT_COMPILATION_UNIT, 171 TokenTypes.LITERAL_BREAK, 172 TokenTypes.LITERAL_FOR, 173 TokenTypes.VARIABLE_DEF, 174 TokenTypes.EXPR, 175 }; 176 } 177 178 @Override 179 public int[] getAcceptableTokens() { 180 return new int[] { 181 TokenTypes.IDENT, 182 TokenTypes.CTOR_DEF, 183 TokenTypes.METHOD_DEF, 184 TokenTypes.SLIST, 185 TokenTypes.OBJBLOCK, 186 TokenTypes.COMPACT_COMPILATION_UNIT, 187 TokenTypes.LITERAL_BREAK, 188 TokenTypes.LITERAL_FOR, 189 TokenTypes.VARIABLE_DEF, 190 TokenTypes.PARAMETER_DEF, 191 TokenTypes.EXPR, 192 }; 193 } 194 195 @Override 196 public void beginTree(DetailAST rootAST) { 197 scopeStack.clear(); 198 currentScopeAssignedVariables.clear(); 199 throwingBranches.clear(); 200 } 201 202 // -@cs[CyclomaticComplexity] The only optimization which can be done here is moving CASE-block 203 // expressions to separate methods, but that will not increase readability. 204 @Override 205 public void visitToken(DetailAST ast) { 206 switch (ast.getType()) { 207 case TokenTypes.COMPACT_COMPILATION_UNIT, TokenTypes.OBJBLOCK, 208 TokenTypes.METHOD_DEF, TokenTypes.CTOR_DEF, TokenTypes.LITERAL_FOR -> 209 scopeStack.push(new ScopeData()); 210 211 case TokenTypes.SLIST -> { 212 if (ThrowingBranch.isThrowingBranch(ast)) { 213 throwingBranches.put(ast, new ThrowingBranch(scopeStack)); 214 } 215 currentScopeAssignedVariables.push(new ArrayDeque<>()); 216 if (ast.getParent().getType() != TokenTypes.CASE_GROUP 217 || ast.getParent().getParent() 218 .findFirstToken(TokenTypes.CASE_GROUP) == ast.getParent()) { 219 storePrevScopeUninitializedVariableData(); 220 scopeStack.push(new ScopeData()); 221 } 222 } 223 224 case TokenTypes.PARAMETER_DEF -> { 225 if (!isInLambda(ast) 226 && ast.findFirstToken(TokenTypes.MODIFIERS) 227 .findFirstToken(TokenTypes.FINAL) == null 228 && !isInMethodWithoutBody(ast) 229 && !isMultipleTypeCatch(ast) 230 && !CheckUtil.isReceiverParameter(ast)) { 231 insertParameter(ast); 232 } 233 } 234 235 case TokenTypes.VARIABLE_DEF -> { 236 if (ast.getParent().getType() != TokenTypes.OBJBLOCK 237 && ast.findFirstToken(TokenTypes.MODIFIERS) 238 .findFirstToken(TokenTypes.FINAL) == null 239 && !isVariableInForInit(ast) 240 && shouldCheckEnhancedForLoopVariable(ast) 241 && shouldCheckUnnamedVariable(ast)) { 242 insertVariable(ast); 243 } 244 } 245 246 case TokenTypes.IDENT -> { 247 final int parentType = ast.getParent().getType(); 248 if (isAssignOperator(parentType) && isFirstChild(ast)) { 249 final Optional<FinalVariableCandidate> candidate = getFinalCandidate(ast); 250 if (candidate.isPresent()) { 251 determineAssignmentConditions(ast, candidate.orElseThrow()); 252 currentScopeAssignedVariables.peek().add(ast); 253 } 254 removeFinalVariableCandidateFromStack(ast); 255 } 256 } 257 258 case TokenTypes.LITERAL_BREAK -> scopeStack.peek().containsBreak = true; 259 260 case TokenTypes.EXPR -> { 261 // Switch labeled expression has no slist 262 if (ast.getParent().getType() == TokenTypes.SWITCH_RULE) { 263 storePrevScopeUninitializedVariableData(); 264 } 265 } 266 267 default -> throw new IllegalStateException("Incorrect token type"); 268 } 269 } 270 271 @Override 272 public void leaveToken(DetailAST ast) { 273 Map<String, FinalVariableCandidate> scope = null; 274 final DetailAST parentAst = ast.getParent(); 275 switch (ast.getType()) { 276 case TokenTypes.OBJBLOCK, TokenTypes.CTOR_DEF, TokenTypes.METHOD_DEF, 277 TokenTypes.LITERAL_FOR -> 278 scope = scopeStack.pop().scope; 279 280 case TokenTypes.EXPR -> { 281 // Switch labeled expression has no slist 282 if (parentAst.getType() == TokenTypes.SWITCH_RULE 283 && shouldUpdateUninitializedVariables(parentAst)) { 284 updateAllUninitializedVariables(); 285 } 286 } 287 288 case TokenTypes.SLIST -> { 289 boolean containsBreak = false; 290 if (parentAst.getType() != TokenTypes.CASE_GROUP 291 || findLastCaseGroupWhichContainsSlist(parentAst.getParent()) 292 == parentAst) { 293 containsBreak = scopeStack.peek().containsBreak; 294 scope = scopeStack.pop().scope; 295 } 296 if (containsBreak || shouldUpdateUninitializedVariables(parentAst)) { 297 updateAllUninitializedVariables(); 298 } 299 final ThrowingBranch branch = throwingBranches.remove(ast); 300 if (branch != null) { 301 branch.restore(); 302 } 303 updateCurrentScopeAssignedVariables(); 304 } 305 306 default -> { 307 // do nothing 308 } 309 } 310 311 if (scope != null) { 312 for (FinalVariableCandidate candidate : scope.values()) { 313 final DetailAST ident = candidate.variableIdent; 314 log(ident, MSG_KEY, ident.getText()); 315 } 316 } 317 } 318 319 /** 320 * Update assigned variables in a temporary stack. 321 */ 322 private void updateCurrentScopeAssignedVariables() { 323 // -@cs[MoveVariableInsideIf] assignment value is a modification call, so it can't be moved 324 final Deque<DetailAST> poppedScopeAssignedVariableData = 325 currentScopeAssignedVariables.pop(); 326 final Deque<DetailAST> currentScopeAssignedVariableData = 327 currentScopeAssignedVariables.peek(); 328 if (currentScopeAssignedVariableData != null) { 329 currentScopeAssignedVariableData.addAll(poppedScopeAssignedVariableData); 330 } 331 } 332 333 /** 334 * Determines identifier assignment conditions (assigned or already assigned). 335 * 336 * @param ident identifier. 337 * @param candidate final local variable candidate. 338 */ 339 private static void determineAssignmentConditions(DetailAST ident, 340 FinalVariableCandidate candidate) { 341 if (candidate.assigned) { 342 final int[] blockTypes = { 343 TokenTypes.LITERAL_ELSE, 344 TokenTypes.CASE_GROUP, 345 TokenTypes.SWITCH_RULE, 346 }; 347 if (!isInSpecificCodeBlocks(ident, blockTypes)) { 348 candidate.alreadyAssigned = true; 349 } 350 } 351 else { 352 candidate.assigned = true; 353 } 354 } 355 356 /** 357 * Checks whether the scope of a node is restricted to a specific code blocks. 358 * 359 * @param node node. 360 * @param blockTypes int array of all block types to check. 361 * @return true if the scope of a node is restricted to specific code block types. 362 */ 363 private static boolean isInSpecificCodeBlocks(DetailAST node, int... blockTypes) { 364 boolean returnValue = false; 365 for (int blockType : blockTypes) { 366 for (DetailAST token = node; token != null; token = token.getParent()) { 367 final int type = token.getType(); 368 if (type == blockType) { 369 returnValue = true; 370 break; 371 } 372 } 373 } 374 return returnValue; 375 } 376 377 /** 378 * Gets final variable candidate for ast. 379 * 380 * @param ast ast. 381 * @return Optional of {@link FinalVariableCandidate} for ast from scopeStack. 382 */ 383 private Optional<FinalVariableCandidate> getFinalCandidate(DetailAST ast) { 384 Optional<FinalVariableCandidate> result = Optional.empty(); 385 final Iterator<ScopeData> iterator = scopeStack.descendingIterator(); 386 while (iterator.hasNext() && result.isEmpty()) { 387 final ScopeData scopeData = iterator.next(); 388 result = scopeData.findFinalVariableCandidateForAst(ast); 389 } 390 return result; 391 } 392 393 /** 394 * Store un-initialized variables in a temporary stack for future use. 395 */ 396 private void storePrevScopeUninitializedVariableData() { 397 final ScopeData scopeData = scopeStack.peek(); 398 final Deque<DetailAST> prevScopeUninitializedVariableData = 399 new ArrayDeque<>(); 400 scopeData.uninitializedVariables.forEach(prevScopeUninitializedVariableData::push); 401 scopeData.prevScopeUninitializedVariables = prevScopeUninitializedVariableData; 402 } 403 404 /** 405 * Update current scope data uninitialized variable according to the whole scope data. 406 */ 407 private void updateAllUninitializedVariables() { 408 final boolean hasSomeScopes = !currentScopeAssignedVariables.isEmpty(); 409 if (hasSomeScopes) { 410 scopeStack.forEach(scopeData -> { 411 updateUninitializedVariables(scopeData.prevScopeUninitializedVariables); 412 }); 413 } 414 } 415 416 /** 417 * Update current scope data uninitialized variable according to the specific scope data. 418 * 419 * @param scopeUninitializedVariableData variable for specific stack of uninitialized variables 420 */ 421 private void updateUninitializedVariables(Deque<DetailAST> scopeUninitializedVariableData) { 422 final Iterator<DetailAST> iterator = currentScopeAssignedVariables.peek().iterator(); 423 while (iterator.hasNext()) { 424 final DetailAST assignedVariable = iterator.next(); 425 boolean shouldRemove = false; 426 for (DetailAST variable : scopeUninitializedVariableData) { 427 for (ScopeData scopeData : scopeStack) { 428 final FinalVariableCandidate candidate = 429 scopeData.scope.get(variable.getText()); 430 DetailAST storedVariable = null; 431 if (candidate != null) { 432 storedVariable = candidate.variableIdent; 433 } 434 if (storedVariable != null 435 && isSameVariables(assignedVariable, variable)) { 436 scopeData.uninitializedVariables.push(variable); 437 shouldRemove = true; 438 } 439 } 440 } 441 if (shouldRemove) { 442 iterator.remove(); 443 } 444 } 445 } 446 447 /** 448 * If there is an {@code else} following or token is CASE_GROUP or 449 * SWITCH_RULE and there is another {@code case} following, then update the 450 * uninitialized variables. 451 * 452 * @param ast token to be checked 453 * @return true if should be updated, else false 454 */ 455 private static boolean shouldUpdateUninitializedVariables(DetailAST ast) { 456 return ast.getLastChild().getType() == TokenTypes.LITERAL_ELSE 457 || isCaseTokenWithAnotherCaseFollowing(ast); 458 } 459 460 /** 461 * If token is CASE_GROUP or SWITCH_RULE and there is another {@code case} following. 462 * 463 * @param ast token to be checked 464 * @return true if token is CASE_GROUP or SWITCH_RULE and there is another {@code case} 465 * following, else false 466 */ 467 private static boolean isCaseTokenWithAnotherCaseFollowing(DetailAST ast) { 468 boolean result = false; 469 if (ast.getType() == TokenTypes.CASE_GROUP) { 470 result = findLastCaseGroupWhichContainsSlist(ast.getParent()) != ast; 471 } 472 else if (ast.getType() == TokenTypes.SWITCH_RULE) { 473 result = ast.getNextSibling().getType() == TokenTypes.SWITCH_RULE; 474 } 475 return result; 476 } 477 478 /** 479 * Returns the last token of type {@link TokenTypes#CASE_GROUP} which contains 480 * {@link TokenTypes#SLIST}. 481 * 482 * @param literalSwitchAst ast node of type {@link TokenTypes#LITERAL_SWITCH} 483 * @return the matching token, or null if no match 484 */ 485 private static DetailAST findLastCaseGroupWhichContainsSlist(DetailAST literalSwitchAst) { 486 DetailAST returnValue = null; 487 for (DetailAST astIterator = literalSwitchAst.getFirstChild(); astIterator != null; 488 astIterator = astIterator.getNextSibling()) { 489 if (astIterator.findFirstToken(TokenTypes.SLIST) != null) { 490 returnValue = astIterator; 491 } 492 } 493 return returnValue; 494 } 495 496 /** 497 * Determines whether enhanced for-loop variable should be checked or not. 498 * 499 * @param ast The ast to compare. 500 * @return true if enhanced for-loop variable should be checked. 501 */ 502 private boolean shouldCheckEnhancedForLoopVariable(DetailAST ast) { 503 return validateEnhancedForLoopVariable 504 || ast.getParent().getType() != TokenTypes.FOR_EACH_CLAUSE; 505 } 506 507 /** 508 * Determines whether unnamed variable should be checked or not. 509 * 510 * @param ast The ast to compare. 511 * @return true if unnamed variable should be checked. 512 */ 513 private boolean shouldCheckUnnamedVariable(DetailAST ast) { 514 return validateUnnamedVariables 515 || !"_".equals(TokenUtil.getIdent(ast).getText()); 516 } 517 518 /** 519 * Insert a parameter at the topmost scope stack. 520 * 521 * @param ast parameter definition AST, but not receiver parameter. 522 */ 523 private void insertParameter(DetailAST ast) { 524 final Map<String, FinalVariableCandidate> scope = scopeStack.peek().scope; 525 final DetailAST astNode = TokenUtil.getIdent(ast); 526 scope.put(astNode.getText(), new FinalVariableCandidate(astNode)); 527 } 528 529 /** 530 * Insert a variable at the topmost scope stack. 531 * 532 * @param variableAst the variable to insert. 533 */ 534 private void insertVariable(DetailAST variableAst) { 535 final Map<String, FinalVariableCandidate> scope = scopeStack.peek().scope; 536 final DetailAST astNode = TokenUtil.getIdent(variableAst); 537 final FinalVariableCandidate candidate = new FinalVariableCandidate(astNode); 538 // for-each variables are implicitly assigned 539 candidate.assigned = variableAst.getParent().getType() == TokenTypes.FOR_EACH_CLAUSE; 540 scope.put(astNode.getText(), candidate); 541 if (!isInitialized(variableAst)) { 542 scopeStack.peek().uninitializedVariables.add(astNode); 543 } 544 } 545 546 /** 547 * Check if VARIABLE_DEF is initialized or not. 548 * 549 * @param ast VARIABLE_DEF to be checked 550 * @return true if initialized 551 */ 552 private static boolean isInitialized(DetailAST ast) { 553 return ast.getLastChild().getType() == TokenTypes.ASSIGN; 554 } 555 556 /** 557 * Whether the ast is the first child of its parent. 558 * 559 * @param ast the ast to check. 560 * @return true if the ast is the first child of its parent. 561 */ 562 private static boolean isFirstChild(DetailAST ast) { 563 return ast.getPreviousSibling() == null; 564 } 565 566 /** 567 * Removes the final variable candidate from the Stack. 568 * 569 * @param ast variable to remove. 570 */ 571 private void removeFinalVariableCandidateFromStack(DetailAST ast) { 572 final Iterator<ScopeData> iterator = scopeStack.descendingIterator(); 573 while (iterator.hasNext()) { 574 final ScopeData scopeData = iterator.next(); 575 final Map<String, FinalVariableCandidate> scope = scopeData.scope; 576 final FinalVariableCandidate candidate = scope.get(ast.getText()); 577 DetailAST storedVariable = null; 578 if (candidate != null) { 579 storedVariable = candidate.variableIdent; 580 } 581 if (storedVariable != null && isSameVariables(storedVariable, ast)) { 582 if (shouldRemoveFinalVariableCandidate(scopeData, ast)) { 583 scope.remove(ast.getText()); 584 } 585 break; 586 } 587 } 588 } 589 590 /** 591 * Check if given parameter definition is a multiple type catch. 592 * 593 * @param parameterDefAst parameter definition 594 * @return true if it is a multiple type catch, false otherwise 595 */ 596 private static boolean isMultipleTypeCatch(DetailAST parameterDefAst) { 597 final DetailAST typeAst = parameterDefAst.findFirstToken(TokenTypes.TYPE); 598 return typeAst.findFirstToken(TokenTypes.BOR) != null; 599 } 600 601 /** 602 * Whether the final variable candidate should be removed from the list of final local variable 603 * candidates. 604 * 605 * @param scopeData the scope data of the variable. 606 * @param ast the variable ast. 607 * @return true, if the variable should be removed. 608 */ 609 private static boolean shouldRemoveFinalVariableCandidate(ScopeData scopeData, DetailAST ast) { 610 boolean shouldRemove = true; 611 for (DetailAST variable : scopeData.uninitializedVariables) { 612 if (variable.getText().equals(ast.getText())) { 613 // if the variable is declared outside the loop and initialized inside 614 // the loop, then it cannot be declared final, as it can be initialized 615 // more than once in this case 616 final DetailAST currAstLoopAstParent = getParentLoop(ast); 617 final DetailAST currVarLoopAstParent = getParentLoop(variable); 618 if (currAstLoopAstParent == currVarLoopAstParent) { 619 final FinalVariableCandidate candidate = scopeData.scope.get(ast.getText()); 620 shouldRemove = candidate.alreadyAssigned; 621 } 622 scopeData.uninitializedVariables.remove(variable); 623 break; 624 } 625 } 626 return shouldRemove; 627 } 628 629 /** 630 * Get the ast node of type {@link FinalVariableCandidate#LOOP_TYPES} that is the ancestor 631 * of the current ast node, if there is no such node, null is returned. 632 * 633 * @param ast ast node 634 * @return ast node of type {@code FinalVariableCandidate#LOOP_TYPES} that is the ancestor 635 * of the current ast node, null if no such node exists 636 */ 637 private static DetailAST getParentLoop(DetailAST ast) { 638 DetailAST parentLoop = ast; 639 while (parentLoop != null 640 && !LOOP_TYPES.get(parentLoop.getType())) { 641 parentLoop = parentLoop.getParent(); 642 } 643 return parentLoop; 644 } 645 646 /** 647 * Is Arithmetic operator. 648 * 649 * @param parentType token AST 650 * @return true is token type is in arithmetic operator 651 */ 652 private static boolean isAssignOperator(int parentType) { 653 return ASSIGN_OPERATOR_TYPES.get(parentType); 654 } 655 656 /** 657 * Checks if current variable is defined in 658 * {@link TokenTypes#FOR_INIT for-loop init}, e.g.: 659 * 660 * <p> 661 * {@code 662 * for (int i = 0, j = 0; i < j; i++) { . . . } 663 * } 664 * </p> 665 * {@code i, j} are defined in {@code TokenTypes#FOR_INIT for-loop init} 666 * 667 * @param variableDef variable definition node. 668 * @return true if variable is defined in {@code TokenTypes#FOR_INIT for-loop init} 669 */ 670 private static boolean isVariableInForInit(DetailAST variableDef) { 671 return variableDef.getParent().getType() == TokenTypes.FOR_INIT; 672 } 673 674 /** 675 * Checks if a parameter is within a method that has no implementation body. 676 * 677 * @param parameterDefAst the AST node representing the parameter definition 678 * @return true if the parameter is in a method without a body 679 */ 680 private static boolean isInMethodWithoutBody(DetailAST parameterDefAst) { 681 final DetailAST methodDefAst = parameterDefAst.getParent().getParent(); 682 return methodDefAst.findFirstToken(TokenTypes.SLIST) == null; 683 } 684 685 /** 686 * Check if current param is lambda's param. 687 * 688 * @param paramDef {@link TokenTypes#PARAMETER_DEF parameter def}. 689 * @return true if current param is lambda's param. 690 */ 691 private static boolean isInLambda(DetailAST paramDef) { 692 return paramDef.getParent().getParent().getType() == TokenTypes.LAMBDA; 693 } 694 695 /** 696 * Find the Class, Constructor, Enum, Method, or Field in which it is defined. 697 * 698 * @param ast Variable for which we want to find the scope in which it is defined 699 * @return ast The Class or Constructor or Method in which it is defined. 700 */ 701 private static DetailAST findFirstUpperNamedBlock(DetailAST ast) { 702 DetailAST astTraverse = ast; 703 while (!TokenUtil.isOfType(astTraverse, TokenTypes.METHOD_DEF, TokenTypes.CLASS_DEF, 704 TokenTypes.ENUM_DEF, TokenTypes.CTOR_DEF, TokenTypes.COMPACT_CTOR_DEF) 705 && !ScopeUtil.isClassFieldDef(astTraverse)) { 706 astTraverse = astTraverse.getParent(); 707 } 708 return astTraverse; 709 } 710 711 /** 712 * Check if both the Variables are same. 713 * 714 * @param ast1 Variable to compare 715 * @param ast2 Variable to compare 716 * @return true if both the variables are same, otherwise false 717 */ 718 private static boolean isSameVariables(DetailAST ast1, DetailAST ast2) { 719 final DetailAST classOrMethodOfAst1 = 720 findFirstUpperNamedBlock(ast1); 721 final DetailAST classOrMethodOfAst2 = 722 findFirstUpperNamedBlock(ast2); 723 return classOrMethodOfAst1 == classOrMethodOfAst2 && ast1.getText().equals(ast2.getText()); 724 } 725 726 /** Assignment state for a conditional branch that cannot complete normally. */ 727 private static final class ThrowingBranch { 728 729 /** Candidates that were unassigned before entering the branch, with their scopes. */ 730 private final Map<FinalVariableCandidate, ScopeData> unassigned = new HashMap<>(); 731 732 /** 733 * Captures the state before entering a throwing branch. 734 * 735 * @param scopes enclosing scopes 736 */ 737 private ThrowingBranch(Deque<ScopeData> scopes) { 738 for (ScopeData data : scopes) { 739 for (FinalVariableCandidate candidate : data.scope.values()) { 740 if (!candidate.assigned 741 && data.uninitializedVariables.contains(candidate.variableIdent)) { 742 unassigned.put(candidate, data); 743 } 744 } 745 } 746 } 747 748 /** 749 * Checks for a block ending in a throw outside exception handlers. 750 * Enclosing try statements are excluded because a handler can resume execution. 751 * 752 * @param ast block to inspect 753 * @return whether assignments in the block cannot reach following statements 754 */ 755 private static boolean isThrowingBranch(DetailAST ast) { 756 boolean result = TokenUtil.isOfType(ast.getLastChild().getPreviousSibling(), 757 TokenTypes.LITERAL_THROW); 758 for (DetailAST parent = ast; result && parent != null; 759 parent = parent.getParent()) { 760 result = parent.getType() != TokenTypes.LITERAL_TRY; 761 } 762 return result; 763 } 764 765 /** 766 * Restores assignment state on the path that does not enter the branch. 767 */ 768 private void restore() { 769 unassigned.forEach((candidate, data) -> { 770 candidate.assigned = false; 771 if (!data.uninitializedVariables.contains(candidate.variableIdent)) { 772 data.uninitializedVariables.add(candidate.variableIdent); 773 } 774 }); 775 } 776 777 } 778 779 /** 780 * Holder for the scope data. 781 */ 782 private static final class ScopeData { 783 784 /** Contains variable definitions. */ 785 private final Map<String, FinalVariableCandidate> scope = new HashMap<>(); 786 787 /** Contains definitions of uninitialized variables. */ 788 private final Deque<DetailAST> uninitializedVariables = new ArrayDeque<>(); 789 790 /** Contains definitions of previous scope uninitialized variables. */ 791 private Deque<DetailAST> prevScopeUninitializedVariables = new ArrayDeque<>(); 792 793 /** Whether there is a {@code break} in the scope. */ 794 private boolean containsBreak; 795 796 /** 797 * Creates a new {@code ScopeData} instance. 798 */ 799 private ScopeData() { 800 // no code by default 801 } 802 803 /** 804 * Searches for final local variable candidate for ast in the scope. 805 * 806 * @param ast ast. 807 * @return Optional of {@link FinalVariableCandidate}. 808 */ 809 /* package */ Optional<FinalVariableCandidate> 810 findFinalVariableCandidateForAst(DetailAST ast) { 811 Optional<FinalVariableCandidate> result = Optional.empty(); 812 DetailAST storedVariable = null; 813 final Optional<FinalVariableCandidate> candidate = 814 Optional.ofNullable(scope.get(ast.getText())); 815 if (candidate.isPresent()) { 816 storedVariable = candidate.orElseThrow().variableIdent; 817 } 818 if (storedVariable != null && isSameVariables(storedVariable, ast)) { 819 result = candidate; 820 } 821 return result; 822 } 823 824 } 825 826 /** Represents information about final local variable candidate. */ 827 private static final class FinalVariableCandidate { 828 829 /** Identifier token. */ 830 private final DetailAST variableIdent; 831 /** Whether the variable is assigned. */ 832 private boolean assigned; 833 /** Whether the variable is already assigned. */ 834 private boolean alreadyAssigned; 835 836 /** 837 * Creates new instance. 838 * 839 * @param variableIdent variable identifier. 840 */ 841 private FinalVariableCandidate(DetailAST variableIdent) { 842 this.variableIdent = variableIdent; 843 } 844 845 } 846 847}