001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.coding;
021
022import java.util.HashSet;
023import java.util.Locale;
024import java.util.Objects;
025import java.util.Set;
026import java.util.regex.Pattern;
027
028import javax.annotation.Nullable;
029
030import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
031import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
032import com.puppycrawl.tools.checkstyle.api.DetailAST;
033import com.puppycrawl.tools.checkstyle.api.Scope;
034import com.puppycrawl.tools.checkstyle.api.TokenTypes;
035import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
036import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
037import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
038
039/**
040 * <div>
041 * Checks that a local variable or a parameter does not shadow
042 * a field that is defined in the same class.
043 * </div>
044 *
045 * <p>
046 * Notes:
047 * It is possible to configure the check to ignore all property setter methods.
048 * </p>
049 *
050 * <p>
051 * A method is recognized as a setter if it is in the following form
052 * </p>
053 * <div class="wrapper"><pre class="prettyprint"><code class="language-text">
054 * ${returnType} set${Name}(${anyType} ${name}) { ... }
055 * </code></pre></div>
056 *
057 * <p>
058 * where ${anyType} is any primitive type, class or interface name;
059 * ${name} is name of the variable that is being set and ${Name} its
060 * capitalized form that appears in the method name. By default, it is expected
061 * that setter returns void, i.e. ${returnType} is 'void'. For example
062 * </p>
063 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
064 * void setTime(long time) { ... }
065 * </code></pre></div>
066 *
067 * <p>
068 * Any other return types will not let method match a setter pattern. However,
069 * by setting <em>setterCanReturnItsClass</em> property to <em>true</em>
070 * definition of a setter is expanded, so that setter return type can also be
071 * a class in which setter is declared. For example
072 * </p>
073 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
074 * class PageBuilder {
075 *   PageBuilder setName(String name) { ... }
076 * }
077 * </code></pre></div>
078 *
079 * <p>
080 * Such methods are known as chain-setters and a common when Builder-pattern
081 * is used. Property <em>setterCanReturnItsClass</em> has effect only if
082 * <em>ignoreSetter</em> is set to true.
083 * </p>
084 *
085 * @since 3.0
086 */
087@FileStatefulCheck
088public class HiddenFieldCheck
089    extends AbstractCheck {
090
091    /**
092     * A key is pointing to the warning message text in "messages.properties"
093     * file.
094     */
095    public static final String MSG_KEY = "hidden.field";
096
097    /**
098     * Stack of sets of field names,
099     * 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}