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