View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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  import com.puppycrawl.tools.checkstyle.utils.NullUtil;
32  
33  /**
34   * <div>
35   * Detects uncommented {@code main} methods.
36   * </div>
37   *
38   * <p>
39   * Rationale: A {@code main} method is often used for debugging purposes.
40   * When debugging is finished, developers often forget to remove the method,
41   * which changes the API and increases the size of the resulting class or JAR file.
42   * Except for the real program entry points, all {@code main} methods
43   * should be removed or commented out of the sources.
44   * </p>
45   *
46   * <p>
47   * Note: Compact source files
48   * (<a href="https://openjdk.org/jeps/512">JEP 512</a>)
49   * are skipped by design. The whole purpose of compact source files is to serve as
50   * standalone single-file programs with a {@code main} method as the entry point.
51   * Unlike regular classes where leftover {@code main} methods may be
52   * debugging artifacts, compact sources inherently require a {@code main} method
53   * to function.
54   * </p>
55   *
56   * @since 3.2
57   */
58  @FileStatefulCheck
59  public class UncommentedMainCheck
60      extends AbstractCheck {
61  
62      /**
63       * A key is pointing to the warning message text in "messages.properties"
64       * file.
65       */
66      public static final String MSG_KEY = "uncommented.main";
67  
68      /** Set of possible String array types. */
69      private static final Set<String> STRING_PARAMETER_NAMES = Set.of(
70          String[].class.getCanonicalName(),
71          String.class.getCanonicalName(),
72          String[].class.getSimpleName(),
73          String.class.getSimpleName()
74      );
75  
76      /**
77       * Specify pattern for qualified names of classes which are allowed to
78       * have a {@code main} method.
79       */
80      private Pattern excludedClasses = Pattern.compile("^$");
81      /** Current class name. */
82      private String currentClass;
83      /** Current package. */
84      private FullIdent packageName;
85      /** Class definition depth. */
86      private int classDepth;
87  
88      /**
89       * Creates a new {@code UncommentedMainCheck} instance.
90       */
91      public UncommentedMainCheck() {
92          // no code by default
93      }
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 -> visitPackageDef(ast);
143             case TokenTypes.RECORD_DEF, TokenTypes.CLASS_DEF -> visitClassOrRecordDef(ast);
144             case TokenTypes.METHOD_DEF -> visitMethodDef(ast);
145             default -> throw new IllegalStateException(ast.toString());
146         }
147     }
148 
149     /**
150      * Sets current package.
151      *
152      * @param packageDef node for package definition
153      */
154     private void visitPackageDef(DetailAST packageDef) {
155         packageName = FullIdent.createFullIdent(packageDef.getLastChild()
156                 .getPreviousSibling());
157     }
158 
159     /**
160      * If not inner class then change current class name.
161      *
162      * @param classOrRecordDef node for class or record definition
163      */
164     private void visitClassOrRecordDef(DetailAST classOrRecordDef) {
165         // we are not use inner classes because they can not
166         // have static methods
167         if (classDepth == 0) {
168             final DetailAST ident =
169                     NullUtil.notNull(classOrRecordDef.findFirstToken(TokenTypes.IDENT));
170             currentClass = packageName.getText() + "." + ident.getText();
171             classDepth++;
172         }
173     }
174 
175     /**
176      * Checks method definition if this is
177      * {@code public static void main(String[])}.
178      *
179      * @param method method definition node
180      */
181     private void visitMethodDef(DetailAST method) {
182         if (classDepth == 1
183                 // method not in inner class or in interface definition
184                 && checkClassName()
185                 && checkName(method)
186                 && checkModifiers(method)
187                 && checkType(method)
188                 && checkParams(method)) {
189             log(method, MSG_KEY);
190         }
191     }
192 
193     /**
194      * Checks that current class is not excluded.
195      *
196      * @return true if check passed, false otherwise
197      */
198     private boolean checkClassName() {
199         return !excludedClasses.matcher(currentClass).find();
200     }
201 
202     /**
203      * Checks that method name is @quot;main@quot;.
204      *
205      * @param method the METHOD_DEF node
206      * @return true if check passed, false otherwise
207      */
208     private static boolean checkName(DetailAST method) {
209         final DetailAST ident = NullUtil.notNull(method.findFirstToken(TokenTypes.IDENT));
210         return "main".equals(ident.getText());
211     }
212 
213     /**
214      * Checks that method has final and static modifiers.
215      *
216      * @param method the METHOD_DEF node
217      * @return true if check passed, false otherwise
218      */
219     private static boolean checkModifiers(DetailAST method) {
220         final DetailAST modifiers =
221             NullUtil.notNull(method.findFirstToken(TokenTypes.MODIFIERS));
222 
223         return modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null
224             && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null;
225     }
226 
227     /**
228      * Checks that return type is {@code void}.
229      *
230      * @param method the METHOD_DEF node
231      * @return true if check passed, false otherwise
232      */
233     private static boolean checkType(DetailAST method) {
234         final DetailAST type =
235             NullUtil.notNull(method.findFirstToken(TokenTypes.TYPE)).getFirstChild();
236         return type.getType() == TokenTypes.LITERAL_VOID;
237     }
238 
239     /**
240      * Checks that method has only {@code String[]} or only {@code String...} param.
241      *
242      * @param method the METHOD_DEF node
243      * @return true if check passed, false otherwise
244      */
245     private static boolean checkParams(DetailAST method) {
246         boolean checkPassed = false;
247         final DetailAST params =
248                 NullUtil.notNull(method.findFirstToken(TokenTypes.PARAMETERS));
249 
250         if (params.getChildCount() == 1) {
251             final DetailAST parameterType =
252                     NullUtil.notNull(params.getFirstChild()
253                             .findFirstToken(TokenTypes.TYPE));
254             final boolean isArrayDeclaration =
255                 parameterType.findFirstToken(TokenTypes.ARRAY_DECLARATOR) != null;
256             final Optional<DetailAST> varargs = Optional.ofNullable(
257                 params.getFirstChild().findFirstToken(TokenTypes.ELLIPSIS));
258 
259             if (isArrayDeclaration || varargs.isPresent()) {
260                 checkPassed = isStringType(parameterType.getFirstChild());
261             }
262         }
263         return checkPassed;
264     }
265 
266     /**
267      * Whether the type is java.lang.String.
268      *
269      * @param typeAst the type to check.
270      * @return true, if the type is java.lang.String.
271      */
272     private static boolean isStringType(DetailAST typeAst) {
273         final FullIdent type = FullIdent.createFullIdent(typeAst);
274         return STRING_PARAMETER_NAMES.contains(type.getText());
275     }
276 
277 }