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