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