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    @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}