001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2016 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.HashMap;
026import java.util.Iterator;
027import java.util.Map;
028
029import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
030import com.puppycrawl.tools.checkstyle.api.DetailAST;
031import com.puppycrawl.tools.checkstyle.api.TokenTypes;
032import com.puppycrawl.tools.checkstyle.utils.ScopeUtils;
033
034/**
035 * <p>
036 * Ensures that local variables that never get their values changed,
037 * must be declared final.
038 * </p>
039 * <p>
040 * An example of how to configure the check to validate variable definition is:
041 * </p>
042 * <pre>
043 * &lt;module name="FinalLocalVariable"&gt;
044 *     &lt;property name="tokens" value="VARIABLE_DEF"/&gt;
045 * &lt;/module&gt;
046 * </pre>
047 * <p>
048 * By default, this Check skip final validation on
049 *  <a href = "http://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.14.2">
050 * Enhanced For-Loop</a>
051 * </p>
052 * <p>
053 * Option 'validateEnhancedForLoopVariable' could be used to make Check to validate even variable
054 *  from Enhanced For Loop.
055 * </p>
056 * <p>
057 * An example of how to configure the check so that it also validates enhanced For Loop Variable is:
058 * </p>
059 * <pre>
060 * &lt;module name="FinalLocalVariable"&gt;
061 *     &lt;property name="tokens" value="VARIABLE_DEF"/&gt;
062 *     &lt;property name="validateEnhancedForLoopVariable" value="true"/&gt;
063 * &lt;/module&gt;
064 * </pre>
065 * <p>Example:</p>
066 * <p>
067 * {@code
068 * for (int number : myNumbers) { // violation
069 *    System.out.println(number);
070 * }
071 * }
072 * </p>
073 * @author k_gibbs, r_auckenthaler
074 * @author Vladislav Lisetskiy
075 */
076public class FinalLocalVariableCheck extends AbstractCheck {
077
078    /**
079     * A key is pointing to the warning message text in "messages.properties"
080     * file.
081     */
082    public static final String MSG_KEY = "final.variable";
083
084    /**
085     * Assign operator types.
086     */
087    private static final int[] ASSIGN_OPERATOR_TYPES = {
088        TokenTypes.POST_INC,
089        TokenTypes.POST_DEC,
090        TokenTypes.ASSIGN,
091        TokenTypes.PLUS_ASSIGN,
092        TokenTypes.MINUS_ASSIGN,
093        TokenTypes.STAR_ASSIGN,
094        TokenTypes.DIV_ASSIGN,
095        TokenTypes.MOD_ASSIGN,
096        TokenTypes.SR_ASSIGN,
097        TokenTypes.BSR_ASSIGN,
098        TokenTypes.SL_ASSIGN,
099        TokenTypes.BAND_ASSIGN,
100        TokenTypes.BXOR_ASSIGN,
101        TokenTypes.BOR_ASSIGN,
102        TokenTypes.INC,
103        TokenTypes.DEC,
104    };
105
106    /**
107     * Loop types.
108     */
109    private static final int[] LOOP_TYPES = {
110        TokenTypes.LITERAL_FOR,
111        TokenTypes.LITERAL_WHILE,
112        TokenTypes.LITERAL_DO,
113    };
114
115    /** Scope Deque. */
116    private final Deque<ScopeData> scopeStack = new ArrayDeque<>();
117
118    /** Uninitialized variables of previous scope. */
119    private final Deque<Deque<DetailAST>> prevScopeUninitializedVariables =
120            new ArrayDeque<>();
121
122    /** Controls whether to check enhanced for-loop variable. */
123    private boolean validateEnhancedForLoopVariable;
124
125    static {
126        // Array sorting for binary search
127        Arrays.sort(ASSIGN_OPERATOR_TYPES);
128        Arrays.sort(LOOP_TYPES);
129    }
130
131    /**
132     * Whether to check enhanced for-loop variable or not.
133     * @param validateEnhancedForLoopVariable whether to check for-loop variable
134     */
135    public final void setValidateEnhancedForLoopVariable(boolean validateEnhancedForLoopVariable) {
136        this.validateEnhancedForLoopVariable = validateEnhancedForLoopVariable;
137    }
138
139    @Override
140    public int[] getRequiredTokens() {
141        return new int[] {
142            TokenTypes.IDENT,
143            TokenTypes.CTOR_DEF,
144            TokenTypes.METHOD_DEF,
145            TokenTypes.SLIST,
146            TokenTypes.OBJBLOCK,
147        };
148    }
149
150    @Override
151    public int[] getDefaultTokens() {
152        return new int[] {
153            TokenTypes.IDENT,
154            TokenTypes.CTOR_DEF,
155            TokenTypes.METHOD_DEF,
156            TokenTypes.SLIST,
157            TokenTypes.OBJBLOCK,
158            TokenTypes.VARIABLE_DEF,
159        };
160    }
161
162    @Override
163    public int[] getAcceptableTokens() {
164        return new int[] {
165            TokenTypes.IDENT,
166            TokenTypes.CTOR_DEF,
167            TokenTypes.METHOD_DEF,
168            TokenTypes.SLIST,
169            TokenTypes.OBJBLOCK,
170            TokenTypes.VARIABLE_DEF,
171            TokenTypes.PARAMETER_DEF,
172        };
173    }
174
175    @Override
176    public void visitToken(DetailAST ast) {
177        switch (ast.getType()) {
178            case TokenTypes.OBJBLOCK:
179            case TokenTypes.METHOD_DEF:
180            case TokenTypes.CTOR_DEF:
181                scopeStack.push(new ScopeData());
182                break;
183            case TokenTypes.SLIST:
184                if (ast.getParent().getType() != TokenTypes.CASE_GROUP
185                    || ast.getParent().getParent().findFirstToken(TokenTypes.CASE_GROUP)
186                    == ast.getParent()) {
187                    storePrevScopeUninitializedVariableData();
188                    scopeStack.push(new ScopeData());
189                }
190                break;
191            case TokenTypes.PARAMETER_DEF:
192                if (!isInLambda(ast)
193                        && !ast.branchContains(TokenTypes.FINAL)
194                        && !isInAbstractOrNativeMethod(ast)
195                        && !ScopeUtils.isInInterfaceBlock(ast)) {
196                    insertParameter(ast);
197                }
198                break;
199            case TokenTypes.VARIABLE_DEF:
200                if (ast.getParent().getType() != TokenTypes.OBJBLOCK
201                        && !ast.branchContains(TokenTypes.FINAL)
202                        && !isVariableInForInit(ast)
203                        && shouldCheckEnhancedForLoopVariable(ast)) {
204                    insertVariable(ast);
205                }
206                break;
207
208            case TokenTypes.IDENT:
209                final int parentType = ast.getParent().getType();
210                if (isAssignOperator(parentType)
211                        && isFirstChild(ast)) {
212                    removeVariable(ast);
213                }
214                break;
215
216            default:
217                throw new IllegalStateException("Incorrect token type");
218        }
219    }
220
221    @Override
222    public void leaveToken(DetailAST ast) {
223        Map<String, DetailAST> scope = null;
224        switch (ast.getType()) {
225            case TokenTypes.OBJBLOCK:
226            case TokenTypes.CTOR_DEF:
227            case TokenTypes.METHOD_DEF:
228                scope = scopeStack.pop().scope;
229                break;
230            case TokenTypes.SLIST:
231                final Deque<DetailAST> prevScopeUnitializedVariableData =
232                    prevScopeUninitializedVariables.peek();
233                if (ast.getParent().getType() != TokenTypes.CASE_GROUP
234                    || findLastChildWhichContainsSpecifiedToken(ast.getParent().getParent(),
235                            TokenTypes.CASE_GROUP, TokenTypes.SLIST) == ast.getParent()) {
236                    scope = scopeStack.pop().scope;
237                    prevScopeUninitializedVariables.pop();
238                }
239                final DetailAST parent = ast.getParent();
240                if (shouldUpdateUninitializedVariables(parent)) {
241                    updateUninitializedVariables(prevScopeUnitializedVariableData);
242                }
243                break;
244            default:
245                // do nothing
246        }
247        if (scope != null) {
248            for (DetailAST node : scope.values()) {
249                log(node.getLineNo(), node.getColumnNo(), MSG_KEY, node.getText());
250            }
251        }
252    }
253
254    /**
255     * Store un-initialized variables in a temporary stack for future use.
256     */
257    private void storePrevScopeUninitializedVariableData() {
258        final ScopeData scopeData = scopeStack.peek();
259        final Deque<DetailAST> prevScopeUnitializedVariableData =
260                new ArrayDeque<>();
261        for (DetailAST variable : scopeData.uninitializedVariables) {
262            prevScopeUnitializedVariableData.push(variable);
263        }
264        prevScopeUninitializedVariables.push(prevScopeUnitializedVariableData);
265    }
266
267    /**
268     * Update current scope data uninitialized variable according to the previous scope data.
269     * @param prevScopeUnitializedVariableData variable for previous stack of uninitialized
270     *     variables
271     */
272    private void updateUninitializedVariables(Deque<DetailAST>
273            prevScopeUnitializedVariableData) {
274        // Check for only previous scope
275        for (DetailAST variable : prevScopeUnitializedVariableData) {
276            for (ScopeData scopeData : scopeStack) {
277                final DetailAST storedVariable = scopeData.scope.get(variable.getText());
278                if (storedVariable != null && isSameVariables(storedVariable, variable)
279                        && !scopeData.uninitializedVariables.contains(storedVariable)) {
280                    scopeData.uninitializedVariables.push(variable);
281                }
282            }
283        }
284        // Check for rest of the scope
285        for (Deque<DetailAST> unitializedVariableData : prevScopeUninitializedVariables) {
286            for (DetailAST variable : unitializedVariableData) {
287                for (ScopeData scopeData : scopeStack) {
288                    final DetailAST storedVariable = scopeData.scope.get(variable.getText());
289                    if (storedVariable != null
290                            && isSameVariables(storedVariable, variable)
291                            && !scopeData.uninitializedVariables.contains(storedVariable)) {
292                        scopeData.uninitializedVariables.push(variable);
293                    }
294                }
295            }
296        }
297    }
298
299    /**
300     * If token is LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, or LITERAL_ELSE, then do not
301     * update the uninitialized variables.
302     * @param ast token to be checked
303     * @return true if should be updated, else false
304     */
305    private boolean shouldUpdateUninitializedVariables(DetailAST ast) {
306        return ast.getType() != TokenTypes.LITERAL_TRY
307                && ast.getType() != TokenTypes.LITERAL_CATCH
308                && ast.getType() != TokenTypes.LITERAL_FINALLY
309                && ast.getType() != TokenTypes.LITERAL_ELSE;
310    }
311
312    /**
313     * Returns the last child token that makes a specified type and contains containType in
314     * its branch.
315     * @param ast token to be tested
316     * @param childType the token type to match
317     * @param containType the token type which has to be present in the branch
318     * @return the matching token, or null if no match
319     */
320    public DetailAST findLastChildWhichContainsSpecifiedToken(DetailAST ast, int childType,
321            int containType) {
322        DetailAST returnValue = null;
323        for (DetailAST astIterator = ast.getFirstChild(); astIterator != null;
324                astIterator = astIterator.getNextSibling()) {
325            if (astIterator.getType() == childType && astIterator.branchContains(containType)) {
326                returnValue = astIterator;
327            }
328        }
329        return returnValue;
330    }
331
332    /**
333     * Determines whether enhanced for-loop variable should be checked or not.
334     * @param ast The ast to compare.
335     * @return true if enhanced for-loop variable should be checked.
336     */
337    private boolean shouldCheckEnhancedForLoopVariable(DetailAST ast) {
338        return validateEnhancedForLoopVariable
339                || ast.getParent().getType() != TokenTypes.FOR_EACH_CLAUSE;
340    }
341
342    /**
343     * Insert a parameter at the topmost scope stack.
344     * @param ast the variable to insert.
345     */
346    private void insertParameter(DetailAST ast) {
347        final Map<String, DetailAST> scope = scopeStack.peek().scope;
348        final DetailAST astNode = ast.findFirstToken(TokenTypes.IDENT);
349        scope.put(astNode.getText(), astNode);
350    }
351
352    /**
353     * Insert a variable at the topmost scope stack.
354     * @param ast the variable to insert.
355     */
356    private void insertVariable(DetailAST ast) {
357        final Map<String, DetailAST> scope = scopeStack.peek().scope;
358        final DetailAST astNode = ast.findFirstToken(TokenTypes.IDENT);
359        scope.put(astNode.getText(), astNode);
360        if (!isInitialized(astNode)) {
361            scopeStack.peek().uninitializedVariables.add(astNode);
362        }
363    }
364
365    /**
366     * Check if VARIABLE_DEF is initialized or not.
367     * @param ast VARIABLE_DEF to be checked
368     * @return true if initialized
369     */
370    private static boolean isInitialized(DetailAST ast) {
371        return ast.getParent().getLastChild().getType() == TokenTypes.ASSIGN;
372    }
373
374    /**
375     * Whether the ast is the first child of its parent.
376     * @param ast the ast to check.
377     * @return true if the ast is the first child of its parent.
378     */
379    private static boolean isFirstChild(DetailAST ast) {
380        return ast.getPreviousSibling() == null;
381    }
382
383    /**
384     * Remove the variable from the Stack.
385     * @param ast Variable to remove
386     */
387    private void removeVariable(DetailAST ast) {
388        final Iterator<ScopeData> iterator = scopeStack.descendingIterator();
389        while (iterator.hasNext()) {
390            final ScopeData scopeData = iterator.next();
391            final Map<String, DetailAST> scope = scopeData.scope;
392            final DetailAST storedVariable = scope.get(ast.getText());
393            if (storedVariable != null && isSameVariables(storedVariable, ast)) {
394                if (shouldRemoveVariable(scopeData, ast)) {
395                    scope.remove(ast.getText());
396                }
397                break;
398            }
399        }
400    }
401
402    /**
403     * Whether the variable should be removed from the list of final local variable
404     * candidates.
405     * @param scopeData the scope data of the variable.
406     * @param ast the variable ast.
407     * @return true, if the variable should be removed.
408     */
409    private static boolean shouldRemoveVariable(ScopeData scopeData, DetailAST ast) {
410        boolean shouldRemove = true;
411        for (DetailAST variable : scopeData.uninitializedVariables) {
412            if (variable.getText().equals(ast.getText())) {
413
414                // if the variable is declared outside the loop and initialized inside
415                // the loop, then it cannot be declared final, as it can be initialized
416                // more than once in this case
417                if (isInTheSameLoop(variable, ast)) {
418                    shouldRemove = false;
419                }
420                scopeData.uninitializedVariables.remove(variable);
421                break;
422            }
423        }
424        return shouldRemove;
425    }
426
427    /**
428     * Is Arithmetic operator.
429     * @param parentType token AST
430     * @return true is token type is in arithmetic operator
431     */
432    private static boolean isAssignOperator(int parentType) {
433        return Arrays.binarySearch(ASSIGN_OPERATOR_TYPES, parentType) >= 0;
434    }
435
436    /**
437     * Checks if current variable is defined in
438     *  {@link TokenTypes#FOR_INIT for-loop init}, e.g.:
439     * <p>
440     * {@code
441     * for (int i = 0, j = 0; i < j; i++) { . . . }
442     * }
443     * </p>
444     * {@code i, j} are defined in {@link TokenTypes#FOR_INIT for-loop init}
445     * @param variableDef variable definition node.
446     * @return true if variable is defined in {@link TokenTypes#FOR_INIT for-loop init}
447     */
448    private static boolean isVariableInForInit(DetailAST variableDef) {
449        return variableDef.getParent().getType() == TokenTypes.FOR_INIT;
450    }
451
452    /**
453     * Determines whether an AST is a descendant of an abstract or native method.
454     * @param ast the AST to check.
455     * @return true if ast is a descendant of an abstract or native method.
456     */
457    private static boolean isInAbstractOrNativeMethod(DetailAST ast) {
458        boolean abstractOrNative = false;
459        DetailAST parent = ast.getParent();
460        while (parent != null && !abstractOrNative) {
461            if (parent.getType() == TokenTypes.METHOD_DEF) {
462                final DetailAST modifiers =
463                    parent.findFirstToken(TokenTypes.MODIFIERS);
464                abstractOrNative = modifiers.branchContains(TokenTypes.ABSTRACT)
465                        || modifiers.branchContains(TokenTypes.LITERAL_NATIVE);
466            }
467            parent = parent.getParent();
468        }
469        return abstractOrNative;
470    }
471
472    /**
473     * Check if current param is lambda's param.
474     * @param paramDef {@link TokenTypes#PARAMETER_DEF parameter def}.
475     * @return true if current param is lambda's param.
476     */
477    private static boolean isInLambda(DetailAST paramDef) {
478        return paramDef.getParent().getParent().getType() == TokenTypes.LAMBDA;
479    }
480
481    /**
482     * Find the Class, Constructor, Enum or Method in which it is defined.
483     * @param ast Variable for which we want to find the scope in which it is defined
484     * @return ast The Class or Constructor or Method in which it is defined.
485     */
486    private static DetailAST findFirstUpperNamedBlock(DetailAST ast) {
487        DetailAST astTraverse = ast;
488        while (astTraverse.getType() != TokenTypes.METHOD_DEF
489                && astTraverse.getType() != TokenTypes.CLASS_DEF
490                && astTraverse.getType() != TokenTypes.ENUM_DEF
491                && astTraverse.getType() != TokenTypes.CTOR_DEF) {
492            astTraverse = astTraverse.getParent();
493        }
494        return astTraverse;
495    }
496
497    /**
498     * Check if both the Variables are same.
499     * @param ast1 Variable to compare
500     * @param ast2 Variable to compare
501     * @return true if both the variables are same, otherwise false
502     */
503    private static boolean isSameVariables(DetailAST ast1, DetailAST ast2) {
504        final DetailAST classOrMethodOfAst1 =
505            findFirstUpperNamedBlock(ast1);
506        final DetailAST classOrMethodOfAst2 =
507            findFirstUpperNamedBlock(ast2);
508        return classOrMethodOfAst1 == classOrMethodOfAst2;
509    }
510
511    /**
512     * Check if both the variables are in the same loop.
513     * @param ast1 variable to compare.
514     * @param ast2 variable to compare.
515     * @return true if both the variables are in the same loop.
516     */
517    private static boolean isInTheSameLoop(DetailAST ast1, DetailAST ast2) {
518        DetailAST loop1 = ast1.getParent();
519        while (loop1 != null && !isLoopAst(loop1.getType())) {
520            loop1 = loop1.getParent();
521        }
522        DetailAST loop2 = ast2.getParent();
523        while (loop2 != null && !isLoopAst(loop2.getType())) {
524            loop2 = loop2.getParent();
525        }
526        return loop1 == null && loop2 == null
527                || loop1 != null && loop1 == loop2;
528    }
529
530    /**
531     * Checks whether the ast is a loop.
532     * @param ast the ast to check.
533     * @return true if the ast is a loop.
534     */
535    private static boolean isLoopAst(int ast) {
536        return Arrays.binarySearch(LOOP_TYPES, ast) >= 0;
537    }
538
539    /**
540     * Holder for the scope data.
541     */
542    private static class ScopeData {
543        /** Contains variable definitions. */
544        private final Map<String, DetailAST> scope = new HashMap<>();
545
546        /** Contains definitions of uninitialized variables. */
547        private final Deque<DetailAST> uninitializedVariables = new ArrayDeque<>();
548    }
549}