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