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 }