001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 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 * <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 * Setter to control whether to check 110 * <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-14.html#jls-14.14.2"> 111 * enhanced for-loop</a> variable. 112 * 113 * @param skipEnhancedForLoopVariable whether to skip enhanced for-loop variable 114 * @since 6.8 115 */ 116 public void setSkipEnhancedForLoopVariable(boolean skipEnhancedForLoopVariable) { 117 this.skipEnhancedForLoopVariable = skipEnhancedForLoopVariable; 118 } 119 120 @Override 121 public int[] getDefaultTokens() { 122 return getRequiredTokens(); 123 } 124 125 @Override 126 public int[] getRequiredTokens() { 127 return new int[] { 128 TokenTypes.OBJBLOCK, 129 TokenTypes.LITERAL_FOR, 130 TokenTypes.FOR_ITERATOR, 131 TokenTypes.FOR_EACH_CLAUSE, 132 TokenTypes.ASSIGN, 133 TokenTypes.PLUS_ASSIGN, 134 TokenTypes.MINUS_ASSIGN, 135 TokenTypes.STAR_ASSIGN, 136 TokenTypes.DIV_ASSIGN, 137 TokenTypes.MOD_ASSIGN, 138 TokenTypes.SR_ASSIGN, 139 TokenTypes.BSR_ASSIGN, 140 TokenTypes.SL_ASSIGN, 141 TokenTypes.BAND_ASSIGN, 142 TokenTypes.BXOR_ASSIGN, 143 TokenTypes.BOR_ASSIGN, 144 TokenTypes.INC, 145 TokenTypes.POST_INC, 146 TokenTypes.DEC, 147 TokenTypes.POST_DEC, 148 }; 149 } 150 151 @Override 152 public int[] getAcceptableTokens() { 153 return getRequiredTokens(); 154 } 155 156 @Override 157 public void beginTree(DetailAST rootAST) { 158 // clear data 159 variableStack.clear(); 160 } 161 162 @Override 163 public void visitToken(DetailAST ast) { 164 switch (ast.getType()) { 165 case TokenTypes.OBJBLOCK: 166 enterBlock(); 167 break; 168 case TokenTypes.LITERAL_FOR: 169 case TokenTypes.FOR_ITERATOR: 170 case TokenTypes.FOR_EACH_CLAUSE: 171 // we need that Tokens only at leaveToken() 172 break; 173 case TokenTypes.ASSIGN: 174 case TokenTypes.PLUS_ASSIGN: 175 case TokenTypes.MINUS_ASSIGN: 176 case TokenTypes.STAR_ASSIGN: 177 case TokenTypes.DIV_ASSIGN: 178 case TokenTypes.MOD_ASSIGN: 179 case TokenTypes.SR_ASSIGN: 180 case TokenTypes.BSR_ASSIGN: 181 case TokenTypes.SL_ASSIGN: 182 case TokenTypes.BAND_ASSIGN: 183 case TokenTypes.BXOR_ASSIGN: 184 case TokenTypes.BOR_ASSIGN: 185 case TokenTypes.INC: 186 case TokenTypes.POST_INC: 187 case TokenTypes.DEC: 188 case TokenTypes.POST_DEC: 189 checkIdent(ast); 190 break; 191 default: 192 throw new IllegalStateException(ILLEGAL_TYPE_OF_TOKEN + ast); 193 } 194 } 195 196 @Override 197 public void leaveToken(DetailAST ast) { 198 switch (ast.getType()) { 199 case TokenTypes.FOR_ITERATOR: 200 leaveForIter(ast.getParent()); 201 break; 202 case TokenTypes.FOR_EACH_CLAUSE: 203 if (!skipEnhancedForLoopVariable) { 204 final DetailAST paramDef = ast.findFirstToken(TokenTypes.VARIABLE_DEF); 205 leaveForEach(paramDef); 206 } 207 break; 208 case TokenTypes.LITERAL_FOR: 209 leaveForDef(ast); 210 break; 211 case TokenTypes.OBJBLOCK: 212 exitBlock(); 213 break; 214 case TokenTypes.ASSIGN: 215 case TokenTypes.PLUS_ASSIGN: 216 case TokenTypes.MINUS_ASSIGN: 217 case TokenTypes.STAR_ASSIGN: 218 case TokenTypes.DIV_ASSIGN: 219 case TokenTypes.MOD_ASSIGN: 220 case TokenTypes.SR_ASSIGN: 221 case TokenTypes.BSR_ASSIGN: 222 case TokenTypes.SL_ASSIGN: 223 case TokenTypes.BAND_ASSIGN: 224 case TokenTypes.BXOR_ASSIGN: 225 case TokenTypes.BOR_ASSIGN: 226 case TokenTypes.INC: 227 case TokenTypes.POST_INC: 228 case TokenTypes.DEC: 229 case TokenTypes.POST_DEC: 230 // we need that Tokens only at visitToken() 231 break; 232 default: 233 throw new IllegalStateException(ILLEGAL_TYPE_OF_TOKEN + ast); 234 } 235 } 236 237 /** 238 * Enters an inner class, which requires a new variable set. 239 */ 240 private void enterBlock() { 241 variableStack.push(new ArrayDeque<>()); 242 } 243 244 /** 245 * Leave an inner class, so restore variable set. 246 */ 247 private void exitBlock() { 248 variableStack.pop(); 249 } 250 251 /** 252 * Get current variable stack. 253 * 254 * @return current variable stack 255 */ 256 private Deque<String> getCurrentVariables() { 257 return variableStack.peek(); 258 } 259 260 /** 261 * Check if ident is parameter. 262 * 263 * @param ast ident to check. 264 */ 265 private void checkIdent(DetailAST ast) { 266 final Deque<String> currentVariables = getCurrentVariables(); 267 final DetailAST identAST = ast.getFirstChild(); 268 269 if (identAST != null && identAST.getType() == TokenTypes.IDENT 270 && currentVariables.contains(identAST.getText())) { 271 log(ast, MSG_KEY, identAST.getText()); 272 } 273 } 274 275 /** 276 * Push current variables to the stack. 277 * 278 * @param ast a for definition. 279 */ 280 private void leaveForIter(DetailAST ast) { 281 final Set<String> variablesToPutInScope = getVariablesManagedByForLoop(ast); 282 for (String variableName : variablesToPutInScope) { 283 getCurrentVariables().push(variableName); 284 } 285 } 286 287 /** 288 * Determines which variable are specific to for loop and should not be 289 * change by inner loop body. 290 * 291 * @param ast For Loop 292 * @return Set of Variable Name which are managed by for 293 */ 294 private static Set<String> getVariablesManagedByForLoop(DetailAST ast) { 295 final Set<String> initializedVariables = getForInitVariables(ast); 296 final Set<String> iteratingVariables = getForIteratorVariables(ast); 297 return initializedVariables.stream().filter(iteratingVariables::contains) 298 .collect(Collectors.toUnmodifiableSet()); 299 } 300 301 /** 302 * Push current variables to the stack. 303 * 304 * @param paramDef a for-each clause variable 305 */ 306 private void leaveForEach(DetailAST paramDef) { 307 // When using record decomposition in enhanced for loops, 308 // we are not able to declare a 'control variable'. 309 final boolean isRecordPattern = paramDef == null; 310 311 if (!isRecordPattern) { 312 final DetailAST paramName = paramDef.findFirstToken(TokenTypes.IDENT); 313 getCurrentVariables().push(paramName.getText()); 314 } 315 } 316 317 /** 318 * Pops the variables from the stack. 319 * 320 * @param ast a for definition. 321 */ 322 private void leaveForDef(DetailAST ast) { 323 final DetailAST forInitAST = ast.findFirstToken(TokenTypes.FOR_INIT); 324 if (forInitAST == null) { 325 final Deque<String> currentVariables = getCurrentVariables(); 326 if (!skipEnhancedForLoopVariable && !currentVariables.isEmpty()) { 327 // this is for-each loop, just pop variables 328 currentVariables.pop(); 329 } 330 } 331 else { 332 final Set<String> variablesManagedByForLoop = getVariablesManagedByForLoop(ast); 333 popCurrentVariables(variablesManagedByForLoop.size()); 334 } 335 } 336 337 /** 338 * Pops given number of variables from currentVariables. 339 * 340 * @param count Count of variables to be popped from currentVariables 341 */ 342 private void popCurrentVariables(int count) { 343 for (int i = 0; i < count; i++) { 344 getCurrentVariables().pop(); 345 } 346 } 347 348 /** 349 * Get all variables initialized In init part of for loop. 350 * 351 * @param ast for loop token 352 * @return set of variables initialized in for loop 353 */ 354 private static Set<String> getForInitVariables(DetailAST ast) { 355 final Set<String> initializedVariables = new HashSet<>(); 356 final DetailAST forInitAST = ast.findFirstToken(TokenTypes.FOR_INIT); 357 358 for (DetailAST parameterDefAST = forInitAST.findFirstToken(TokenTypes.VARIABLE_DEF); 359 parameterDefAST != null; 360 parameterDefAST = parameterDefAST.getNextSibling()) { 361 if (parameterDefAST.getType() == TokenTypes.VARIABLE_DEF) { 362 final DetailAST param = 363 parameterDefAST.findFirstToken(TokenTypes.IDENT); 364 365 initializedVariables.add(param.getText()); 366 } 367 } 368 return initializedVariables; 369 } 370 371 /** 372 * Get all variables which for loop iterating part change in every loop. 373 * 374 * @param ast for loop literal(TokenTypes.LITERAL_FOR) 375 * @return names of variables change in iterating part of for 376 */ 377 private static Set<String> getForIteratorVariables(DetailAST ast) { 378 final Set<String> iteratorVariables = new HashSet<>(); 379 final DetailAST forIteratorAST = ast.findFirstToken(TokenTypes.FOR_ITERATOR); 380 final DetailAST forUpdateListAST = forIteratorAST.findFirstToken(TokenTypes.ELIST); 381 382 findChildrenOfExpressionType(forUpdateListAST).stream() 383 .filter(iteratingExpressionAST -> { 384 return MUTATION_OPERATIONS.get(iteratingExpressionAST.getType()); 385 }).forEach(iteratingExpressionAST -> { 386 final DetailAST oneVariableOperatorChild = iteratingExpressionAST.getFirstChild(); 387 iteratorVariables.add(oneVariableOperatorChild.getText()); 388 }); 389 390 return iteratorVariables; 391 } 392 393 /** 394 * Find all child of given AST of type TokenType.EXPR. 395 * 396 * @param ast parent of expressions to find 397 * @return all child of given ast 398 */ 399 private static List<DetailAST> findChildrenOfExpressionType(DetailAST ast) { 400 final List<DetailAST> foundExpressions = new LinkedList<>(); 401 if (ast != null) { 402 for (DetailAST iteratingExpressionAST = ast.findFirstToken(TokenTypes.EXPR); 403 iteratingExpressionAST != null; 404 iteratingExpressionAST = iteratingExpressionAST.getNextSibling()) { 405 if (iteratingExpressionAST.getType() == TokenTypes.EXPR) { 406 foundExpressions.add(iteratingExpressionAST.getFirstChild()); 407 } 408 } 409 } 410 return foundExpressions; 411 } 412 413}