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