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