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