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