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