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