001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2016 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.Arrays; 024import java.util.Deque; 025import java.util.HashMap; 026import java.util.Iterator; 027import java.util.Map; 028 029import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 030import com.puppycrawl.tools.checkstyle.api.DetailAST; 031import com.puppycrawl.tools.checkstyle.api.TokenTypes; 032import com.puppycrawl.tools.checkstyle.utils.ScopeUtils; 033 034/** 035 * <p> 036 * Ensures that local variables that never get their values changed, 037 * must be declared final. 038 * </p> 039 * <p> 040 * An example of how to configure the check to validate variable definition is: 041 * </p> 042 * <pre> 043 * <module name="FinalLocalVariable"> 044 * <property name="tokens" value="VARIABLE_DEF"/> 045 * </module> 046 * </pre> 047 * <p> 048 * By default, this Check skip final validation on 049 * <a href = "http://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.14.2"> 050 * Enhanced For-Loop</a> 051 * </p> 052 * <p> 053 * Option 'validateEnhancedForLoopVariable' could be used to make Check to validate even variable 054 * from Enhanced For Loop. 055 * </p> 056 * <p> 057 * An example of how to configure the check so that it also validates enhanced For Loop Variable is: 058 * </p> 059 * <pre> 060 * <module name="FinalLocalVariable"> 061 * <property name="tokens" value="VARIABLE_DEF"/> 062 * <property name="validateEnhancedForLoopVariable" value="true"/> 063 * </module> 064 * </pre> 065 * <p>Example:</p> 066 * <p> 067 * {@code 068 * for (int number : myNumbers) { // violation 069 * System.out.println(number); 070 * } 071 * } 072 * </p> 073 * @author k_gibbs, r_auckenthaler 074 * @author Vladislav Lisetskiy 075 */ 076public class FinalLocalVariableCheck extends AbstractCheck { 077 078 /** 079 * A key is pointing to the warning message text in "messages.properties" 080 * file. 081 */ 082 public static final String MSG_KEY = "final.variable"; 083 084 /** 085 * Assign operator types. 086 */ 087 private static final int[] ASSIGN_OPERATOR_TYPES = { 088 TokenTypes.POST_INC, 089 TokenTypes.POST_DEC, 090 TokenTypes.ASSIGN, 091 TokenTypes.PLUS_ASSIGN, 092 TokenTypes.MINUS_ASSIGN, 093 TokenTypes.STAR_ASSIGN, 094 TokenTypes.DIV_ASSIGN, 095 TokenTypes.MOD_ASSIGN, 096 TokenTypes.SR_ASSIGN, 097 TokenTypes.BSR_ASSIGN, 098 TokenTypes.SL_ASSIGN, 099 TokenTypes.BAND_ASSIGN, 100 TokenTypes.BXOR_ASSIGN, 101 TokenTypes.BOR_ASSIGN, 102 TokenTypes.INC, 103 TokenTypes.DEC, 104 }; 105 106 /** 107 * Loop types. 108 */ 109 private static final int[] LOOP_TYPES = { 110 TokenTypes.LITERAL_FOR, 111 TokenTypes.LITERAL_WHILE, 112 TokenTypes.LITERAL_DO, 113 }; 114 115 /** Scope Deque. */ 116 private final Deque<ScopeData> scopeStack = new ArrayDeque<>(); 117 118 /** Uninitialized variables of previous scope. */ 119 private final Deque<Deque<DetailAST>> prevScopeUninitializedVariables = 120 new ArrayDeque<>(); 121 122 /** Controls whether to check enhanced for-loop variable. */ 123 private boolean validateEnhancedForLoopVariable; 124 125 static { 126 // Array sorting for binary search 127 Arrays.sort(ASSIGN_OPERATOR_TYPES); 128 Arrays.sort(LOOP_TYPES); 129 } 130 131 /** 132 * Whether to check enhanced for-loop variable or not. 133 * @param validateEnhancedForLoopVariable whether to check for-loop variable 134 */ 135 public final void setValidateEnhancedForLoopVariable(boolean validateEnhancedForLoopVariable) { 136 this.validateEnhancedForLoopVariable = validateEnhancedForLoopVariable; 137 } 138 139 @Override 140 public int[] getRequiredTokens() { 141 return new int[] { 142 TokenTypes.IDENT, 143 TokenTypes.CTOR_DEF, 144 TokenTypes.METHOD_DEF, 145 TokenTypes.SLIST, 146 TokenTypes.OBJBLOCK, 147 }; 148 } 149 150 @Override 151 public int[] getDefaultTokens() { 152 return new int[] { 153 TokenTypes.IDENT, 154 TokenTypes.CTOR_DEF, 155 TokenTypes.METHOD_DEF, 156 TokenTypes.SLIST, 157 TokenTypes.OBJBLOCK, 158 TokenTypes.VARIABLE_DEF, 159 }; 160 } 161 162 @Override 163 public int[] getAcceptableTokens() { 164 return new int[] { 165 TokenTypes.IDENT, 166 TokenTypes.CTOR_DEF, 167 TokenTypes.METHOD_DEF, 168 TokenTypes.SLIST, 169 TokenTypes.OBJBLOCK, 170 TokenTypes.VARIABLE_DEF, 171 TokenTypes.PARAMETER_DEF, 172 }; 173 } 174 175 @Override 176 public void visitToken(DetailAST ast) { 177 switch (ast.getType()) { 178 case TokenTypes.OBJBLOCK: 179 case TokenTypes.METHOD_DEF: 180 case TokenTypes.CTOR_DEF: 181 scopeStack.push(new ScopeData()); 182 break; 183 case TokenTypes.SLIST: 184 if (ast.getParent().getType() != TokenTypes.CASE_GROUP 185 || ast.getParent().getParent().findFirstToken(TokenTypes.CASE_GROUP) 186 == ast.getParent()) { 187 storePrevScopeUninitializedVariableData(); 188 scopeStack.push(new ScopeData()); 189 } 190 break; 191 case TokenTypes.PARAMETER_DEF: 192 if (!isInLambda(ast) 193 && !ast.branchContains(TokenTypes.FINAL) 194 && !isInAbstractOrNativeMethod(ast) 195 && !ScopeUtils.isInInterfaceBlock(ast)) { 196 insertParameter(ast); 197 } 198 break; 199 case TokenTypes.VARIABLE_DEF: 200 if (ast.getParent().getType() != TokenTypes.OBJBLOCK 201 && !ast.branchContains(TokenTypes.FINAL) 202 && !isVariableInForInit(ast) 203 && shouldCheckEnhancedForLoopVariable(ast)) { 204 insertVariable(ast); 205 } 206 break; 207 208 case TokenTypes.IDENT: 209 final int parentType = ast.getParent().getType(); 210 if (isAssignOperator(parentType) 211 && isFirstChild(ast)) { 212 removeVariable(ast); 213 } 214 break; 215 216 default: 217 throw new IllegalStateException("Incorrect token type"); 218 } 219 } 220 221 @Override 222 public void leaveToken(DetailAST ast) { 223 Map<String, DetailAST> scope = null; 224 switch (ast.getType()) { 225 case TokenTypes.OBJBLOCK: 226 case TokenTypes.CTOR_DEF: 227 case TokenTypes.METHOD_DEF: 228 scope = scopeStack.pop().scope; 229 break; 230 case TokenTypes.SLIST: 231 final Deque<DetailAST> prevScopeUnitializedVariableData = 232 prevScopeUninitializedVariables.peek(); 233 if (ast.getParent().getType() != TokenTypes.CASE_GROUP 234 || findLastChildWhichContainsSpecifiedToken(ast.getParent().getParent(), 235 TokenTypes.CASE_GROUP, TokenTypes.SLIST) == ast.getParent()) { 236 scope = scopeStack.pop().scope; 237 prevScopeUninitializedVariables.pop(); 238 } 239 final DetailAST parent = ast.getParent(); 240 if (shouldUpdateUninitializedVariables(parent)) { 241 updateUninitializedVariables(prevScopeUnitializedVariableData); 242 } 243 break; 244 default: 245 // do nothing 246 } 247 if (scope != null) { 248 for (DetailAST node : scope.values()) { 249 log(node.getLineNo(), node.getColumnNo(), MSG_KEY, node.getText()); 250 } 251 } 252 } 253 254 /** 255 * Store un-initialized variables in a temporary stack for future use. 256 */ 257 private void storePrevScopeUninitializedVariableData() { 258 final ScopeData scopeData = scopeStack.peek(); 259 final Deque<DetailAST> prevScopeUnitializedVariableData = 260 new ArrayDeque<>(); 261 for (DetailAST variable : scopeData.uninitializedVariables) { 262 prevScopeUnitializedVariableData.push(variable); 263 } 264 prevScopeUninitializedVariables.push(prevScopeUnitializedVariableData); 265 } 266 267 /** 268 * Update current scope data uninitialized variable according to the previous scope data. 269 * @param prevScopeUnitializedVariableData variable for previous stack of uninitialized 270 * variables 271 */ 272 private void updateUninitializedVariables(Deque<DetailAST> 273 prevScopeUnitializedVariableData) { 274 // Check for only previous scope 275 for (DetailAST variable : prevScopeUnitializedVariableData) { 276 for (ScopeData scopeData : scopeStack) { 277 final DetailAST storedVariable = scopeData.scope.get(variable.getText()); 278 if (storedVariable != null && isSameVariables(storedVariable, variable) 279 && !scopeData.uninitializedVariables.contains(storedVariable)) { 280 scopeData.uninitializedVariables.push(variable); 281 } 282 } 283 } 284 // Check for rest of the scope 285 for (Deque<DetailAST> unitializedVariableData : prevScopeUninitializedVariables) { 286 for (DetailAST variable : unitializedVariableData) { 287 for (ScopeData scopeData : scopeStack) { 288 final DetailAST storedVariable = scopeData.scope.get(variable.getText()); 289 if (storedVariable != null 290 && isSameVariables(storedVariable, variable) 291 && !scopeData.uninitializedVariables.contains(storedVariable)) { 292 scopeData.uninitializedVariables.push(variable); 293 } 294 } 295 } 296 } 297 } 298 299 /** 300 * If token is LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, or LITERAL_ELSE, then do not 301 * update the uninitialized variables. 302 * @param ast token to be checked 303 * @return true if should be updated, else false 304 */ 305 private boolean shouldUpdateUninitializedVariables(DetailAST ast) { 306 return ast.getType() != TokenTypes.LITERAL_TRY 307 && ast.getType() != TokenTypes.LITERAL_CATCH 308 && ast.getType() != TokenTypes.LITERAL_FINALLY 309 && ast.getType() != TokenTypes.LITERAL_ELSE; 310 } 311 312 /** 313 * Returns the last child token that makes a specified type and contains containType in 314 * its branch. 315 * @param ast token to be tested 316 * @param childType the token type to match 317 * @param containType the token type which has to be present in the branch 318 * @return the matching token, or null if no match 319 */ 320 public DetailAST findLastChildWhichContainsSpecifiedToken(DetailAST ast, int childType, 321 int containType) { 322 DetailAST returnValue = null; 323 for (DetailAST astIterator = ast.getFirstChild(); astIterator != null; 324 astIterator = astIterator.getNextSibling()) { 325 if (astIterator.getType() == childType && astIterator.branchContains(containType)) { 326 returnValue = astIterator; 327 } 328 } 329 return returnValue; 330 } 331 332 /** 333 * Determines whether enhanced for-loop variable should be checked or not. 334 * @param ast The ast to compare. 335 * @return true if enhanced for-loop variable should be checked. 336 */ 337 private boolean shouldCheckEnhancedForLoopVariable(DetailAST ast) { 338 return validateEnhancedForLoopVariable 339 || ast.getParent().getType() != TokenTypes.FOR_EACH_CLAUSE; 340 } 341 342 /** 343 * Insert a parameter at the topmost scope stack. 344 * @param ast the variable to insert. 345 */ 346 private void insertParameter(DetailAST ast) { 347 final Map<String, DetailAST> scope = scopeStack.peek().scope; 348 final DetailAST astNode = ast.findFirstToken(TokenTypes.IDENT); 349 scope.put(astNode.getText(), astNode); 350 } 351 352 /** 353 * Insert a variable at the topmost scope stack. 354 * @param ast the variable to insert. 355 */ 356 private void insertVariable(DetailAST ast) { 357 final Map<String, DetailAST> scope = scopeStack.peek().scope; 358 final DetailAST astNode = ast.findFirstToken(TokenTypes.IDENT); 359 scope.put(astNode.getText(), astNode); 360 if (!isInitialized(astNode)) { 361 scopeStack.peek().uninitializedVariables.add(astNode); 362 } 363 } 364 365 /** 366 * Check if VARIABLE_DEF is initialized or not. 367 * @param ast VARIABLE_DEF to be checked 368 * @return true if initialized 369 */ 370 private static boolean isInitialized(DetailAST ast) { 371 return ast.getParent().getLastChild().getType() == TokenTypes.ASSIGN; 372 } 373 374 /** 375 * Whether the ast is the first child of its parent. 376 * @param ast the ast to check. 377 * @return true if the ast is the first child of its parent. 378 */ 379 private static boolean isFirstChild(DetailAST ast) { 380 return ast.getPreviousSibling() == null; 381 } 382 383 /** 384 * Remove the variable from the Stack. 385 * @param ast Variable to remove 386 */ 387 private void removeVariable(DetailAST ast) { 388 final Iterator<ScopeData> iterator = scopeStack.descendingIterator(); 389 while (iterator.hasNext()) { 390 final ScopeData scopeData = iterator.next(); 391 final Map<String, DetailAST> scope = scopeData.scope; 392 final DetailAST storedVariable = scope.get(ast.getText()); 393 if (storedVariable != null && isSameVariables(storedVariable, ast)) { 394 if (shouldRemoveVariable(scopeData, ast)) { 395 scope.remove(ast.getText()); 396 } 397 break; 398 } 399 } 400 } 401 402 /** 403 * Whether the variable should be removed from the list of final local variable 404 * candidates. 405 * @param scopeData the scope data of the variable. 406 * @param ast the variable ast. 407 * @return true, if the variable should be removed. 408 */ 409 private static boolean shouldRemoveVariable(ScopeData scopeData, DetailAST ast) { 410 boolean shouldRemove = true; 411 for (DetailAST variable : scopeData.uninitializedVariables) { 412 if (variable.getText().equals(ast.getText())) { 413 414 // if the variable is declared outside the loop and initialized inside 415 // the loop, then it cannot be declared final, as it can be initialized 416 // more than once in this case 417 if (isInTheSameLoop(variable, ast)) { 418 shouldRemove = false; 419 } 420 scopeData.uninitializedVariables.remove(variable); 421 break; 422 } 423 } 424 return shouldRemove; 425 } 426 427 /** 428 * Is Arithmetic operator. 429 * @param parentType token AST 430 * @return true is token type is in arithmetic operator 431 */ 432 private static boolean isAssignOperator(int parentType) { 433 return Arrays.binarySearch(ASSIGN_OPERATOR_TYPES, parentType) >= 0; 434 } 435 436 /** 437 * Checks if current variable is defined in 438 * {@link TokenTypes#FOR_INIT for-loop init}, e.g.: 439 * <p> 440 * {@code 441 * for (int i = 0, j = 0; i < j; i++) { . . . } 442 * } 443 * </p> 444 * {@code i, j} are defined in {@link TokenTypes#FOR_INIT for-loop init} 445 * @param variableDef variable definition node. 446 * @return true if variable is defined in {@link TokenTypes#FOR_INIT for-loop init} 447 */ 448 private static boolean isVariableInForInit(DetailAST variableDef) { 449 return variableDef.getParent().getType() == TokenTypes.FOR_INIT; 450 } 451 452 /** 453 * Determines whether an AST is a descendant of an abstract or native method. 454 * @param ast the AST to check. 455 * @return true if ast is a descendant of an abstract or native method. 456 */ 457 private static boolean isInAbstractOrNativeMethod(DetailAST ast) { 458 boolean abstractOrNative = false; 459 DetailAST parent = ast.getParent(); 460 while (parent != null && !abstractOrNative) { 461 if (parent.getType() == TokenTypes.METHOD_DEF) { 462 final DetailAST modifiers = 463 parent.findFirstToken(TokenTypes.MODIFIERS); 464 abstractOrNative = modifiers.branchContains(TokenTypes.ABSTRACT) 465 || modifiers.branchContains(TokenTypes.LITERAL_NATIVE); 466 } 467 parent = parent.getParent(); 468 } 469 return abstractOrNative; 470 } 471 472 /** 473 * Check if current param is lambda's param. 474 * @param paramDef {@link TokenTypes#PARAMETER_DEF parameter def}. 475 * @return true if current param is lambda's param. 476 */ 477 private static boolean isInLambda(DetailAST paramDef) { 478 return paramDef.getParent().getParent().getType() == TokenTypes.LAMBDA; 479 } 480 481 /** 482 * Find the Class, Constructor, Enum or Method in which it is defined. 483 * @param ast Variable for which we want to find the scope in which it is defined 484 * @return ast The Class or Constructor or Method in which it is defined. 485 */ 486 private static DetailAST findFirstUpperNamedBlock(DetailAST ast) { 487 DetailAST astTraverse = ast; 488 while (astTraverse.getType() != TokenTypes.METHOD_DEF 489 && astTraverse.getType() != TokenTypes.CLASS_DEF 490 && astTraverse.getType() != TokenTypes.ENUM_DEF 491 && astTraverse.getType() != TokenTypes.CTOR_DEF) { 492 astTraverse = astTraverse.getParent(); 493 } 494 return astTraverse; 495 } 496 497 /** 498 * Check if both the Variables are same. 499 * @param ast1 Variable to compare 500 * @param ast2 Variable to compare 501 * @return true if both the variables are same, otherwise false 502 */ 503 private static boolean isSameVariables(DetailAST ast1, DetailAST ast2) { 504 final DetailAST classOrMethodOfAst1 = 505 findFirstUpperNamedBlock(ast1); 506 final DetailAST classOrMethodOfAst2 = 507 findFirstUpperNamedBlock(ast2); 508 return classOrMethodOfAst1 == classOrMethodOfAst2; 509 } 510 511 /** 512 * Check if both the variables are in the same loop. 513 * @param ast1 variable to compare. 514 * @param ast2 variable to compare. 515 * @return true if both the variables are in the same loop. 516 */ 517 private static boolean isInTheSameLoop(DetailAST ast1, DetailAST ast2) { 518 DetailAST loop1 = ast1.getParent(); 519 while (loop1 != null && !isLoopAst(loop1.getType())) { 520 loop1 = loop1.getParent(); 521 } 522 DetailAST loop2 = ast2.getParent(); 523 while (loop2 != null && !isLoopAst(loop2.getType())) { 524 loop2 = loop2.getParent(); 525 } 526 return loop1 == null && loop2 == null 527 || loop1 != null && loop1 == loop2; 528 } 529 530 /** 531 * Checks whether the ast is a loop. 532 * @param ast the ast to check. 533 * @return true if the ast is a loop. 534 */ 535 private static boolean isLoopAst(int ast) { 536 return Arrays.binarySearch(LOOP_TYPES, ast) >= 0; 537 } 538 539 /** 540 * Holder for the scope data. 541 */ 542 private static class ScopeData { 543 /** Contains variable definitions. */ 544 private final Map<String, DetailAST> scope = new HashMap<>(); 545 546 /** Contains definitions of uninitialized variables. */ 547 private final Deque<DetailAST> uninitializedVariables = new ArrayDeque<>(); 548 } 549}