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