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;
021
022import java.util.Optional;
023import java.util.Set;
024import java.util.regex.Pattern;
025
026import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
027import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
028import com.puppycrawl.tools.checkstyle.api.DetailAST;
029import com.puppycrawl.tools.checkstyle.api.FullIdent;
030import com.puppycrawl.tools.checkstyle.api.TokenTypes;
031import com.puppycrawl.tools.checkstyle.utils.NullUtil;
032
033/**
034 * <div>
035 * Detects uncommented {@code main} methods.
036 * </div>
037 *
038 * <p>
039 * Rationale: A {@code main} method is often used for debugging purposes.
040 * When debugging is finished, developers often forget to remove the method,
041 * which changes the API and increases the size of the resulting class or JAR file.
042 * Except for the real program entry points, all {@code main} methods
043 * should be removed or commented out of the sources.
044 * </p>
045 *
046 * <p>
047 * Note: Compact source files
048 * (<a href="https://openjdk.org/jeps/512">JEP 512</a>)
049 * are skipped by design. The whole purpose of compact source files is to serve as
050 * standalone single-file programs with a {@code main} method as the entry point.
051 * Unlike regular classes where leftover {@code main} methods may be
052 * debugging artifacts, compact sources inherently require a {@code main} method
053 * to function.
054 * </p>
055 *
056 * @since 3.2
057 */
058@FileStatefulCheck
059public class UncommentedMainCheck
060    extends AbstractCheck {
061
062    /**
063     * A key is pointing to the warning message text in "messages.properties"
064     * file.
065     */
066    public static final String MSG_KEY = "uncommented.main";
067
068    /** Set of possible String array types. */
069    private static final Set<String> STRING_PARAMETER_NAMES = Set.of(
070        String[].class.getCanonicalName(),
071        String.class.getCanonicalName(),
072        String[].class.getSimpleName(),
073        String.class.getSimpleName()
074    );
075
076    /**
077     * Specify pattern for qualified names of classes which are allowed to
078     * have a {@code main} method.
079     */
080    private Pattern excludedClasses = Pattern.compile("^$");
081    /** Current class name. */
082    private String currentClass;
083    /** Current package. */
084    private FullIdent packageName;
085    /** Class definition depth. */
086    private int classDepth;
087
088    /**
089     * Creates a new {@code UncommentedMainCheck} instance.
090     */
091    public UncommentedMainCheck() {
092        // no code by default
093    }
094
095    /**
096     * Setter to specify pattern for qualified names of classes which are allowed
097     * to have a {@code main} method.
098     *
099     * @param excludedClasses a pattern
100     * @since 3.2
101     */
102    public void setExcludedClasses(Pattern excludedClasses) {
103        this.excludedClasses = excludedClasses;
104    }
105
106    @Override
107    public int[] getAcceptableTokens() {
108        return getRequiredTokens();
109    }
110
111    @Override
112    public int[] getDefaultTokens() {
113        return getRequiredTokens();
114    }
115
116    @Override
117    public int[] getRequiredTokens() {
118        return new int[] {
119            TokenTypes.METHOD_DEF,
120            TokenTypes.CLASS_DEF,
121            TokenTypes.PACKAGE_DEF,
122            TokenTypes.RECORD_DEF,
123        };
124    }
125
126    @Override
127    public void beginTree(DetailAST rootAST) {
128        packageName = FullIdent.createFullIdent(null);
129        classDepth = 0;
130    }
131
132    @Override
133    public void leaveToken(DetailAST ast) {
134        if (ast.getType() == TokenTypes.CLASS_DEF) {
135            classDepth--;
136        }
137    }
138
139    @Override
140    public void visitToken(DetailAST ast) {
141        switch (ast.getType()) {
142            case TokenTypes.PACKAGE_DEF -> visitPackageDef(ast);
143            case TokenTypes.RECORD_DEF, TokenTypes.CLASS_DEF -> visitClassOrRecordDef(ast);
144            case TokenTypes.METHOD_DEF -> visitMethodDef(ast);
145            default -> throw new IllegalStateException(ast.toString());
146        }
147    }
148
149    /**
150     * Sets current package.
151     *
152     * @param packageDef node for package definition
153     */
154    private void visitPackageDef(DetailAST packageDef) {
155        packageName = FullIdent.createFullIdent(packageDef.getLastChild()
156                .getPreviousSibling());
157    }
158
159    /**
160     * If not inner class then change current class name.
161     *
162     * @param classOrRecordDef node for class or record definition
163     */
164    private void visitClassOrRecordDef(DetailAST classOrRecordDef) {
165        // we are not use inner classes because they can not
166        // have static methods
167        if (classDepth == 0) {
168            final DetailAST ident =
169                    NullUtil.notNull(classOrRecordDef.findFirstToken(TokenTypes.IDENT));
170            currentClass = packageName.getText() + "." + ident.getText();
171            classDepth++;
172        }
173    }
174
175    /**
176     * Checks method definition if this is
177     * {@code public static void main(String[])}.
178     *
179     * @param method method definition node
180     */
181    private void visitMethodDef(DetailAST method) {
182        if (classDepth == 1
183                // method not in inner class or in interface definition
184                && checkClassName()
185                && checkName(method)
186                && checkModifiers(method)
187                && checkType(method)
188                && checkParams(method)) {
189            log(method, MSG_KEY);
190        }
191    }
192
193    /**
194     * Checks that current class is not excluded.
195     *
196     * @return true if check passed, false otherwise
197     */
198    private boolean checkClassName() {
199        return !excludedClasses.matcher(currentClass).find();
200    }
201
202    /**
203     * Checks that method name is @quot;main@quot;.
204     *
205     * @param method the METHOD_DEF node
206     * @return true if check passed, false otherwise
207     */
208    private static boolean checkName(DetailAST method) {
209        final DetailAST ident = NullUtil.notNull(method.findFirstToken(TokenTypes.IDENT));
210        return "main".equals(ident.getText());
211    }
212
213    /**
214     * Checks that method has final and static modifiers.
215     *
216     * @param method the METHOD_DEF node
217     * @return true if check passed, false otherwise
218     */
219    private static boolean checkModifiers(DetailAST method) {
220        final DetailAST modifiers =
221            NullUtil.notNull(method.findFirstToken(TokenTypes.MODIFIERS));
222
223        return modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null
224            && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null;
225    }
226
227    /**
228     * Checks that return type is {@code void}.
229     *
230     * @param method the METHOD_DEF node
231     * @return true if check passed, false otherwise
232     */
233    private static boolean checkType(DetailAST method) {
234        final DetailAST type =
235            NullUtil.notNull(method.findFirstToken(TokenTypes.TYPE)).getFirstChild();
236        return type.getType() == TokenTypes.LITERAL_VOID;
237    }
238
239    /**
240     * Checks that method has only {@code String[]} or only {@code String...} param.
241     *
242     * @param method the METHOD_DEF node
243     * @return true if check passed, false otherwise
244     */
245    private static boolean checkParams(DetailAST method) {
246        boolean checkPassed = false;
247        final DetailAST params =
248                NullUtil.notNull(method.findFirstToken(TokenTypes.PARAMETERS));
249
250        if (params.getChildCount() == 1) {
251            final DetailAST parameterType =
252                    NullUtil.notNull(params.getFirstChild()
253                            .findFirstToken(TokenTypes.TYPE));
254            final boolean isArrayDeclaration =
255                parameterType.findFirstToken(TokenTypes.ARRAY_DECLARATOR) != null;
256            final Optional<DetailAST> varargs = Optional.ofNullable(
257                params.getFirstChild().findFirstToken(TokenTypes.ELLIPSIS));
258
259            if (isArrayDeclaration || varargs.isPresent()) {
260                checkPassed = isStringType(parameterType.getFirstChild());
261            }
262        }
263        return checkPassed;
264    }
265
266    /**
267     * Whether the type is java.lang.String.
268     *
269     * @param typeAst the type to check.
270     * @return true, if the type is java.lang.String.
271     */
272    private static boolean isStringType(DetailAST typeAst) {
273        final FullIdent type = FullIdent.createFullIdent(typeAst);
274        return STRING_PARAMETER_NAMES.contains(type.getText());
275    }
276
277}