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 * {@snippet :
060 * enum Age {  // violation
061 *   CHILD, ADULT
062 * }
063 *
064 * void main() {}
065 * }
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 * @noinspection EmptyClass, ConfusingMainMethod, NewClassNamingConvention, PackageVisibleInnerClass
076 * @noinspectionreason EmptyClass - until IDEA-394242
077 * @noinspectionreason ConfusingMainMethod - until IDEA-394246
078 * @noinspectionreason NewClassNamingConvention - until IDEA-394247
079 * @noinspectionreason PackageVisibleInnerClass - until IDEA-394411
080 */
081@StatelessCheck
082public class ClassMemberImpliedModifierCheck extends AbstractCheck {
083
084    /**
085     * A key is pointing to the warning message text in "messages.properties" file.
086     */
087    public static final String MSG_KEY = "class.implied.modifier";
088
089    /** Name for 'static' keyword. */
090    private static final String STATIC_KEYWORD = "static";
091
092    /**
093     * Control whether to enforce that {@code static} is explicitly coded
094     * on nested enums in classes and records.
095     */
096    private boolean violateImpliedStaticOnNestedEnum = true;
097
098    /**
099     * Control whether to enforce that {@code static} is explicitly coded
100     * on nested interfaces in classes and records.
101     */
102    private boolean violateImpliedStaticOnNestedInterface = true;
103
104    /**
105     * Control whether to enforce that {@code static} is explicitly coded
106     * on nested records in classes and records.
107     */
108    private boolean violateImpliedStaticOnNestedRecord = true;
109
110    /**
111     * Creates a new {@code ClassMemberImpliedModifierCheck} instance.
112     */
113    public ClassMemberImpliedModifierCheck() {
114        // no code by default
115    }
116
117    /**
118     * Setter to control whether to enforce that {@code static} is explicitly coded
119     * on nested enums in classes and records.
120     *
121     * @param violateImplied
122     *        True to perform the check, false to turn the check off.
123     * @since 8.16
124     */
125    public void setViolateImpliedStaticOnNestedEnum(boolean violateImplied) {
126        violateImpliedStaticOnNestedEnum = violateImplied;
127    }
128
129    /**
130     * Setter to control whether to enforce that {@code static} is explicitly coded
131     * on nested interfaces in classes and records.
132     *
133     * @param violateImplied
134     *        True to perform the check, false to turn the check off.
135     * @since 8.16
136     */
137    public void setViolateImpliedStaticOnNestedInterface(boolean violateImplied) {
138        violateImpliedStaticOnNestedInterface = violateImplied;
139    }
140
141    /**
142     * Setter to control whether to enforce that {@code static} is explicitly coded
143     * on nested records in classes and records.
144     *
145     * @param violateImplied
146     *        True to perform the check, false to turn the check off.
147     * @since 8.36
148     */
149    public void setViolateImpliedStaticOnNestedRecord(boolean violateImplied) {
150        violateImpliedStaticOnNestedRecord = violateImplied;
151    }
152
153    @Override
154    public int[] getDefaultTokens() {
155        return getAcceptableTokens();
156    }
157
158    @Override
159    public int[] getRequiredTokens() {
160        return getAcceptableTokens();
161    }
162
163    @Override
164    public int[] getAcceptableTokens() {
165        return new int[] {
166            TokenTypes.INTERFACE_DEF,
167            TokenTypes.ENUM_DEF,
168            TokenTypes.RECORD_DEF,
169        };
170    }
171
172    @Override
173    public void visitToken(DetailAST ast) {
174        if (isInTypeBlock(ast)) {
175            final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
176            switch (ast.getType()) {
177                case TokenTypes.ENUM_DEF -> {
178                    if (violateImpliedStaticOnNestedEnum
179                            && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) == null) {
180                        log(ast, MSG_KEY, STATIC_KEYWORD);
181                    }
182                }
183
184                case TokenTypes.INTERFACE_DEF -> {
185                    if (violateImpliedStaticOnNestedInterface
186                            && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) == null) {
187                        log(ast, MSG_KEY, STATIC_KEYWORD);
188                    }
189                }
190
191                case TokenTypes.RECORD_DEF -> {
192                    if (violateImpliedStaticOnNestedRecord
193                            && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) == null) {
194                        log(ast, MSG_KEY, STATIC_KEYWORD);
195                    }
196                }
197
198                default -> throw new IllegalStateException(ast.toString());
199            }
200        }
201    }
202
203    /**
204     * Checks if ast is in a class, enum, anon class or record block, including the
205     * implicitly declared class of a compact source file.
206     *
207     * @param ast the current ast
208     * @return true if ast is in a class, enum, anon class or record, including
209     *         the implicitly declared class of a compact source file
210     */
211    private static boolean isInTypeBlock(DetailAST ast) {
212        return ScopeUtil.isInScope(ast, Scope.ANONINNER)
213                || ScopeUtil.isInClassBlock(ast)
214                || ScopeUtil.isInEnumBlock(ast)
215                || ScopeUtil.isInRecordBlock(ast)
216                || ast.getParent().getType() == TokenTypes.COMPACT_COMPILATION_UNIT;
217    }
218
219}