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.modifier;
021
022import com.puppycrawl.tools.checkstyle.StatelessCheck;
023import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
024import com.puppycrawl.tools.checkstyle.api.DetailAST;
025import com.puppycrawl.tools.checkstyle.api.Scope;
026import com.puppycrawl.tools.checkstyle.api.TokenTypes;
027import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
028
029/**
030 * <div>
031 * Checks for implicit modifiers on nested types in classes and records.
032 * </div>
033 *
034 * <p>
035 * This check is effectively the opposite of
036 * <a href="https://checkstyle.org/checks/modifier/redundantmodifier.html">
037 * RedundantModifier</a>.
038 * It checks the modifiers on nested types in classes and records, ensuring that certain modifiers
039 * are explicitly specified even though they are actually redundant.
040 * </p>
041 *
042 * <p>
043 * Nested enums, interfaces, and records within a class are always {@code static} and as such the
044 * compiler does not require the {@code static} modifier. This check provides the ability to enforce
045 * that the {@code static} modifier is explicitly coded and not implicitly added by the compiler.
046 * </p>
047 * {@snippet lang="text" :
048 * public final class Person {
049 *   enum Age {  // violation
050 *     CHILD, ADULT
051 *   }
052 * }
053 * }
054 *
055 * <p>
056 * Enum, interface, and record declarations in a compact source file are members of the
057 * implicitly declared class, so they are also implicitly {@code static}.
058 * </p>
059 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
060 * enum Age {  // violation
061 *   CHILD, ADULT
062 * }
063 *
064 * void main() {}
065 * </code></pre></div>
066 *
067 * <p>
068 * Rationale for this check: Nested enums, interfaces, and records are treated differently from
069 * nested classes as they are only allowed to be {@code static}. Developers should not need to
070 * remember this rule, and this check provides the means to enforce that the modifier is coded
071 * explicitly.
072 * </p>
073 *
074 * @since 8.16
075 */
076@StatelessCheck
077public class ClassMemberImpliedModifierCheck
078    extends AbstractCheck {
079
080    /**
081     * A key is pointing to the warning message text in "messages.properties" file.
082     */
083    public static final String MSG_KEY = "class.implied.modifier";
084
085    /** Name for 'static' keyword. */
086    private static final String STATIC_KEYWORD = "static";
087
088    /**
089     * Control whether to enforce that {@code static} is explicitly coded
090     * on nested enums in classes and records.
091     */
092    private boolean violateImpliedStaticOnNestedEnum = true;
093
094    /**
095     * Control whether to enforce that {@code static} is explicitly coded
096     * on nested interfaces in classes and records.
097     */
098    private boolean violateImpliedStaticOnNestedInterface = true;
099
100    /**
101     * Control whether to enforce that {@code static} is explicitly coded
102     * on nested records in classes and records.
103     */
104    private boolean violateImpliedStaticOnNestedRecord = true;
105
106    /**
107     * Creates a new {@code ClassMemberImpliedModifierCheck} instance.
108     */
109    public ClassMemberImpliedModifierCheck() {
110        // no code by default
111    }
112
113    /**
114     * Setter to control whether to enforce that {@code static} is explicitly coded
115     * on nested enums in classes and records.
116     *
117     * @param violateImplied
118     *        True to perform the check, false to turn the check off.
119     * @since 8.16
120     */
121    public void setViolateImpliedStaticOnNestedEnum(boolean violateImplied) {
122        violateImpliedStaticOnNestedEnum = violateImplied;
123    }
124
125    /**
126     * Setter to control whether to enforce that {@code static} is explicitly coded
127     * on nested interfaces in classes and records.
128     *
129     * @param violateImplied
130     *        True to perform the check, false to turn the check off.
131     * @since 8.16
132     */
133    public void setViolateImpliedStaticOnNestedInterface(boolean violateImplied) {
134        violateImpliedStaticOnNestedInterface = violateImplied;
135    }
136
137    /**
138     * Setter to control whether to enforce that {@code static} is explicitly coded
139     * on nested records in classes and records.
140     *
141     * @param violateImplied
142     *        True to perform the check, false to turn the check off.
143     * @since 8.36
144     */
145    public void setViolateImpliedStaticOnNestedRecord(boolean violateImplied) {
146        violateImpliedStaticOnNestedRecord = violateImplied;
147    }
148
149    @Override
150    public int[] getDefaultTokens() {
151        return getAcceptableTokens();
152    }
153
154    @Override
155    public int[] getRequiredTokens() {
156        return getAcceptableTokens();
157    }
158
159    @Override
160    public int[] getAcceptableTokens() {
161        return new int[] {
162            TokenTypes.INTERFACE_DEF,
163            TokenTypes.ENUM_DEF,
164            TokenTypes.RECORD_DEF,
165        };
166    }
167
168    @Override
169    public void visitToken(DetailAST ast) {
170        if (isInTypeBlock(ast)) {
171            final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
172            switch (ast.getType()) {
173                case TokenTypes.ENUM_DEF -> {
174                    if (violateImpliedStaticOnNestedEnum
175                            && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) == null) {
176                        log(ast, MSG_KEY, STATIC_KEYWORD);
177                    }
178                }
179
180                case TokenTypes.INTERFACE_DEF -> {
181                    if (violateImpliedStaticOnNestedInterface
182                            && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) == null) {
183                        log(ast, MSG_KEY, STATIC_KEYWORD);
184                    }
185                }
186
187                case TokenTypes.RECORD_DEF -> {
188                    if (violateImpliedStaticOnNestedRecord
189                            && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) == null) {
190                        log(ast, MSG_KEY, STATIC_KEYWORD);
191                    }
192                }
193
194                default -> throw new IllegalStateException(ast.toString());
195            }
196        }
197    }
198
199    /**
200     * Checks if ast is in a class, enum, anon class or record block, including the
201     * implicitly declared class of a compact source file.
202     *
203     * @param ast the current ast
204     * @return true if ast is in a class, enum, anon class or record, including
205     *         the implicitly declared class of a compact source file
206     */
207    private static boolean isInTypeBlock(DetailAST ast) {
208        return ScopeUtil.isInScope(ast, Scope.ANONINNER)
209                || ScopeUtil.isInClassBlock(ast)
210                || ScopeUtil.isInEnumBlock(ast)
211                || ScopeUtil.isInRecordBlock(ast)
212                || ast.getParent().getType() == TokenTypes.COMPACT_COMPILATION_UNIT;
213    }
214
215}