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