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   * @since 3.2
47   */
48  @FileStatefulCheck
49  public class UncommentedMainCheck
50      extends AbstractCheck {
51  
52      /**
53       * A key is pointing to the warning message text in "messages.properties"
54       * file.
55       */
56      public static final String MSG_KEY = "uncommented.main";
57  
58      /** Set of possible String array types. */
59      private static final Set<String> STRING_PARAMETER_NAMES = Set.of(
60          String[].class.getCanonicalName(),
61          String.class.getCanonicalName(),
62          String[].class.getSimpleName(),
63          String.class.getSimpleName()
64      );
65  
66      /**
67       * Specify pattern for qualified names of classes which are allowed to
68       * have a {@code main} method.
69       */
70      private Pattern excludedClasses = Pattern.compile("^$");
71      /** Current class name. */
72      private String currentClass;
73      /** Current package. */
74      private FullIdent packageName;
75      /** Class definition depth. */
76      private int classDepth;
77  
78      /**
79       * Setter to specify pattern for qualified names of classes which are allowed
80       * to have a {@code main} method.
81       *
82       * @param excludedClasses a pattern
83       * @since 3.2
84       */
85      public void setExcludedClasses(Pattern excludedClasses) {
86          this.excludedClasses = excludedClasses;
87      }
88  
89      @Override
90      public int[] getAcceptableTokens() {
91          return getRequiredTokens();
92      }
93  
94      @Override
95      public int[] getDefaultTokens() {
96          return getRequiredTokens();
97      }
98  
99      @Override
100     public int[] getRequiredTokens() {
101         return new int[] {
102             TokenTypes.METHOD_DEF,
103             TokenTypes.CLASS_DEF,
104             TokenTypes.PACKAGE_DEF,
105             TokenTypes.RECORD_DEF,
106         };
107     }
108 
109     @Override
110     public void beginTree(DetailAST rootAST) {
111         packageName = FullIdent.createFullIdent(null);
112         classDepth = 0;
113     }
114 
115     @Override
116     public void leaveToken(DetailAST ast) {
117         if (ast.getType() == TokenTypes.CLASS_DEF) {
118             classDepth--;
119         }
120     }
121 
122     @Override
123     public void visitToken(DetailAST ast) {
124         switch (ast.getType()) {
125             case TokenTypes.PACKAGE_DEF -> visitPackageDef(ast);
126             case TokenTypes.RECORD_DEF, TokenTypes.CLASS_DEF -> visitClassOrRecordDef(ast);
127             case TokenTypes.METHOD_DEF -> visitMethodDef(ast);
128             default -> throw new IllegalStateException(ast.toString());
129         }
130     }
131 
132     /**
133      * Sets current package.
134      *
135      * @param packageDef node for package definition
136      */
137     private void visitPackageDef(DetailAST packageDef) {
138         packageName = FullIdent.createFullIdent(packageDef.getLastChild()
139                 .getPreviousSibling());
140     }
141 
142     /**
143      * If not inner class then change current class name.
144      *
145      * @param classOrRecordDef node for class or record definition
146      */
147     private void visitClassOrRecordDef(DetailAST classOrRecordDef) {
148         // we are not use inner classes because they can not
149         // have static methods
150         if (classDepth == 0) {
151             final DetailAST ident =
152                     NullUtil.notNull(classOrRecordDef.findFirstToken(TokenTypes.IDENT));
153             currentClass = packageName.getText() + "." + ident.getText();
154             classDepth++;
155         }
156     }
157 
158     /**
159      * Checks method definition if this is
160      * {@code public static void main(String[])}.
161      *
162      * @param method method definition node
163      */
164     private void visitMethodDef(DetailAST method) {
165         if (classDepth == 1
166                 // method not in inner class or in interface definition
167                 && checkClassName()
168                 && checkName(method)
169                 && checkModifiers(method)
170                 && checkType(method)
171                 && checkParams(method)) {
172             log(method, MSG_KEY);
173         }
174     }
175 
176     /**
177      * Checks that current class is not excluded.
178      *
179      * @return true if check passed, false otherwise
180      */
181     private boolean checkClassName() {
182         return !excludedClasses.matcher(currentClass).find();
183     }
184 
185     /**
186      * Checks that method name is @quot;main@quot;.
187      *
188      * @param method the METHOD_DEF node
189      * @return true if check passed, false otherwise
190      */
191     private static boolean checkName(DetailAST method) {
192         final DetailAST ident = NullUtil.notNull(method.findFirstToken(TokenTypes.IDENT));
193         return "main".equals(ident.getText());
194     }
195 
196     /**
197      * Checks that method has final and static modifiers.
198      *
199      * @param method the METHOD_DEF node
200      * @return true if check passed, false otherwise
201      */
202     private static boolean checkModifiers(DetailAST method) {
203         final DetailAST modifiers =
204             NullUtil.notNull(method.findFirstToken(TokenTypes.MODIFIERS));
205 
206         return modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null
207             && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null;
208     }
209 
210     /**
211      * Checks that return type is {@code void}.
212      *
213      * @param method the METHOD_DEF node
214      * @return true if check passed, false otherwise
215      */
216     private static boolean checkType(DetailAST method) {
217         final DetailAST type =
218             NullUtil.notNull(method.findFirstToken(TokenTypes.TYPE)).getFirstChild();
219         return type.getType() == TokenTypes.LITERAL_VOID;
220     }
221 
222     /**
223      * Checks that method has only {@code String[]} or only {@code String...} param.
224      *
225      * @param method the METHOD_DEF node
226      * @return true if check passed, false otherwise
227      */
228     private static boolean checkParams(DetailAST method) {
229         boolean checkPassed = false;
230         final DetailAST params =
231                 NullUtil.notNull(method.findFirstToken(TokenTypes.PARAMETERS));
232 
233         if (params.getChildCount() == 1) {
234             final DetailAST parameterType =
235                     NullUtil.notNull(params.getFirstChild()
236                             .findFirstToken(TokenTypes.TYPE));
237             final boolean isArrayDeclaration =
238                 parameterType.findFirstToken(TokenTypes.ARRAY_DECLARATOR) != null;
239             final Optional<DetailAST> varargs = Optional.ofNullable(
240                 params.getFirstChild().findFirstToken(TokenTypes.ELLIPSIS));
241 
242             if (isArrayDeclaration || varargs.isPresent()) {
243                 checkPassed = isStringType(parameterType.getFirstChild());
244             }
245         }
246         return checkPassed;
247     }
248 
249     /**
250      * Whether the type is java.lang.String.
251      *
252      * @param typeAst the type to check.
253      * @return true, if the type is java.lang.String.
254      */
255     private static boolean isStringType(DetailAST typeAst) {
256         final FullIdent type = FullIdent.createFullIdent(typeAst);
257         return STRING_PARAMETER_NAMES.contains(type.getText());
258     }
259 
260 }