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