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