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 to function. 053 * </p> 054 * 055 * @since 3.2 056 */ 057@FileStatefulCheck 058public class UncommentedMainCheck extends AbstractCheck { 059 060 /** 061 * A key is pointing to the warning message text in "messages.properties" file. 062 */ 063 public static final String MSG_KEY = "uncommented.main"; 064 065 /** Set of possible String array types. */ 066 private static final Set<String> STRING_PARAMETER_NAMES = Set.of( 067 String[].class.getCanonicalName(), 068 String.class.getCanonicalName(), 069 String[].class.getSimpleName(), 070 String.class.getSimpleName() 071 ); 072 073 /** 074 * Specify pattern for qualified names of classes which are allowed to 075 * have a {@code main} method. 076 */ 077 private Pattern excludedClasses = Pattern.compile("^$"); 078 /** Current class name. */ 079 private String currentClass; 080 /** Current package. */ 081 private FullIdent packageName; 082 /** Class definition depth. */ 083 private int classDepth; 084 085 /** 086 * Creates a new {@code UncommentedMainCheck} instance. 087 */ 088 public UncommentedMainCheck() { 089 // no code by default 090 } 091 092 /** 093 * Setter to specify pattern for qualified names of classes which are allowed 094 * to have a {@code main} method. 095 * 096 * @param excludedClasses a pattern 097 * @since 3.2 098 */ 099 public void setExcludedClasses(Pattern excludedClasses) { 100 this.excludedClasses = excludedClasses; 101 } 102 103 @Override 104 public int[] getAcceptableTokens() { 105 return getRequiredTokens(); 106 } 107 108 @Override 109 public int[] getDefaultTokens() { 110 return getRequiredTokens(); 111 } 112 113 @Override 114 public int[] getRequiredTokens() { 115 return new int[] { 116 TokenTypes.METHOD_DEF, 117 TokenTypes.CLASS_DEF, 118 TokenTypes.PACKAGE_DEF, 119 TokenTypes.RECORD_DEF, 120 }; 121 } 122 123 @Override 124 public void beginTree(DetailAST rootAST) { 125 packageName = FullIdent.createFullIdent(null); 126 classDepth = 0; 127 } 128 129 @Override 130 public void leaveToken(DetailAST ast) { 131 if (ast.getType() == TokenTypes.CLASS_DEF) { 132 classDepth--; 133 } 134 } 135 136 @Override 137 public void visitToken(DetailAST ast) { 138 switch (ast.getType()) { 139 case TokenTypes.PACKAGE_DEF -> visitPackageDef(ast); 140 case TokenTypes.RECORD_DEF, TokenTypes.CLASS_DEF -> visitClassOrRecordDef(ast); 141 case TokenTypes.METHOD_DEF -> visitMethodDef(ast); 142 default -> throw new IllegalStateException(ast.toString()); 143 } 144 } 145 146 /** 147 * Sets current package. 148 * 149 * @param packageDef node for package definition 150 */ 151 private void visitPackageDef(DetailAST packageDef) { 152 packageName = FullIdent.createFullIdent(packageDef.getLastChild() 153 .getPreviousSibling()); 154 } 155 156 /** 157 * If not inner class then change current class name. 158 * 159 * @param classOrRecordDef node for class or record definition 160 */ 161 private void visitClassOrRecordDef(DetailAST classOrRecordDef) { 162 // we are not use inner classes because they can not 163 // have static methods 164 if (classDepth == 0) { 165 final DetailAST ident = 166 NullUtil.notNull(classOrRecordDef.findFirstToken(TokenTypes.IDENT)); 167 currentClass = packageName.getText() + "." + ident.getText(); 168 classDepth++; 169 } 170 } 171 172 /** 173 * Checks method definition if this is 174 * {@code public static void main(String[])}. 175 * 176 * @param method method definition node 177 */ 178 private void visitMethodDef(DetailAST method) { 179 if (classDepth == 1 180 // method not in inner class or in interface definition 181 && checkClassName() 182 && checkName(method) 183 && checkModifiers(method) 184 && checkType(method) 185 && checkParams(method)) { 186 log(method, MSG_KEY); 187 } 188 } 189 190 /** 191 * Checks that current class is not excluded. 192 * 193 * @return true if check passed, false otherwise 194 */ 195 private boolean checkClassName() { 196 return !excludedClasses.matcher(currentClass).find(); 197 } 198 199 /** 200 * Checks that method name is @quot;main@quot;. 201 * 202 * @param method the METHOD_DEF node 203 * @return true if check passed, false otherwise 204 */ 205 private static boolean checkName(DetailAST method) { 206 final DetailAST ident = NullUtil.notNull(method.findFirstToken(TokenTypes.IDENT)); 207 return "main".equals(ident.getText()); 208 } 209 210 /** 211 * Checks that method has final and static modifiers. 212 * 213 * @param method the METHOD_DEF node 214 * @return true if check passed, false otherwise 215 */ 216 private static boolean checkModifiers(DetailAST method) { 217 final DetailAST modifiers = 218 NullUtil.notNull(method.findFirstToken(TokenTypes.MODIFIERS)); 219 220 return modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null 221 && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null; 222 } 223 224 /** 225 * Checks that return type is {@code void}. 226 * 227 * @param method the METHOD_DEF node 228 * @return true if check passed, false otherwise 229 */ 230 private static boolean checkType(DetailAST method) { 231 final DetailAST type = 232 NullUtil.notNull(method.findFirstToken(TokenTypes.TYPE)).getFirstChild(); 233 return type.getType() == TokenTypes.LITERAL_VOID; 234 } 235 236 /** 237 * Checks that method has only {@code String[]} or only {@code String...} param. 238 * 239 * @param method the METHOD_DEF node 240 * @return true if check passed, false otherwise 241 */ 242 private static boolean checkParams(DetailAST method) { 243 boolean checkPassed = false; 244 final DetailAST params = 245 NullUtil.notNull(method.findFirstToken(TokenTypes.PARAMETERS)); 246 247 if (params.getChildCount() == 1) { 248 final DetailAST parameterType = 249 NullUtil.notNull(params.getFirstChild() 250 .findFirstToken(TokenTypes.TYPE)); 251 final boolean isArrayDeclaration = 252 parameterType.findFirstToken(TokenTypes.ARRAY_DECLARATOR) != null; 253 final Optional<DetailAST> varargs = Optional.ofNullable( 254 params.getFirstChild().findFirstToken(TokenTypes.ELLIPSIS)); 255 256 if (isArrayDeclaration || varargs.isPresent()) { 257 checkPassed = isStringType(parameterType.getFirstChild()); 258 } 259 } 260 return checkPassed; 261 } 262 263 /** 264 * Whether the type is java.lang.String. 265 * 266 * @param typeAst the type to check. 267 * @return true, if the type is java.lang.String. 268 */ 269 private static boolean isStringType(DetailAST typeAst) { 270 final FullIdent type = FullIdent.createFullIdent(typeAst); 271 return STRING_PARAMETER_NAMES.contains(type.getText()); 272 } 273 274}