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