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