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