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