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            if (classDepth == 1) {
098                currentClass = null;
099            }
100            classDepth--;
101        }
102    }
103
104    @Override
105    public void visitToken(DetailAST ast) {
106
107        switch (ast.getType()) {
108            case TokenTypes.PACKAGE_DEF:
109                visitPackageDef(ast);
110                break;
111            case TokenTypes.CLASS_DEF:
112                visitClassDef(ast);
113                break;
114            case TokenTypes.METHOD_DEF:
115                visitMethodDef(ast);
116                break;
117            default:
118                throw new IllegalStateException(ast.toString());
119        }
120    }
121
122    /**
123     * Sets current package.
124     * @param packageDef node for package definition
125     */
126    private void visitPackageDef(DetailAST packageDef) {
127        packageName = FullIdent.createFullIdent(packageDef.getLastChild()
128                .getPreviousSibling());
129    }
130
131    /**
132     * If not inner class then change current class name.
133     * @param classDef node for class definition
134     */
135    private void visitClassDef(DetailAST classDef) {
136        // we are not use inner classes because they can not
137        // have static methods
138        if (classDepth == 0) {
139            final DetailAST ident = classDef.findFirstToken(TokenTypes.IDENT);
140            currentClass = packageName.getText() + "." + ident.getText();
141            classDepth++;
142        }
143    }
144
145    /**
146     * Checks method definition if this is
147     * {@code public static void main(String[])}.
148     * @param method method definition node
149     */
150    private void visitMethodDef(DetailAST method) {
151        if (classDepth == 1
152                // method not in inner class or in interface definition
153                && checkClassName()
154                && checkName(method)
155                && checkModifiers(method)
156                && checkType(method)
157                && checkParams(method)) {
158            log(method.getLineNo(), MSG_KEY);
159        }
160    }
161
162    /**
163     * Checks that current class is not excluded.
164     * @return true if check passed, false otherwise
165     */
166    private boolean checkClassName() {
167        return !excludedClasses.matcher(currentClass).find();
168    }
169
170    /**
171     * Checks that method name is @quot;main@quot;.
172     * @param method the METHOD_DEF node
173     * @return true if check passed, false otherwise
174     */
175    private static boolean checkName(DetailAST method) {
176        final DetailAST ident = method.findFirstToken(TokenTypes.IDENT);
177        return "main".equals(ident.getText());
178    }
179
180    /**
181     * Checks that method has final and static modifiers.
182     * @param method the METHOD_DEF node
183     * @return true if check passed, false otherwise
184     */
185    private static boolean checkModifiers(DetailAST method) {
186        final DetailAST modifiers =
187            method.findFirstToken(TokenTypes.MODIFIERS);
188
189        return modifiers.branchContains(TokenTypes.LITERAL_PUBLIC)
190            && modifiers.branchContains(TokenTypes.LITERAL_STATIC);
191    }
192
193    /**
194     * Checks that return type is {@code void}.
195     * @param method the METHOD_DEF node
196     * @return true if check passed, false otherwise
197     */
198    private static boolean checkType(DetailAST method) {
199        final DetailAST type =
200            method.findFirstToken(TokenTypes.TYPE).getFirstChild();
201        return type.getType() == TokenTypes.LITERAL_VOID;
202    }
203
204    /**
205     * Checks that method has only {@code String[]} or only {@code String...} param.
206     * @param method the METHOD_DEF node
207     * @return true if check passed, false otherwise
208     */
209    private static boolean checkParams(DetailAST method) {
210        boolean checkPassed = false;
211        final DetailAST params = method.findFirstToken(TokenTypes.PARAMETERS);
212
213        if (params.getChildCount() == 1) {
214            final DetailAST parameterType = params.getFirstChild().findFirstToken(TokenTypes.TYPE);
215            final Optional<DetailAST> arrayDecl = Optional.ofNullable(
216                parameterType.findFirstToken(TokenTypes.ARRAY_DECLARATOR));
217            final Optional<DetailAST> varargs = Optional.ofNullable(
218                params.getFirstChild().findFirstToken(TokenTypes.ELLIPSIS));
219
220            if (arrayDecl.isPresent()) {
221                checkPassed = isStringType(arrayDecl.get().getFirstChild());
222            }
223            else if (varargs.isPresent()) {
224                checkPassed = isStringType(parameterType.getFirstChild());
225            }
226        }
227        return checkPassed;
228    }
229
230    /**
231     * Whether the type is java.lang.String.
232     * @param typeAst the type to check.
233     * @return true, if the type is java.lang.String.
234     */
235    private static boolean isStringType(DetailAST typeAst) {
236        final FullIdent type = FullIdent.createFullIdent(typeAst);
237        return "String".equals(type.getText())
238            || "java.lang.String".equals(type.getText());
239    }
240}