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