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