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 * <p>
034 * Detects uncommented {@code main} methods.
035 * </p>
036 * <p>
037 * Rationale: A {@code main} method is often used for debugging purposes.
038 * When debugging is finished, developers often forget to remove the method,
039 * which changes the API and increases the size of the resulting class or JAR file.
040 * Except for the real program entry points, all {@code main} methods
041 * should be removed or commented out of the sources.
042 * </p>
043 * <ul>
044 * <li>
045 * Property {@code excludedClasses} - Specify pattern for qualified names of
046 * classes which are allowed to have a {@code main} method.
047 * Type is {@code java.util.regex.Pattern}.
048 * Default value is {@code "^$"}.
049 * </li>
050 * </ul>
051 * <p>
052 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
053 * </p>
054 * <p>
055 * Violation Message Keys:
056 * </p>
057 * <ul>
058 * <li>
059 * {@code uncommented.main}
060 * </li>
061 * </ul>
062 *
063 * @since 3.2
064 */
065@FileStatefulCheck
066public class UncommentedMainCheck
067    extends AbstractCheck {
068
069    /**
070     * A key is pointing to the warning message text in "messages.properties"
071     * file.
072     */
073    public static final String MSG_KEY = "uncommented.main";
074
075    /** Set of possible String array types. */
076    private static final Set<String> STRING_PARAMETER_NAMES = Set.of(
077        String[].class.getCanonicalName(),
078        String.class.getCanonicalName(),
079        String[].class.getSimpleName(),
080        String.class.getSimpleName()
081    );
082
083    /**
084     * Specify pattern for qualified names of classes which are allowed to
085     * have a {@code main} method.
086     */
087    private Pattern excludedClasses = Pattern.compile("^$");
088    /** Current class name. */
089    private String currentClass;
090    /** Current package. */
091    private FullIdent packageName;
092    /** Class definition depth. */
093    private int classDepth;
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:
143                visitPackageDef(ast);
144                break;
145            case TokenTypes.RECORD_DEF:
146            case TokenTypes.CLASS_DEF:
147                visitClassOrRecordDef(ast);
148                break;
149            case TokenTypes.METHOD_DEF:
150                visitMethodDef(ast);
151                break;
152            default:
153                throw new IllegalStateException(ast.toString());
154        }
155    }
156
157    /**
158     * Sets current package.
159     *
160     * @param packageDef node for package definition
161     */
162    private void visitPackageDef(DetailAST packageDef) {
163        packageName = FullIdent.createFullIdent(packageDef.getLastChild()
164                .getPreviousSibling());
165    }
166
167    /**
168     * If not inner class then change current class name.
169     *
170     * @param classOrRecordDef node for class or record definition
171     */
172    private void visitClassOrRecordDef(DetailAST classOrRecordDef) {
173        // we are not use inner classes because they can not
174        // have static methods
175        if (classDepth == 0) {
176            final DetailAST ident = classOrRecordDef.findFirstToken(TokenTypes.IDENT);
177            currentClass = packageName.getText() + "." + ident.getText();
178            classDepth++;
179        }
180    }
181
182    /**
183     * Checks method definition if this is
184     * {@code public static void main(String[])}.
185     *
186     * @param method method definition node
187     */
188    private void visitMethodDef(DetailAST method) {
189        if (classDepth == 1
190                // method not in inner class or in interface definition
191                && checkClassName()
192                && checkName(method)
193                && checkModifiers(method)
194                && checkType(method)
195                && checkParams(method)) {
196            log(method, MSG_KEY);
197        }
198    }
199
200    /**
201     * Checks that current class is not excluded.
202     *
203     * @return true if check passed, false otherwise
204     */
205    private boolean checkClassName() {
206        return !excludedClasses.matcher(currentClass).find();
207    }
208
209    /**
210     * Checks that method name is @quot;main@quot;.
211     *
212     * @param method the METHOD_DEF node
213     * @return true if check passed, false otherwise
214     */
215    private static boolean checkName(DetailAST method) {
216        final DetailAST ident = method.findFirstToken(TokenTypes.IDENT);
217        return "main".equals(ident.getText());
218    }
219
220    /**
221     * Checks that method has final and static modifiers.
222     *
223     * @param method the METHOD_DEF node
224     * @return true if check passed, false otherwise
225     */
226    private static boolean checkModifiers(DetailAST method) {
227        final DetailAST modifiers =
228            method.findFirstToken(TokenTypes.MODIFIERS);
229
230        return modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null
231            && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null;
232    }
233
234    /**
235     * Checks that return type is {@code void}.
236     *
237     * @param method the METHOD_DEF node
238     * @return true if check passed, false otherwise
239     */
240    private static boolean checkType(DetailAST method) {
241        final DetailAST type =
242            method.findFirstToken(TokenTypes.TYPE).getFirstChild();
243        return type.getType() == TokenTypes.LITERAL_VOID;
244    }
245
246    /**
247     * Checks that method has only {@code String[]} or only {@code String...} param.
248     *
249     * @param method the METHOD_DEF node
250     * @return true if check passed, false otherwise
251     */
252    private static boolean checkParams(DetailAST method) {
253        boolean checkPassed = false;
254        final DetailAST params = method.findFirstToken(TokenTypes.PARAMETERS);
255
256        if (params.getChildCount() == 1) {
257            final DetailAST parameterType = params.getFirstChild().findFirstToken(TokenTypes.TYPE);
258            final boolean isArrayDeclaration =
259                parameterType.findFirstToken(TokenTypes.ARRAY_DECLARATOR) != null;
260            final Optional<DetailAST> varargs = Optional.ofNullable(
261                params.getFirstChild().findFirstToken(TokenTypes.ELLIPSIS));
262
263            if (isArrayDeclaration || varargs.isPresent()) {
264                checkPassed = isStringType(parameterType.getFirstChild());
265            }
266        }
267        return checkPassed;
268    }
269
270    /**
271     * Whether the type is java.lang.String.
272     *
273     * @param typeAst the type to check.
274     * @return true, if the type is java.lang.String.
275     */
276    private static boolean isStringType(DetailAST typeAst) {
277        final FullIdent type = FullIdent.createFullIdent(typeAst);
278        return STRING_PARAMETER_NAMES.contains(type.getText());
279    }
280
281}