1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks;
21  
22  import java.util.Optional;
23  import java.util.regex.Pattern;
24  
25  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
26  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
27  import com.puppycrawl.tools.checkstyle.api.DetailAST;
28  import com.puppycrawl.tools.checkstyle.api.FullIdent;
29  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
30  
31  /**
32   * Detects uncommented main methods. Basically detects
33   * any main method, since if it is detectable
34   * that means it is uncommented.
35   *
36   * <pre class="body">
37   * &lt;module name=&quot;UncommentedMain&quot;/&gt;
38   * </pre>
39   *
40   */
41  @FileStatefulCheck
42  public class UncommentedMainCheck
43      extends AbstractCheck {
44  
45      /**
46       * A key is pointing to the warning message text in "messages.properties"
47       * file.
48       */
49      public static final String MSG_KEY = "uncommented.main";
50  
51      /** Compiled regexp to exclude classes from check. */
52      private Pattern excludedClasses = Pattern.compile("^$");
53      /** Current class name. */
54      private String currentClass;
55      /** Current package. */
56      private FullIdent packageName;
57      /** Class definition depth. */
58      private int classDepth;
59  
60      /**
61       * Set the excluded classes pattern.
62       * @param excludedClasses a pattern
63       */
64      public void setExcludedClasses(Pattern excludedClasses) {
65          this.excludedClasses = excludedClasses;
66      }
67  
68      @Override
69      public int[] getAcceptableTokens() {
70          return getRequiredTokens();
71      }
72  
73      @Override
74      public int[] getDefaultTokens() {
75          return getRequiredTokens();
76      }
77  
78      @Override
79      public int[] getRequiredTokens() {
80          return new int[] {
81              TokenTypes.METHOD_DEF,
82              TokenTypes.CLASS_DEF,
83              TokenTypes.PACKAGE_DEF,
84          };
85      }
86  
87      @Override
88      public void beginTree(DetailAST rootAST) {
89          packageName = FullIdent.createFullIdent(null);
90          currentClass = null;
91          classDepth = 0;
92      }
93  
94      @Override
95      public void leaveToken(DetailAST ast) {
96          if (ast.getType() == TokenTypes.CLASS_DEF) {
97              classDepth--;
98          }
99      }
100 
101     @Override
102     public void visitToken(DetailAST ast) {
103         switch (ast.getType()) {
104             case TokenTypes.PACKAGE_DEF:
105                 visitPackageDef(ast);
106                 break;
107             case TokenTypes.CLASS_DEF:
108                 visitClassDef(ast);
109                 break;
110             case TokenTypes.METHOD_DEF:
111                 visitMethodDef(ast);
112                 break;
113             default:
114                 throw new IllegalStateException(ast.toString());
115         }
116     }
117 
118     /**
119      * Sets current package.
120      * @param packageDef node for package definition
121      */
122     private void visitPackageDef(DetailAST packageDef) {
123         packageName = FullIdent.createFullIdent(packageDef.getLastChild()
124                 .getPreviousSibling());
125     }
126 
127     /**
128      * If not inner class then change current class name.
129      * @param classDef node for class definition
130      */
131     private void visitClassDef(DetailAST classDef) {
132         // we are not use inner classes because they can not
133         // have static methods
134         if (classDepth == 0) {
135             final DetailAST ident = classDef.findFirstToken(TokenTypes.IDENT);
136             currentClass = packageName.getText() + "." + ident.getText();
137             classDepth++;
138         }
139     }
140 
141     /**
142      * Checks method definition if this is
143      * {@code public static void main(String[])}.
144      * @param method method definition node
145      */
146     private void visitMethodDef(DetailAST method) {
147         if (classDepth == 1
148                 // method not in inner class or in interface definition
149                 && checkClassName()
150                 && checkName(method)
151                 && checkModifiers(method)
152                 && checkType(method)
153                 && checkParams(method)) {
154             log(method.getLineNo(), MSG_KEY);
155         }
156     }
157 
158     /**
159      * Checks that current class is not excluded.
160      * @return true if check passed, false otherwise
161      */
162     private boolean checkClassName() {
163         return !excludedClasses.matcher(currentClass).find();
164     }
165 
166     /**
167      * Checks that method name is @quot;main@quot;.
168      * @param method the METHOD_DEF node
169      * @return true if check passed, false otherwise
170      */
171     private static boolean checkName(DetailAST method) {
172         final DetailAST ident = method.findFirstToken(TokenTypes.IDENT);
173         return "main".equals(ident.getText());
174     }
175 
176     /**
177      * Checks that method has final and static modifiers.
178      * @param method the METHOD_DEF node
179      * @return true if check passed, false otherwise
180      */
181     private static boolean checkModifiers(DetailAST method) {
182         final DetailAST modifiers =
183             method.findFirstToken(TokenTypes.MODIFIERS);
184 
185         return modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null
186             && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null;
187     }
188 
189     /**
190      * Checks that return type is {@code void}.
191      * @param method the METHOD_DEF node
192      * @return true if check passed, false otherwise
193      */
194     private static boolean checkType(DetailAST method) {
195         final DetailAST type =
196             method.findFirstToken(TokenTypes.TYPE).getFirstChild();
197         return type.getType() == TokenTypes.LITERAL_VOID;
198     }
199 
200     /**
201      * Checks that method has only {@code String[]} or only {@code String...} param.
202      * @param method the METHOD_DEF node
203      * @return true if check passed, false otherwise
204      */
205     private static boolean checkParams(DetailAST method) {
206         boolean checkPassed = false;
207         final DetailAST params = method.findFirstToken(TokenTypes.PARAMETERS);
208 
209         if (params.getChildCount() == 1) {
210             final DetailAST parameterType = params.getFirstChild().findFirstToken(TokenTypes.TYPE);
211             final Optional<DetailAST> arrayDecl = Optional.ofNullable(
212                 parameterType.findFirstToken(TokenTypes.ARRAY_DECLARATOR));
213             final Optional<DetailAST> varargs = Optional.ofNullable(
214                 params.getFirstChild().findFirstToken(TokenTypes.ELLIPSIS));
215 
216             if (arrayDecl.isPresent()) {
217                 checkPassed = isStringType(arrayDecl.get().getFirstChild());
218             }
219             else if (varargs.isPresent()) {
220                 checkPassed = isStringType(parameterType.getFirstChild());
221             }
222         }
223         return checkPassed;
224     }
225 
226     /**
227      * Whether the type is java.lang.String.
228      * @param typeAst the type to check.
229      * @return true, if the type is java.lang.String.
230      */
231     private static boolean isStringType(DetailAST typeAst) {
232         final FullIdent type = FullIdent.createFullIdent(typeAst);
233         return "String".equals(type.getText())
234             || "java.lang.String".equals(type.getText());
235     }
236 
237 }