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