001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2022 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.HashSet; 026import java.util.LinkedList; 027import java.util.List; 028import java.util.Set; 029import java.util.stream.Collectors; 030 031import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 032import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 033import com.puppycrawl.tools.checkstyle.api.DetailAST; 034import com.puppycrawl.tools.checkstyle.api.TokenTypes; 035import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 036 037/** 038 * <p> 039 * Checks that for loop control variables are not modified 040 * inside the for block. An example is: 041 * </p> 042 * <pre> 043 * for (int i = 0; i < 1; i++) { 044 * i++; // violation 045 * } 046 * </pre> 047 * <p> 048 * Rationale: If the control variable is modified inside the loop 049 * body, the program flow becomes more difficult to follow. 050 * See <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-14.html#jls-14.14"> 051 * FOR statement</a> specification for more details. 052 * </p> 053 * <p> 054 * Such loop would be suppressed: 055 * </p> 056 * <pre> 057 * for (int i = 0; i < 10;) { 058 * i++; 059 * } 060 * </pre> 061 * <p> 062 * NOTE:The check works with only primitive type variables. 063 * The check will not work for arrays used as control variable.An example is 064 * </p> 065 * <pre> 066 * for (int a[]={0};a[0] < 10;a[0]++) { 067 * a[0]++; // it will skip this violation 068 * } 069 * </pre> 070 * <ul> 071 * <li> 072 * Property {@code skipEnhancedForLoopVariable} - Control whether to check 073 * <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-14.html#jls-14.14.2"> 074 * enhanced for-loop</a> variable. 075 * Type is {@code boolean}. 076 * Default value is {@code false}. 077 * </li> 078 * </ul> 079 * <p> 080 * To configure the check: 081 * </p> 082 * <pre> 083 * <module name="ModifiedControlVariable"/> 084 * </pre> 085 * <p> 086 * Example: 087 * </p> 088 * <pre> 089 * for(int i=0;i < 8;i++) { 090 * i++; // violation, control variable modified 091 * } 092 * String args1[]={"Coding", "block"}; 093 * for (String arg: args1) { 094 * arg = arg.trim(); // violation, control variable modified 095 * } 096 * </pre> 097 * <p> 098 * By default, This Check validates 099 * <a href = "https://docs.oracle.com/javase/specs/jls/se11/html/jls-14.html#jls-14.14.2"> 100 * Enhanced For-Loop</a>. 101 * </p> 102 * <p> 103 * Option 'skipEnhancedForLoopVariable' could be used to skip check of variable 104 * from Enhanced For Loop. 105 * </p> 106 * <p> 107 * An example of how to configure the check so that it skips enhanced For Loop Variable is: 108 * </p> 109 * <pre> 110 * <module name="ModifiedControlVariable"> 111 * <property name="skipEnhancedForLoopVariable" value="true"/> 112 * </module> 113 * </pre> 114 * <p>Example:</p> 115 * 116 * <pre> 117 * for(int i=0;i < 8;i++) { 118 * i++; // violation, control variable modified 119 * } 120 * String args1[]={"Coding", "block"}; 121 * for (String arg: args1) { 122 * arg = arg.trim(); // ok 123 * } 124 * </pre> 125 * <p> 126 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 127 * </p> 128 * <p> 129 * Violation Message Keys: 130 * </p> 131 * <ul> 132 * <li> 133 * {@code modified.control.variable} 134 * </li> 135 * </ul> 136 * 137 * @since 3.5 138 */ 139@FileStatefulCheck 140public final class ModifiedControlVariableCheck extends AbstractCheck { 141 142 /** 143 * A key is pointing to the warning message text in "messages.properties" 144 * file. 145 */ 146 public static final String MSG_KEY = "modified.control.variable"; 147 148 /** 149 * Message thrown with IllegalStateException. 150 */ 151 private static final String ILLEGAL_TYPE_OF_TOKEN = "Illegal type of token: "; 152 153 /** Operations which can change control variable in update part of the loop. */ 154 private static final BitSet MUTATION_OPERATIONS = TokenUtil.asBitSet( 155 TokenTypes.POST_INC, 156 TokenTypes.POST_DEC, 157 TokenTypes.DEC, 158 TokenTypes.INC, 159 TokenTypes.ASSIGN); 160 161 /** Stack of block parameters. */ 162 private final Deque<Deque<String>> variableStack = new ArrayDeque<>(); 163 164 /** 165 * Control whether to check 166 * <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-14.html#jls-14.14.2"> 167 * enhanced for-loop</a> variable. 168 */ 169 private boolean skipEnhancedForLoopVariable; 170 171 /** 172 * Setter to control whether to check 173 * <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-14.html#jls-14.14.2"> 174 * enhanced for-loop</a> variable. 175 * 176 * @param skipEnhancedForLoopVariable whether to skip enhanced for-loop variable 177 */ 178 public void setSkipEnhancedForLoopVariable(boolean skipEnhancedForLoopVariable) { 179 this.skipEnhancedForLoopVariable = skipEnhancedForLoopVariable; 180 } 181 182 @Override 183 public int[] getDefaultTokens() { 184 return getRequiredTokens(); 185 } 186 187 @Override 188 public int[] getRequiredTokens() { 189 return new int[] { 190 TokenTypes.OBJBLOCK, 191 TokenTypes.LITERAL_FOR, 192 TokenTypes.FOR_ITERATOR, 193 TokenTypes.FOR_EACH_CLAUSE, 194 TokenTypes.ASSIGN, 195 TokenTypes.PLUS_ASSIGN, 196 TokenTypes.MINUS_ASSIGN, 197 TokenTypes.STAR_ASSIGN, 198 TokenTypes.DIV_ASSIGN, 199 TokenTypes.MOD_ASSIGN, 200 TokenTypes.SR_ASSIGN, 201 TokenTypes.BSR_ASSIGN, 202 TokenTypes.SL_ASSIGN, 203 TokenTypes.BAND_ASSIGN, 204 TokenTypes.BXOR_ASSIGN, 205 TokenTypes.BOR_ASSIGN, 206 TokenTypes.INC, 207 TokenTypes.POST_INC, 208 TokenTypes.DEC, 209 TokenTypes.POST_DEC, 210 }; 211 } 212 213 @Override 214 public int[] getAcceptableTokens() { 215 return getRequiredTokens(); 216 } 217 218 @Override 219 public void beginTree(DetailAST rootAST) { 220 // clear data 221 variableStack.clear(); 222 } 223 224 @Override 225 public void visitToken(DetailAST ast) { 226 switch (ast.getType()) { 227 case TokenTypes.OBJBLOCK: 228 enterBlock(); 229 break; 230 case TokenTypes.LITERAL_FOR: 231 case TokenTypes.FOR_ITERATOR: 232 case TokenTypes.FOR_EACH_CLAUSE: 233 // we need that Tokens only at leaveToken() 234 break; 235 case TokenTypes.ASSIGN: 236 case TokenTypes.PLUS_ASSIGN: 237 case TokenTypes.MINUS_ASSIGN: 238 case TokenTypes.STAR_ASSIGN: 239 case TokenTypes.DIV_ASSIGN: 240 case TokenTypes.MOD_ASSIGN: 241 case TokenTypes.SR_ASSIGN: 242 case TokenTypes.BSR_ASSIGN: 243 case TokenTypes.SL_ASSIGN: 244 case TokenTypes.BAND_ASSIGN: 245 case TokenTypes.BXOR_ASSIGN: 246 case TokenTypes.BOR_ASSIGN: 247 case TokenTypes.INC: 248 case TokenTypes.POST_INC: 249 case TokenTypes.DEC: 250 case TokenTypes.POST_DEC: 251 checkIdent(ast); 252 break; 253 default: 254 throw new IllegalStateException(ILLEGAL_TYPE_OF_TOKEN + ast); 255 } 256 } 257 258 @Override 259 public void leaveToken(DetailAST ast) { 260 switch (ast.getType()) { 261 case TokenTypes.FOR_ITERATOR: 262 leaveForIter(ast.getParent()); 263 break; 264 case TokenTypes.FOR_EACH_CLAUSE: 265 if (!skipEnhancedForLoopVariable) { 266 final DetailAST paramDef = ast.findFirstToken(TokenTypes.VARIABLE_DEF); 267 leaveForEach(paramDef); 268 } 269 break; 270 case TokenTypes.LITERAL_FOR: 271 leaveForDef(ast); 272 break; 273 case TokenTypes.OBJBLOCK: 274 exitBlock(); 275 break; 276 case TokenTypes.ASSIGN: 277 case TokenTypes.PLUS_ASSIGN: 278 case TokenTypes.MINUS_ASSIGN: 279 case TokenTypes.STAR_ASSIGN: 280 case TokenTypes.DIV_ASSIGN: 281 case TokenTypes.MOD_ASSIGN: 282 case TokenTypes.SR_ASSIGN: 283 case TokenTypes.BSR_ASSIGN: 284 case TokenTypes.SL_ASSIGN: 285 case TokenTypes.BAND_ASSIGN: 286 case TokenTypes.BXOR_ASSIGN: 287 case TokenTypes.BOR_ASSIGN: 288 case TokenTypes.INC: 289 case TokenTypes.POST_INC: 290 case TokenTypes.DEC: 291 case TokenTypes.POST_DEC: 292 // we need that Tokens only at visitToken() 293 break; 294 default: 295 throw new IllegalStateException(ILLEGAL_TYPE_OF_TOKEN + ast); 296 } 297 } 298 299 /** 300 * Enters an inner class, which requires a new variable set. 301 */ 302 private void enterBlock() { 303 variableStack.push(new ArrayDeque<>()); 304 } 305 306 /** 307 * Leave an inner class, so restore variable set. 308 */ 309 private void exitBlock() { 310 variableStack.pop(); 311 } 312 313 /** 314 * Get current variable stack. 315 * 316 * @return current variable stack 317 */ 318 private Deque<String> getCurrentVariables() { 319 return variableStack.peek(); 320 } 321 322 /** 323 * Check if ident is parameter. 324 * 325 * @param ast ident to check. 326 */ 327 private void checkIdent(DetailAST ast) { 328 final Deque<String> currentVariables = getCurrentVariables(); 329 final DetailAST identAST = ast.getFirstChild(); 330 331 if (identAST != null && identAST.getType() == TokenTypes.IDENT 332 && currentVariables.contains(identAST.getText())) { 333 log(ast, MSG_KEY, identAST.getText()); 334 } 335 } 336 337 /** 338 * Push current variables to the stack. 339 * 340 * @param ast a for definition. 341 */ 342 private void leaveForIter(DetailAST ast) { 343 final Set<String> variablesToPutInScope = getVariablesManagedByForLoop(ast); 344 for (String variableName : variablesToPutInScope) { 345 getCurrentVariables().push(variableName); 346 } 347 } 348 349 /** 350 * Determines which variable are specific to for loop and should not be 351 * change by inner loop body. 352 * 353 * @param ast For Loop 354 * @return Set of Variable Name which are managed by for 355 */ 356 private static Set<String> getVariablesManagedByForLoop(DetailAST ast) { 357 final Set<String> initializedVariables = getForInitVariables(ast); 358 final Set<String> iteratingVariables = getForIteratorVariables(ast); 359 return initializedVariables.stream().filter(iteratingVariables::contains) 360 .collect(Collectors.toSet()); 361 } 362 363 /** 364 * Push current variables to the stack. 365 * 366 * @param paramDef a for-each clause variable 367 */ 368 private void leaveForEach(DetailAST paramDef) { 369 final DetailAST paramName = paramDef.findFirstToken(TokenTypes.IDENT); 370 getCurrentVariables().push(paramName.getText()); 371 } 372 373 /** 374 * Pops the variables from the stack. 375 * 376 * @param ast a for definition. 377 */ 378 private void leaveForDef(DetailAST ast) { 379 final DetailAST forInitAST = ast.findFirstToken(TokenTypes.FOR_INIT); 380 if (forInitAST == null) { 381 if (!skipEnhancedForLoopVariable) { 382 // this is for-each loop, just pop variables 383 getCurrentVariables().pop(); 384 } 385 } 386 else { 387 final Set<String> variablesManagedByForLoop = getVariablesManagedByForLoop(ast); 388 popCurrentVariables(variablesManagedByForLoop.size()); 389 } 390 } 391 392 /** 393 * Pops given number of variables from currentVariables. 394 * 395 * @param count Count of variables to be popped from currentVariables 396 */ 397 private void popCurrentVariables(int count) { 398 for (int i = 0; i < count; i++) { 399 getCurrentVariables().pop(); 400 } 401 } 402 403 /** 404 * Get all variables initialized In init part of for loop. 405 * 406 * @param ast for loop token 407 * @return set of variables initialized in for loop 408 */ 409 private static Set<String> getForInitVariables(DetailAST ast) { 410 final Set<String> initializedVariables = new HashSet<>(); 411 final DetailAST forInitAST = ast.findFirstToken(TokenTypes.FOR_INIT); 412 413 for (DetailAST parameterDefAST = forInitAST.findFirstToken(TokenTypes.VARIABLE_DEF); 414 parameterDefAST != null; 415 parameterDefAST = parameterDefAST.getNextSibling()) { 416 if (parameterDefAST.getType() == TokenTypes.VARIABLE_DEF) { 417 final DetailAST param = 418 parameterDefAST.findFirstToken(TokenTypes.IDENT); 419 420 initializedVariables.add(param.getText()); 421 } 422 } 423 return initializedVariables; 424 } 425 426 /** 427 * Get all variables which for loop iterating part change in every loop. 428 * 429 * @param ast for loop literal(TokenTypes.LITERAL_FOR) 430 * @return names of variables change in iterating part of for 431 */ 432 private static Set<String> getForIteratorVariables(DetailAST ast) { 433 final Set<String> iteratorVariables = new HashSet<>(); 434 final DetailAST forIteratorAST = ast.findFirstToken(TokenTypes.FOR_ITERATOR); 435 final DetailAST forUpdateListAST = forIteratorAST.findFirstToken(TokenTypes.ELIST); 436 437 findChildrenOfExpressionType(forUpdateListAST).stream() 438 .filter(iteratingExpressionAST -> { 439 return MUTATION_OPERATIONS.get(iteratingExpressionAST.getType()); 440 }).forEach(iteratingExpressionAST -> { 441 final DetailAST oneVariableOperatorChild = iteratingExpressionAST.getFirstChild(); 442 iteratorVariables.add(oneVariableOperatorChild.getText()); 443 }); 444 445 return iteratorVariables; 446 } 447 448 /** 449 * Find all child of given AST of type TokenType.EXPR 450 * 451 * @param ast parent of expressions to find 452 * @return all child of given ast 453 */ 454 private static List<DetailAST> findChildrenOfExpressionType(DetailAST ast) { 455 final List<DetailAST> foundExpressions = new LinkedList<>(); 456 if (ast != null) { 457 for (DetailAST iteratingExpressionAST = ast.findFirstToken(TokenTypes.EXPR); 458 iteratingExpressionAST != null; 459 iteratingExpressionAST = iteratingExpressionAST.getNextSibling()) { 460 if (iteratingExpressionAST.getType() == TokenTypes.EXPR) { 461 foundExpressions.add(iteratingExpressionAST.getFirstChild()); 462 } 463 } 464 } 465 return foundExpressions; 466 } 467 468}