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