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