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