001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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.HashSet;
023import java.util.Locale;
024import java.util.Objects;
025import java.util.Set;
026import java.util.regex.Pattern;
027
028import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
029import com.puppycrawl.tools.checkstyle.api.DetailAST;
030import com.puppycrawl.tools.checkstyle.api.Scope;
031import com.puppycrawl.tools.checkstyle.api.TokenTypes;
032import com.puppycrawl.tools.checkstyle.utils.CheckUtils;
033import com.puppycrawl.tools.checkstyle.utils.ScopeUtils;
034
035/**
036 * Checks that a local variable or a parameter does not shadow
037 * a field that is defined in the same class.
038 *
039 * <p>An example of how to configure the check is:
040 * <pre>
041 * &lt;module name="HiddenField"/&gt;
042 * </pre>
043 *
044 * <p>An example of how to configure the check so that it checks variables but not
045 * parameters is:
046 * <pre>
047 * &lt;module name="HiddenField"&gt;
048 *    &lt;property name="tokens" value="VARIABLE_DEF"/&gt;
049 * &lt;/module&gt;
050 * </pre>
051 *
052 * <p>An example of how to configure the check so that it ignores the parameter of
053 * a setter method is:
054 * <pre>
055 * &lt;module name="HiddenField"&gt;
056 *    &lt;property name="ignoreSetter" value="true"/&gt;
057 * &lt;/module&gt;
058 * </pre>
059 *
060 * <p>A method is recognized as a setter if it is in the following form
061 * <pre>
062 * ${returnType} set${Name}(${anyType} ${name}) { ... }
063 * </pre>
064 * where ${anyType} is any primitive type, class or interface name;
065 * ${name} is name of the variable that is being set and ${Name} its
066 * capitalized form that appears in the method name. By default it is expected
067 * that setter returns void, i.e. ${returnType} is 'void'. For example
068 * <pre>
069 * void setTime(long time) { ... }
070 * </pre>
071 * Any other return types will not let method match a setter pattern. However,
072 * by setting <em>setterCanReturnItsClass</em> property to <em>true</em>
073 * definition of a setter is expanded, so that setter return type can also be
074 * a class in which setter is declared. For example
075 * <pre>
076 * class PageBuilder {
077 *   PageBuilder setName(String name) { ... }
078 * }
079 * </pre>
080 * Such methods are known as chain-setters and a common when Builder-pattern
081 * is used. Property <em>setterCanReturnItsClass</em> has effect only if
082 * <em>ignoreSetter</em> is set to true.
083 *
084 * <p>An example of how to configure the check so that it ignores the parameter
085 * of either a setter that returns void or a chain-setter.
086 * <pre>
087 * &lt;module name="HiddenField"&gt;
088 *    &lt;property name="ignoreSetter" value="true"/&gt;
089 *    &lt;property name="setterCanReturnItsClass" value="true"/&gt;
090 * &lt;/module&gt;
091 * </pre>
092 *
093 * <p>An example of how to configure the check so that it ignores constructor
094 * parameters is:
095 * <pre>
096 * &lt;module name="HiddenField"&gt;
097 *    &lt;property name="ignoreConstructorParameter" value="true"/&gt;
098 * &lt;/module&gt;
099 * </pre>
100 *
101 * <p>An example of how to configure the check so that it ignores variables and parameters
102 * named 'test':
103 * <pre>
104 * &lt;module name="HiddenField"&gt;
105 *    &lt;property name="ignoreFormat" value="^test$"/&gt;
106 * &lt;/module&gt;
107 * </pre>
108 *
109 * <pre>
110 * {@code
111 * class SomeClass
112 * {
113 *     private List&lt;String&gt; test;
114 *
115 *     private void addTest(List&lt;String&gt; test) // no violation
116 *     {
117 *         this.test.addAll(test);
118 *     }
119 *
120 *     private void foo()
121 *     {
122 *         final List&lt;String&gt; test = new ArrayList&lt;&gt;(); // no violation
123 *         ...
124 *     }
125 * }
126 * }
127 * </pre>
128 *
129 * @author Dmitri Priimak
130 */
131public class HiddenFieldCheck
132    extends AbstractCheck {
133    /**
134     * A key is pointing to the warning message text in "messages.properties"
135     * file.
136     */
137    public static final String MSG_KEY = "hidden.field";
138
139    /** Stack of sets of field names,
140     * one for each class of a set of nested classes.
141     */
142    private FieldFrame frame;
143
144    /** Pattern for names of variables and parameters to ignore. */
145    private Pattern ignoreFormat;
146
147    /** Controls whether to check the parameter of a property setter method. */
148    private boolean ignoreSetter;
149
150    /**
151     * If ignoreSetter is set to true then this variable controls what
152     * the setter method can return By default setter must return void.
153     * However, is this variable is set to true then setter can also
154     * return class in which is declared.
155     */
156    private boolean setterCanReturnItsClass;
157
158    /** Controls whether to check the parameter of a constructor. */
159    private boolean ignoreConstructorParameter;
160
161    /** Controls whether to check the parameter of abstract methods. */
162    private boolean ignoreAbstractMethods;
163
164    @Override
165    public int[] getDefaultTokens() {
166        return getAcceptableTokens();
167    }
168
169    @Override
170    public int[] getAcceptableTokens() {
171        return new int[] {
172            TokenTypes.VARIABLE_DEF,
173            TokenTypes.PARAMETER_DEF,
174            TokenTypes.CLASS_DEF,
175            TokenTypes.ENUM_DEF,
176            TokenTypes.ENUM_CONSTANT_DEF,
177            TokenTypes.LAMBDA,
178        };
179    }
180
181    @Override
182    public int[] getRequiredTokens() {
183        return new int[] {
184            TokenTypes.CLASS_DEF,
185            TokenTypes.ENUM_DEF,
186            TokenTypes.ENUM_CONSTANT_DEF,
187        };
188    }
189
190    @Override
191    public void beginTree(DetailAST rootAST) {
192        frame = new FieldFrame(null, true, null);
193    }
194
195    @Override
196    public void visitToken(DetailAST ast) {
197        final int type = ast.getType();
198        switch (type) {
199            case TokenTypes.VARIABLE_DEF:
200            case TokenTypes.PARAMETER_DEF:
201                processVariable(ast);
202                break;
203            case TokenTypes.LAMBDA:
204                processLambda(ast);
205                break;
206            default:
207                visitOtherTokens(ast, type);
208        }
209    }
210
211    /**
212     * Process a lambda token.
213     * Checks whether a lambda parameter shadows a field.
214     * Note, that when parameter of lambda expression is untyped,
215     * ANTLR parses the parameter as an identifier.
216     * @param ast the lambda token.
217     */
218    private void processLambda(DetailAST ast) {
219        final DetailAST firstChild = ast.getFirstChild();
220        if (firstChild.getType() == TokenTypes.IDENT) {
221            final String untypedLambdaParameterName = firstChild.getText();
222            if (isStaticOrInstanceField(firstChild, untypedLambdaParameterName)) {
223                log(firstChild, MSG_KEY, untypedLambdaParameterName);
224            }
225        }
226        else {
227            // Type of lambda parameter is not omitted.
228            processVariable(ast);
229        }
230    }
231
232    /**
233     * Called to process tokens other than {@link TokenTypes#VARIABLE_DEF}
234     * and {@link TokenTypes#PARAMETER_DEF}.
235     *
236     * @param ast token to process
237     * @param type type of the token
238     */
239    private void visitOtherTokens(DetailAST ast, int type) {
240        //A more thorough check of enum constant class bodies is
241        //possible (checking for hidden fields against the enum
242        //class body in addition to enum constant class bodies)
243        //but not attempted as it seems out of the scope of this
244        //check.
245        final DetailAST typeMods = ast.findFirstToken(TokenTypes.MODIFIERS);
246        final boolean isStaticInnerType =
247                typeMods != null
248                        && typeMods.branchContains(TokenTypes.LITERAL_STATIC);
249        final String frameName;
250
251        if (type == TokenTypes.CLASS_DEF || type == TokenTypes.ENUM_DEF) {
252            frameName = ast.findFirstToken(TokenTypes.IDENT).getText();
253        }
254        else {
255            frameName = null;
256        }
257        final FieldFrame newFrame = new FieldFrame(frame, isStaticInnerType, frameName);
258
259        //add fields to container
260        final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
261        // enum constants may not have bodies
262        if (objBlock != null) {
263            DetailAST child = objBlock.getFirstChild();
264            while (child != null) {
265                if (child.getType() == TokenTypes.VARIABLE_DEF) {
266                    final String name =
267                        child.findFirstToken(TokenTypes.IDENT).getText();
268                    final DetailAST mods =
269                        child.findFirstToken(TokenTypes.MODIFIERS);
270                    if (mods.branchContains(TokenTypes.LITERAL_STATIC)) {
271                        newFrame.addStaticField(name);
272                    }
273                    else {
274                        newFrame.addInstanceField(name);
275                    }
276                }
277                child = child.getNextSibling();
278            }
279        }
280        // push container
281        frame = newFrame;
282    }
283
284    @Override
285    public void leaveToken(DetailAST ast) {
286        if (ast.getType() == TokenTypes.CLASS_DEF
287            || ast.getType() == TokenTypes.ENUM_DEF
288            || ast.getType() == TokenTypes.ENUM_CONSTANT_DEF) {
289            //pop
290            frame = frame.getParent();
291        }
292    }
293
294    /**
295     * Process a variable token.
296     * Check whether a local variable or parameter shadows a field.
297     * Store a field for later comparison with local variables and parameters.
298     * @param ast the variable token.
299     */
300    private void processVariable(DetailAST ast) {
301        if (!ScopeUtils.isInInterfaceOrAnnotationBlock(ast)
302            && !CheckUtils.isReceiverParameter(ast)
303            && (ScopeUtils.isLocalVariableDef(ast)
304                || ast.getType() == TokenTypes.PARAMETER_DEF)) {
305            // local variable or parameter. Does it shadow a field?
306            final DetailAST nameAST = ast.findFirstToken(TokenTypes.IDENT);
307            final String name = nameAST.getText();
308
309            if ((isStaticFieldHiddenFromAnonymousClass(ast, name)
310                        || isStaticOrInstanceField(ast, name))
311                    && !isMatchingRegexp(name)
312                    && !isIgnoredParam(ast, name)) {
313                log(nameAST, MSG_KEY, name);
314            }
315        }
316    }
317
318    /**
319     * Checks whether a static field is hidden from closure.
320     * @param nameAST local variable or parameter.
321     * @param name field name.
322     * @return true if static field is hidden from closure.
323     */
324    private boolean isStaticFieldHiddenFromAnonymousClass(DetailAST nameAST, String name) {
325        return isInStatic(nameAST)
326            && frame.containsStaticField(name);
327    }
328
329    /**
330     * Checks whether method or constructor parameter is ignored.
331     * @param ast the parameter token.
332     * @param name the parameter name.
333     * @return true if parameter is ignored.
334     */
335    private boolean isIgnoredParam(DetailAST ast, String name) {
336        return isIgnoredSetterParam(ast, name)
337            || isIgnoredConstructorParam(ast)
338            || isIgnoredParamOfAbstractMethod(ast);
339    }
340
341    /**
342     * Check for static or instance field.
343     * @param ast token
344     * @param name identifier of token
345     * @return true if static or instance field
346     */
347    private boolean isStaticOrInstanceField(DetailAST ast, String name) {
348        return frame.containsStaticField(name)
349                || !isInStatic(ast) && frame.containsInstanceField(name);
350    }
351
352    /**
353     * Check name by regExp.
354     * @param name string value to check
355     * @return true is regexp is matching
356     */
357    private boolean isMatchingRegexp(String name) {
358        return ignoreFormat != null && ignoreFormat.matcher(name).find();
359    }
360
361    /**
362     * Determines whether an AST node is in a static method or static
363     * initializer.
364     * @param ast the node to check.
365     * @return true if ast is in a static method or a static block;
366     */
367    private static boolean isInStatic(DetailAST ast) {
368        DetailAST parent = ast.getParent();
369        boolean inStatic = false;
370
371        while (parent != null && !inStatic) {
372            if (parent.getType() == TokenTypes.STATIC_INIT) {
373                inStatic = true;
374            }
375            else if (parent.getType() == TokenTypes.METHOD_DEF
376                        && !ScopeUtils.isInScope(parent, Scope.ANONINNER)
377                        || parent.getType() == TokenTypes.VARIABLE_DEF) {
378                final DetailAST mods =
379                    parent.findFirstToken(TokenTypes.MODIFIERS);
380                inStatic = mods.branchContains(TokenTypes.LITERAL_STATIC);
381                break;
382            }
383            else {
384                parent = parent.getParent();
385            }
386        }
387        return inStatic;
388    }
389
390    /**
391     * Decides whether to ignore an AST node that is the parameter of a
392     * setter method, where the property setter method for field 'xyz' has
393     * name 'setXyz', one parameter named 'xyz', and return type void
394     * (default behavior) or return type is name of the class in which
395     * such method is declared (allowed only if
396     * {@link #setSetterCanReturnItsClass(boolean)} is called with
397     * value <em>true</em>).
398     *
399     * @param ast the AST to check.
400     * @param name the name of ast.
401     * @return true if ast should be ignored because check property
402     *     ignoreSetter is true and ast is the parameter of a setter method.
403     */
404    private boolean isIgnoredSetterParam(DetailAST ast, String name) {
405        if (ignoreSetter && ast.getType() == TokenTypes.PARAMETER_DEF) {
406            final DetailAST parametersAST = ast.getParent();
407            final DetailAST methodAST = parametersAST.getParent();
408            if (parametersAST.getChildCount() == 1
409                && methodAST.getType() == TokenTypes.METHOD_DEF
410                && isSetterMethod(methodAST, name)) {
411                return true;
412            }
413        }
414        return false;
415    }
416
417    /**
418     * Determine if a specific method identified by methodAST and a single
419     * variable name aName is a setter. This recognition partially depends
420     * on mSetterCanReturnItsClass property.
421     *
422     * @param aMethodAST AST corresponding to a method call
423     * @param aName name of single parameter of this method.
424     * @return true of false indicating of method is a setter or not.
425     */
426    private boolean isSetterMethod(DetailAST aMethodAST, String aName) {
427        final String methodName =
428            aMethodAST.findFirstToken(TokenTypes.IDENT).getText();
429        boolean isSetterMethod = false;
430
431        if (("set" + capitalize(aName)).equals(methodName)) {
432            // method name did match set${Name}(${anyType} ${aName})
433            // where ${Name} is capitalized version of ${aName}
434            // therefore this method is potentially a setter
435            final DetailAST typeAST = aMethodAST.findFirstToken(TokenTypes.TYPE);
436            final String returnType = typeAST.getFirstChild().getText();
437            if (typeAST.branchContains(TokenTypes.LITERAL_VOID)
438                    || setterCanReturnItsClass && frame.isEmbeddedIn(returnType)) {
439                // this method has signature
440                //
441                //     void set${Name}(${anyType} ${name})
442                //
443                // and therefore considered to be a setter
444                //
445                // or
446                //
447                // return type is not void, but it is the same as the class
448                // where method is declared and and mSetterCanReturnItsClass
449                // is set to true
450                isSetterMethod = true;
451            }
452        }
453
454        return isSetterMethod;
455    }
456
457    /**
458     * Capitalizes a given property name the way we expect to see it in
459     * a setter name.
460     * @param name a property name
461     * @return capitalized property name
462     */
463    private static String capitalize(final String name) {
464        String setterName = name;
465        // we should not capitalize the first character if the second
466        // one is a capital one, since according to JavaBeans spec
467        // setXYzz() is a setter for XYzz property, not for xYzz one.
468        if (name.length() == 1 || !Character.isUpperCase(name.charAt(1))) {
469            setterName = name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1);
470        }
471        return setterName;
472    }
473
474    /**
475     * Decides whether to ignore an AST node that is the parameter of a
476     * constructor.
477     * @param ast the AST to check.
478     * @return true if ast should be ignored because check property
479     *     ignoreConstructorParameter is true and ast is a constructor parameter.
480     */
481    private boolean isIgnoredConstructorParam(DetailAST ast) {
482        boolean result = false;
483        if (ignoreConstructorParameter
484                && ast.getType() == TokenTypes.PARAMETER_DEF) {
485            final DetailAST parametersAST = ast.getParent();
486            final DetailAST constructorAST = parametersAST.getParent();
487            result = constructorAST.getType() == TokenTypes.CTOR_DEF;
488        }
489        return result;
490    }
491
492    /**
493     * Decides whether to ignore an AST node that is the parameter of an
494     * abstract method.
495     * @param ast the AST to check.
496     * @return true if ast should be ignored because check property
497     *     ignoreAbstractMethods is true and ast is a parameter of abstract methods.
498     */
499    private boolean isIgnoredParamOfAbstractMethod(DetailAST ast) {
500        boolean result = false;
501        if (ignoreAbstractMethods
502                && ast.getType() == TokenTypes.PARAMETER_DEF) {
503            final DetailAST method = ast.getParent().getParent();
504            if (method.getType() == TokenTypes.METHOD_DEF) {
505                final DetailAST mods = method.findFirstToken(TokenTypes.MODIFIERS);
506                result = mods.branchContains(TokenTypes.ABSTRACT);
507            }
508        }
509        return result;
510    }
511
512    /**
513     * Set the ignore format for the specified regular expression.
514     * @param pattern a pattern.
515     */
516    public void setIgnoreFormat(Pattern pattern) {
517        ignoreFormat = pattern;
518    }
519
520    /**
521     * Set whether to ignore the parameter of a property setter method.
522     * @param ignoreSetter decide whether to ignore the parameter of
523     *     a property setter method.
524     */
525    public void setIgnoreSetter(boolean ignoreSetter) {
526        this.ignoreSetter = ignoreSetter;
527    }
528
529    /**
530     * Controls if setter can return only void (default behavior) or it
531     * can also return class in which it is declared.
532     *
533     * @param aSetterCanReturnItsClass if true then setter can return
534     *        either void or class in which it is declared. If false then
535     *        in order to be recognized as setter method (otherwise
536     *        already recognized as a setter) must return void.  Later is
537     *        the default behavior.
538     */
539    public void setSetterCanReturnItsClass(
540        boolean aSetterCanReturnItsClass) {
541        setterCanReturnItsClass = aSetterCanReturnItsClass;
542    }
543
544    /**
545     * Set whether to ignore constructor parameters.
546     * @param ignoreConstructorParameter decide whether to ignore
547     *     constructor parameters.
548     */
549    public void setIgnoreConstructorParameter(
550        boolean ignoreConstructorParameter) {
551        this.ignoreConstructorParameter = ignoreConstructorParameter;
552    }
553
554    /**
555     * Set whether to ignore parameters of abstract methods.
556     * @param ignoreAbstractMethods decide whether to ignore
557     *     parameters of abstract methods.
558     */
559    public void setIgnoreAbstractMethods(
560        boolean ignoreAbstractMethods) {
561        this.ignoreAbstractMethods = ignoreAbstractMethods;
562    }
563
564    /**
565     * Holds the names of static and instance fields of a type.
566     * @author Rick Giles
567     */
568    private static class FieldFrame {
569        /** Name of the frame, such name of the class or enum declaration. */
570        private final String frameName;
571
572        /** Is this a static inner type. */
573        private final boolean staticType;
574
575        /** Parent frame. */
576        private final FieldFrame parent;
577
578        /** Set of instance field names. */
579        private final Set<String> instanceFields = new HashSet<>();
580
581        /** Set of static field names. */
582        private final Set<String> staticFields = new HashSet<>();
583
584        /**
585         * Creates new frame.
586         * @param parent parent frame.
587         * @param staticType is this a static inner type (class or enum).
588         * @param frameName name associated with the frame, which can be a
589         */
590        FieldFrame(FieldFrame parent, boolean staticType, String frameName) {
591            this.parent = parent;
592            this.staticType = staticType;
593            this.frameName = frameName;
594        }
595
596        /**
597         * Adds an instance field to this FieldFrame.
598         * @param field  the name of the instance field.
599         */
600        public void addInstanceField(String field) {
601            instanceFields.add(field);
602        }
603
604        /**
605         * Adds a static field to this FieldFrame.
606         * @param field  the name of the instance field.
607         */
608        public void addStaticField(String field) {
609            staticFields.add(field);
610        }
611
612        /**
613         * Determines whether this FieldFrame contains an instance field.
614         * @param field the field to check.
615         * @return true if this FieldFrame contains instance field field.
616         */
617        public boolean containsInstanceField(String field) {
618            return instanceFields.contains(field)
619                    || parent != null
620                    && !staticType
621                    && parent.containsInstanceField(field);
622
623        }
624
625        /**
626         * Determines whether this FieldFrame contains a static field.
627         * @param field the field to check.
628         * @return true if this FieldFrame contains static field field.
629         */
630        public boolean containsStaticField(String field) {
631            return staticFields.contains(field)
632                    || parent != null
633                    && parent.containsStaticField(field);
634        }
635
636        /**
637         * Getter for parent frame.
638         * @return parent frame.
639         */
640        public FieldFrame getParent() {
641            return parent;
642        }
643
644        /**
645         * Check if current frame is embedded in class or enum with
646         * specific name.
647         *
648         * @param classOrEnumName name of class or enum that we are looking
649         *     for in the chain of field frames.
650         *
651         * @return true if current frame is embedded in class or enum
652         *     with name classOrNameName
653         */
654        private boolean isEmbeddedIn(String classOrEnumName) {
655            FieldFrame currentFrame = this;
656            while (currentFrame != null) {
657                if (Objects.equals(currentFrame.frameName, classOrEnumName)) {
658                    return true;
659                }
660                currentFrame = currentFrame.parent;
661            }
662            return false;
663        }
664    }
665}