View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.Set;
24  import java.util.regex.Pattern;
25  
26  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
27  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28  import com.puppycrawl.tools.checkstyle.api.DetailAST;
29  import com.puppycrawl.tools.checkstyle.api.FullIdent;
30  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
31  
32  /**
33   * <p>
34   * Detects uncommented {@code main} methods.
35   * </p>
36   * <p>
37   * Rationale: A {@code main} method is often used for debugging purposes.
38   * When debugging is finished, developers often forget to remove the method,
39   * which changes the API and increases the size of the resulting class or JAR file.
40   * Except for the real program entry points, all {@code main} methods
41   * should be removed or commented out of the sources.
42   * </p>
43   * <ul>
44   * <li>
45   * Property {@code excludedClasses} - Specify pattern for qualified names of
46   * classes which are allowed to have a {@code main} method.
47   * Type is {@code java.util.regex.Pattern}.
48   * Default value is {@code "^$"}.
49   * </li>
50   * </ul>
51   * <p>
52   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
53   * </p>
54   * <p>
55   * Violation Message Keys:
56   * </p>
57   * <ul>
58   * <li>
59   * {@code uncommented.main}
60   * </li>
61   * </ul>
62   *
63   * @since 3.2
64   */
65  @FileStatefulCheck
66  public class UncommentedMainCheck
67      extends AbstractCheck {
68  
69      /**
70       * A key is pointing to the warning message text in "messages.properties"
71       * file.
72       */
73      public static final String MSG_KEY = "uncommented.main";
74  
75      /** Set of possible String array types. */
76      private static final Set<String> STRING_PARAMETER_NAMES = Set.of(
77          String[].class.getCanonicalName(),
78          String.class.getCanonicalName(),
79          String[].class.getSimpleName(),
80          String.class.getSimpleName()
81      );
82  
83      /**
84       * Specify pattern for qualified names of classes which are allowed to
85       * have a {@code main} method.
86       */
87      private Pattern excludedClasses = Pattern.compile("^$");
88      /** Current class name. */
89      private String currentClass;
90      /** Current package. */
91      private FullIdent packageName;
92      /** Class definition depth. */
93      private int classDepth;
94  
95      /**
96       * Setter to specify pattern for qualified names of classes which are allowed
97       * to have a {@code main} method.
98       *
99       * @param excludedClasses a pattern
100      * @since 3.2
101      */
102     public void setExcludedClasses(Pattern excludedClasses) {
103         this.excludedClasses = excludedClasses;
104     }
105 
106     @Override
107     public int[] getAcceptableTokens() {
108         return getRequiredTokens();
109     }
110 
111     @Override
112     public int[] getDefaultTokens() {
113         return getRequiredTokens();
114     }
115 
116     @Override
117     public int[] getRequiredTokens() {
118         return new int[] {
119             TokenTypes.METHOD_DEF,
120             TokenTypes.CLASS_DEF,
121             TokenTypes.PACKAGE_DEF,
122             TokenTypes.RECORD_DEF,
123         };
124     }
125 
126     @Override
127     public void beginTree(DetailAST rootAST) {
128         packageName = FullIdent.createFullIdent(null);
129         classDepth = 0;
130     }
131 
132     @Override
133     public void leaveToken(DetailAST ast) {
134         if (ast.getType() == TokenTypes.CLASS_DEF) {
135             classDepth--;
136         }
137     }
138 
139     @Override
140     public void visitToken(DetailAST ast) {
141         switch (ast.getType()) {
142             case TokenTypes.PACKAGE_DEF:
143                 visitPackageDef(ast);
144                 break;
145             case TokenTypes.RECORD_DEF:
146             case TokenTypes.CLASS_DEF:
147                 visitClassOrRecordDef(ast);
148                 break;
149             case TokenTypes.METHOD_DEF:
150                 visitMethodDef(ast);
151                 break;
152             default:
153                 throw new IllegalStateException(ast.toString());
154         }
155     }
156 
157     /**
158      * Sets current package.
159      *
160      * @param packageDef node for package definition
161      */
162     private void visitPackageDef(DetailAST packageDef) {
163         packageName = FullIdent.createFullIdent(packageDef.getLastChild()
164                 .getPreviousSibling());
165     }
166 
167     /**
168      * If not inner class then change current class name.
169      *
170      * @param classOrRecordDef node for class or record definition
171      */
172     private void visitClassOrRecordDef(DetailAST classOrRecordDef) {
173         // we are not use inner classes because they can not
174         // have static methods
175         if (classDepth == 0) {
176             final DetailAST ident = classOrRecordDef.findFirstToken(TokenTypes.IDENT);
177             currentClass = packageName.getText() + "." + ident.getText();
178             classDepth++;
179         }
180     }
181 
182     /**
183      * Checks method definition if this is
184      * {@code public static void main(String[])}.
185      *
186      * @param method method definition node
187      */
188     private void visitMethodDef(DetailAST method) {
189         if (classDepth == 1
190                 // method not in inner class or in interface definition
191                 && checkClassName()
192                 && checkName(method)
193                 && checkModifiers(method)
194                 && checkType(method)
195                 && checkParams(method)) {
196             log(method, MSG_KEY);
197         }
198     }
199 
200     /**
201      * Checks that current class is not excluded.
202      *
203      * @return true if check passed, false otherwise
204      */
205     private boolean checkClassName() {
206         return !excludedClasses.matcher(currentClass).find();
207     }
208 
209     /**
210      * Checks that method name is @quot;main@quot;.
211      *
212      * @param method the METHOD_DEF node
213      * @return true if check passed, false otherwise
214      */
215     private static boolean checkName(DetailAST method) {
216         final DetailAST ident = method.findFirstToken(TokenTypes.IDENT);
217         return "main".equals(ident.getText());
218     }
219 
220     /**
221      * Checks that method has final and static modifiers.
222      *
223      * @param method the METHOD_DEF node
224      * @return true if check passed, false otherwise
225      */
226     private static boolean checkModifiers(DetailAST method) {
227         final DetailAST modifiers =
228             method.findFirstToken(TokenTypes.MODIFIERS);
229 
230         return modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null
231             && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null;
232     }
233 
234     /**
235      * Checks that return type is {@code void}.
236      *
237      * @param method the METHOD_DEF node
238      * @return true if check passed, false otherwise
239      */
240     private static boolean checkType(DetailAST method) {
241         final DetailAST type =
242             method.findFirstToken(TokenTypes.TYPE).getFirstChild();
243         return type.getType() == TokenTypes.LITERAL_VOID;
244     }
245 
246     /**
247      * Checks that method has only {@code String[]} or only {@code String...} param.
248      *
249      * @param method the METHOD_DEF node
250      * @return true if check passed, false otherwise
251      */
252     private static boolean checkParams(DetailAST method) {
253         boolean checkPassed = false;
254         final DetailAST params = method.findFirstToken(TokenTypes.PARAMETERS);
255 
256         if (params.getChildCount() == 1) {
257             final DetailAST parameterType = params.getFirstChild().findFirstToken(TokenTypes.TYPE);
258             final boolean isArrayDeclaration =
259                 parameterType.findFirstToken(TokenTypes.ARRAY_DECLARATOR) != null;
260             final Optional<DetailAST> varargs = Optional.ofNullable(
261                 params.getFirstChild().findFirstToken(TokenTypes.ELLIPSIS));
262 
263             if (isArrayDeclaration || varargs.isPresent()) {
264                 checkPassed = isStringType(parameterType.getFirstChild());
265             }
266         }
267         return checkPassed;
268     }
269 
270     /**
271      * Whether the type is java.lang.String.
272      *
273      * @param typeAst the type to check.
274      * @return true, if the type is java.lang.String.
275      */
276     private static boolean isStringType(DetailAST typeAst) {
277         final FullIdent type = FullIdent.createFullIdent(typeAst);
278         return STRING_PARAMETER_NAMES.contains(type.getText());
279     }
280 
281 }