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