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