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   * {@snippet lang="text" :
54   * ${returnType} set${Name}(${anyType} ${name}) { ... }
55   * }
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   * {@snippet lang="text" :
64   * void setTime(long time) {  }
65   * }
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   * {@snippet lang="text" :
74   * class PageBuilder {
75   *   PageBuilder setName(String name) {  }
76   * }
77   * }
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     /**
124      * Creates a new {@code HiddenFieldCheck} instance.
125      */
126     public HiddenFieldCheck() {
127         // no code by default
128     }
129 
130     @Override
131     public int[] getDefaultTokens() {
132         return getAcceptableTokens();
133     }
134 
135     @Override
136     public int[] getAcceptableTokens() {
137         return new int[] {
138             TokenTypes.VARIABLE_DEF,
139             TokenTypes.PARAMETER_DEF,
140             TokenTypes.CLASS_DEF,
141             TokenTypes.ENUM_DEF,
142             TokenTypes.ENUM_CONSTANT_DEF,
143             TokenTypes.PATTERN_VARIABLE_DEF,
144             TokenTypes.LAMBDA,
145             TokenTypes.RECORD_DEF,
146             TokenTypes.RECORD_COMPONENT_DEF,
147             TokenTypes.COMPACT_COMPILATION_UNIT,
148         };
149     }
150 
151     @Override
152     public int[] getRequiredTokens() {
153         return new int[] {
154             TokenTypes.CLASS_DEF,
155             TokenTypes.ENUM_DEF,
156             TokenTypes.ENUM_CONSTANT_DEF,
157             TokenTypes.RECORD_DEF,
158             TokenTypes.COMPACT_COMPILATION_UNIT,
159         };
160     }
161 
162     @Override
163     public void beginTree(DetailAST rootAST) {
164         frame = new FieldFrame(null, true, null);
165     }
166 
167     @Override
168     public void visitToken(DetailAST ast) {
169         final int type = ast.getType();
170         switch (type) {
171             case TokenTypes.VARIABLE_DEF,
172                  TokenTypes.PARAMETER_DEF,
173                  TokenTypes.PATTERN_VARIABLE_DEF,
174                  TokenTypes.RECORD_COMPONENT_DEF -> processVariable(ast);
175             case TokenTypes.LAMBDA -> processLambda(ast);
176             default -> visitOtherTokens(ast, type);
177         }
178     }
179 
180     /**
181      * Process a lambda token.
182      * Checks whether a lambda parameter shadows a field.
183      * Note, that when parameter of lambda expression is untyped,
184      * ANTLR parses the parameter as an identifier.
185      *
186      * @param ast the lambda token.
187      */
188     private void processLambda(DetailAST ast) {
189         final DetailAST firstChild = ast.getFirstChild();
190         if (TokenUtil.isOfType(firstChild, TokenTypes.IDENT)) {
191             final String untypedLambdaParameterName = firstChild.getText();
192             if (frame.containsStaticField(untypedLambdaParameterName)
193                 || isInstanceField(firstChild, untypedLambdaParameterName)) {
194                 log(firstChild, MSG_KEY, untypedLambdaParameterName);
195             }
196         }
197     }
198 
199     /**
200      * Called to process tokens other than {@link TokenTypes#VARIABLE_DEF}
201      * and {@link TokenTypes#PARAMETER_DEF}.
202      *
203      * @param ast token to process
204      * @param type type of the token
205      */
206     private void visitOtherTokens(DetailAST ast, int type) {
207         // A more thorough check of enum constant class bodies is
208         // possible (checking for hidden fields against the enum
209         // class body in addition to enum constant class bodies)
210         // but not attempted as it seems out of the scope of this
211         // check.
212         final DetailAST typeMods = ast.findFirstToken(TokenTypes.MODIFIERS);
213         final boolean isStaticInnerType =
214                 typeMods != null
215                         && typeMods.findFirstToken(TokenTypes.LITERAL_STATIC) != null
216                         // inner record is implicitly static
217                         || ast.getType() == TokenTypes.RECORD_DEF;
218         final String frameName;
219 
220         if (type == TokenTypes.CLASS_DEF
221                 || type == TokenTypes.ENUM_DEF) {
222             frameName = ast.findFirstToken(TokenTypes.IDENT).getText();
223         }
224         else {
225             frameName = null;
226         }
227         final FieldFrame newFrame = new FieldFrame(frame, isStaticInnerType, frameName);
228 
229         // add fields to container
230         final DetailAST objBlock = getFieldContainer(ast);
231         // enum constants may not have bodies
232         if (objBlock != null) {
233             DetailAST child = objBlock.getFirstChild();
234             while (child != null) {
235                 if (child.getType() == TokenTypes.VARIABLE_DEF) {
236                     final String name =
237                         child.findFirstToken(TokenTypes.IDENT).getText();
238                     final DetailAST mods =
239                         child.findFirstToken(TokenTypes.MODIFIERS);
240                     if (mods.findFirstToken(TokenTypes.LITERAL_STATIC) == null) {
241                         newFrame.addInstanceField(name);
242                     }
243                     else {
244                         newFrame.addStaticField(name);
245                     }
246                 }
247                 child = child.getNextSibling();
248             }
249         }
250         if (ast.getType() == TokenTypes.RECORD_DEF) {
251             final DetailAST recordComponents =
252                 ast.findFirstToken(TokenTypes.RECORD_COMPONENTS);
253 
254             // For each record component definition, we will add it to this frame.
255             TokenUtil.forEachChild(recordComponents,
256                 TokenTypes.RECORD_COMPONENT_DEF, node -> {
257                     final String name = node.findFirstToken(TokenTypes.IDENT).getText();
258                     newFrame.addInstanceField(name);
259                 });
260         }
261         // push container
262         frame = newFrame;
263     }
264 
265     /**
266      * Gets the member container for field declaration harvesting.
267      *
268      * @param ast the type definition node.
269      * @return the member container, either the compact compilation unit
270      *     itself or the OBJBLOCK child of a standard type definition.
271      */
272     @Nullable
273     private static DetailAST getFieldContainer(DetailAST ast) {
274         final DetailAST result;
275         if (ast.getType() == TokenTypes.COMPACT_COMPILATION_UNIT) {
276             result = ast;
277         }
278         else {
279             result = ast.findFirstToken(TokenTypes.OBJBLOCK);
280         }
281         return result;
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             || ast.getType() == TokenTypes.RECORD_DEF) {
290             // pop
291             frame = frame.getParent();
292         }
293     }
294 
295     /**
296      * Process a variable token.
297      * Check whether a local variable or parameter shadows a field.
298      * Store a field for later comparison with local variables and parameters.
299      *
300      * @param ast the variable token.
301      */
302     private void processVariable(DetailAST ast) {
303         if (!ScopeUtil.isInInterfaceOrAnnotationBlock(ast)
304             && !CheckUtil.isReceiverParameter(ast)
305             && (ScopeUtil.isLocalVariableDef(ast)
306                 || ast.getType() == TokenTypes.PARAMETER_DEF
307                 || ast.getType() == TokenTypes.PATTERN_VARIABLE_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      *
323      * @param ast the parameter token.
324      * @param name the parameter name.
325      * @return true if parameter is ignored.
326      */
327     private boolean isIgnoredParam(DetailAST ast, String name) {
328         return isIgnoredSetterParam(ast, name)
329             || isIgnoredConstructorParam(ast)
330             || isIgnoredParamOfAbstractMethod(ast);
331     }
332 
333     /**
334      * Check for instance field.
335      *
336      * @param ast token
337      * @param name identifier of token
338      * @return true if instance field
339      */
340     private boolean isInstanceField(DetailAST ast, String name) {
341         return !isInStatic(ast) && frame.containsInstanceField(name);
342     }
343 
344     /**
345      * Check name by regExp.
346      *
347      * @param name string value to check
348      * @return true is regexp is matching
349      */
350     private boolean isMatchingRegexp(String name) {
351         return ignoreFormat != null && ignoreFormat.matcher(name).find();
352     }
353 
354     /**
355      * Determines whether an AST node is in a static method or static
356      * initializer.
357      *
358      * @param ast the node to check.
359      * @return true if ast is in a static method or a static block;
360      */
361     private static boolean isInStatic(DetailAST ast) {
362         DetailAST parent = ast.getParent();
363         boolean inStatic = false;
364 
365         while (parent != null && !inStatic) {
366             if (parent.getType() == TokenTypes.STATIC_INIT) {
367                 inStatic = true;
368             }
369             else if (parent.getType() == TokenTypes.METHOD_DEF
370                         && !ScopeUtil.isInScope(parent, Scope.ANONINNER)
371                         || parent.getType() == TokenTypes.VARIABLE_DEF) {
372                 final DetailAST mods =
373                     parent.findFirstToken(TokenTypes.MODIFIERS);
374                 inStatic = mods.findFirstToken(TokenTypes.LITERAL_STATIC) != null;
375                 break;
376             }
377             else {
378                 parent = parent.getParent();
379             }
380         }
381         return inStatic;
382     }
383 
384     /**
385      * Decides whether to ignore an AST node that is the parameter of a
386      * setter method, where the property setter method for field 'xyz' has
387      * name 'setXyz', one parameter named 'xyz', and return type void
388      * (default behavior) or return type is name of the class in which
389      * such method is declared (allowed only if
390      * {@link #setSetterCanReturnItsClass(boolean)} is called with
391      * value <em>true</em>).
392      *
393      * @param ast the AST to check.
394      * @param name the name of ast.
395      * @return true if ast should be ignored because check property
396      *     ignoreSetter is true and ast is the parameter of a setter method.
397      */
398     private boolean isIgnoredSetterParam(DetailAST ast, String name) {
399         boolean isIgnoredSetterParam = false;
400         if (ignoreSetter) {
401             final DetailAST parametersAST = ast.getParent();
402             final DetailAST methodAST = parametersAST.getParent();
403             if (parametersAST.getChildCount() == 1
404                 && methodAST.getType() == TokenTypes.METHOD_DEF
405                 && isSetterMethod(methodAST, name)) {
406                 isIgnoredSetterParam = true;
407             }
408         }
409         return isIgnoredSetterParam;
410     }
411 
412     /**
413      * Determine if a specific method identified by methodAST and a single
414      * variable name parameterName is a setter. This recognition partially depends
415      * on setterCanReturnItsClass property.
416      *
417      * @param methodAST AST corresponding to a method call
418      * @param parameterName name of single parameter of this method.
419      * @return true of false indicating of method is a setter or not.
420      */
421     private boolean isSetterMethod(DetailAST methodAST, String parameterName) {
422         final String methodName =
423             methodAST.findFirstToken(TokenTypes.IDENT).getText();
424         boolean isSetterMethod = false;
425 
426         if (("set" + capitalize(parameterName)).equals(methodName)) {
427             // method name did match set${Name}(${anyType} ${parameterName})
428             // where ${Name} is capitalized version of ${parameterName}
429             // therefore this method is potentially a setter
430             final DetailAST typeAST = methodAST.findFirstToken(TokenTypes.TYPE);
431             final String returnType = typeAST.getFirstChild().getText();
432             if (typeAST.findFirstToken(TokenTypes.LITERAL_VOID) != null
433                     || setterCanReturnItsClass && frame.isEmbeddedIn(returnType)) {
434                 // this method has signature
435                 //
436                 //     void set${Name}(${anyType} ${name})
437                 //
438                 // and therefore considered to be a setter
439                 //
440                 // or
441                 //
442                 // return type is not void, but it is the same as the class
443                 // where method is declared and setterCanReturnItsClass
444                 // is set to true
445                 isSetterMethod = true;
446             }
447         }
448 
449         return isSetterMethod;
450     }
451 
452     /**
453      * Capitalizes a given property name the way we expect to see it in
454      * a setter name.
455      *
456      * @param name a property name
457      * @return capitalized property name
458      */
459     private static String capitalize(final String name) {
460         String setterName = name;
461         // we should not capitalize the first character if the second
462         // one is a capital one, since according to JavaBeans spec
463         // setFooBar() is a setter for FooBar property, not for fooBar one.
464         if (name.length() == 1 || !Character.isUpperCase(name.charAt(1))) {
465             setterName = name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1);
466         }
467         return setterName;
468     }
469 
470     /**
471      * Decides whether to ignore an AST node that is the parameter of a
472      * constructor.
473      *
474      * @param ast the AST to check.
475      * @return true if ast should be ignored because check property
476      *     ignoreConstructorParameter is true and ast is a constructor parameter.
477      */
478     private boolean isIgnoredConstructorParam(DetailAST ast) {
479         boolean result = false;
480         if (ignoreConstructorParameter
481                 && ast.getType() == TokenTypes.PARAMETER_DEF) {
482             final DetailAST parametersAST = ast.getParent();
483             final DetailAST constructorAST = parametersAST.getParent();
484             result = constructorAST.getType() == TokenTypes.CTOR_DEF;
485         }
486         return result;
487     }
488 
489     /**
490      * Decides whether to ignore an AST node that is the parameter of an
491      * abstract method.
492      *
493      * @param ast the AST to check.
494      * @return true if ast should be ignored because check property
495      *     ignoreAbstractMethods is true and ast is a parameter of abstract methods.
496      */
497     private boolean isIgnoredParamOfAbstractMethod(DetailAST ast) {
498         boolean result = false;
499         if (ignoreAbstractMethods) {
500             final DetailAST method = ast.getParent().getParent();
501             if (method.getType() == TokenTypes.METHOD_DEF) {
502                 final DetailAST mods = method.findFirstToken(TokenTypes.MODIFIERS);
503                 result = mods.findFirstToken(TokenTypes.ABSTRACT) != null;
504             }
505         }
506         return result;
507     }
508 
509     /**
510      * Setter to define the RegExp for names of variables and parameters to ignore.
511      *
512      * @param pattern a pattern.
513      * @since 3.2
514      */
515     public void setIgnoreFormat(Pattern pattern) {
516         ignoreFormat = pattern;
517     }
518 
519     /**
520      * Setter to allow to ignore the parameter of a property setter method.
521      *
522      * @param ignoreSetter decide whether to ignore the parameter of
523      *     a property setter method.
524      * @since 3.2
525      */
526     public void setIgnoreSetter(boolean ignoreSetter) {
527         this.ignoreSetter = ignoreSetter;
528     }
529 
530     /**
531      * Setter to allow to expand the definition of a setter method to include methods
532      * that return the class' instance.
533      *
534      * @param setterCanReturnItsClass if true then setter can return
535      *        either void or class in which it is declared. If false then
536      *        in order to be recognized as setter method (otherwise
537      *        already recognized as a setter) must return void.  Later is
538      *        the default behavior.
539      * @since 6.3
540      */
541     public void setSetterCanReturnItsClass(
542         boolean setterCanReturnItsClass) {
543         this.setterCanReturnItsClass = setterCanReturnItsClass;
544     }
545 
546     /**
547      * Setter to control whether to ignore constructor parameters.
548      *
549      * @param ignoreConstructorParameter decide whether to ignore
550      *     constructor parameters.
551      * @since 3.2
552      */
553     public void setIgnoreConstructorParameter(
554         boolean ignoreConstructorParameter) {
555         this.ignoreConstructorParameter = ignoreConstructorParameter;
556     }
557 
558     /**
559      * Setter to control whether to ignore parameters of abstract methods.
560      *
561      * @param ignoreAbstractMethods decide whether to ignore
562      *     parameters of abstract methods.
563      * @since 4.0
564      */
565     public void setIgnoreAbstractMethods(
566         boolean ignoreAbstractMethods) {
567         this.ignoreAbstractMethods = ignoreAbstractMethods;
568     }
569 
570     /**
571      * Holds the names of static and instance fields of a type.
572      */
573     private static final class FieldFrame {
574 
575         /** Name of the frame, such name of the class or enum declaration. */
576         private final String frameName;
577 
578         /** Is this a static inner type. */
579         private final boolean staticType;
580 
581         /** Parent frame. */
582         private final FieldFrame parent;
583 
584         /** Set of instance field names. */
585         private final Set<String> instanceFields = new HashSet<>();
586 
587         /** Set of static field names. */
588         private final Set<String> staticFields = new HashSet<>();
589 
590         /**
591          * Creates new frame.
592          *
593          * @param parent parent frame.
594          * @param staticType is this a static inner type (class or enum).
595          * @param frameName name associated with the frame, which can be a
596          */
597         private FieldFrame(FieldFrame parent, boolean staticType, String frameName) {
598             this.parent = parent;
599             this.staticType = staticType;
600             this.frameName = frameName;
601         }
602 
603         /**
604          * Adds an instance field to this FieldFrame.
605          *
606          * @param field  the name of the instance field.
607          */
608         /* package */ void addInstanceField(String field) {
609             instanceFields.add(field);
610         }
611 
612         /**
613          * Adds a static field to this FieldFrame.
614          *
615          * @param field  the name of the instance field.
616          */
617         /* package */ void addStaticField(String field) {
618             staticFields.add(field);
619         }
620 
621         /**
622          * Determines whether this FieldFrame contains an instance field.
623          *
624          * @param field the field to check
625          * @return true if this FieldFrame contains instance field
626          */
627         /* package */ boolean containsInstanceField(String field) {
628             FieldFrame currentParent = parent;
629             boolean contains = instanceFields.contains(field);
630             boolean isStaticType = staticType;
631             while (!isStaticType && !contains) {
632                 contains = currentParent.instanceFields.contains(field);
633                 isStaticType = currentParent.staticType;
634                 currentParent = currentParent.parent;
635             }
636             return contains;
637         }
638 
639         /**
640          * Determines whether this FieldFrame contains a static field.
641          *
642          * @param field the field to check
643          * @return true if this FieldFrame contains static field
644          */
645         /* package */ boolean containsStaticField(String field) {
646             FieldFrame currentParent = parent;
647             boolean contains = staticFields.contains(field);
648             while (currentParent != null && !contains) {
649                 contains = currentParent.staticFields.contains(field);
650                 currentParent = currentParent.parent;
651             }
652             return contains;
653         }
654 
655         /**
656          * Getter for parent frame.
657          *
658          * @return parent frame.
659          */
660         /* package */ FieldFrame getParent() {
661             return parent;
662         }
663 
664         /**
665          * Check if current frame is embedded in class or enum with
666          * specific name.
667          *
668          * @param classOrEnumName name of class or enum that we are looking
669          *     for in the chain of field frames.
670          *
671          * @return true if current frame is embedded in class or enum
672          *     with name classOrNameName
673          */
674         private boolean isEmbeddedIn(String classOrEnumName) {
675             FieldFrame currentFrame = this;
676             boolean isEmbeddedIn = false;
677             while (currentFrame != null) {
678                 if (Objects.equals(currentFrame.frameName, classOrEnumName)) {
679                     isEmbeddedIn = true;
680                     break;
681                 }
682                 currentFrame = currentFrame.parent;
683             }
684             return isEmbeddedIn;
685         }
686 
687     }
688 
689 }