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