001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks; 021 022import java.util.Optional; 023import java.util.Set; 024import java.util.regex.Pattern; 025 026import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 027import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 028import com.puppycrawl.tools.checkstyle.api.DetailAST; 029import com.puppycrawl.tools.checkstyle.api.FullIdent; 030import com.puppycrawl.tools.checkstyle.api.TokenTypes; 031import com.puppycrawl.tools.checkstyle.utils.NullUtil; 032 033/** 034 * <div> 035 * Detects uncommented {@code main} methods. 036 * </div> 037 * 038 * <p> 039 * Rationale: A {@code main} method is often used for debugging purposes. 040 * When debugging is finished, developers often forget to remove the method, 041 * which changes the API and increases the size of the resulting class or JAR file. 042 * Except for the real program entry points, all {@code main} methods 043 * should be removed or commented out of the sources. 044 * </p> 045 * 046 * <p> 047 * Note: Compact source files 048 * (<a href="https://openjdk.org/jeps/512">JEP 512</a>) 049 * are skipped by design. The whole purpose of compact source files is to serve as 050 * standalone single-file programs with a {@code main} method as the entry point. 051 * Unlike regular classes where leftover {@code main} methods may be 052 * debugging artifacts, compact sources inherently require a {@code main} method 053 * to function. 054 * </p> 055 * 056 * @since 3.2 057 */ 058@FileStatefulCheck 059public class UncommentedMainCheck 060 extends AbstractCheck { 061 062 /** 063 * A key is pointing to the warning message text in "messages.properties" 064 * file. 065 */ 066 public static final String MSG_KEY = "uncommented.main"; 067 068 /** Set of possible String array types. */ 069 private static final Set<String> STRING_PARAMETER_NAMES = Set.of( 070 String[].class.getCanonicalName(), 071 String.class.getCanonicalName(), 072 String[].class.getSimpleName(), 073 String.class.getSimpleName() 074 ); 075 076 /** 077 * Specify pattern for qualified names of classes which are allowed to 078 * have a {@code main} method. 079 */ 080 private Pattern excludedClasses = Pattern.compile("^$"); 081 /** Current class name. */ 082 private String currentClass; 083 /** Current package. */ 084 private FullIdent packageName; 085 /** Class definition depth. */ 086 private int classDepth; 087 088 /** 089 * Creates a new {@code UncommentedMainCheck} instance. 090 */ 091 public UncommentedMainCheck() { 092 // no code by default 093 } 094 095 /** 096 * Setter to specify pattern for qualified names of classes which are allowed 097 * to have a {@code main} method. 098 * 099 * @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}