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