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