001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2024 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; 031 032/** 033 * <p> 034 * Detects uncommented {@code main} methods. 035 * </p> 036 * <p> 037 * Rationale: A {@code main} method is often used for debugging purposes. 038 * When debugging is finished, developers often forget to remove the method, 039 * which changes the API and increases the size of the resulting class or JAR file. 040 * Except for the real program entry points, all {@code main} methods 041 * should be removed or commented out of the sources. 042 * </p> 043 * <ul> 044 * <li> 045 * Property {@code excludedClasses} - Specify pattern for qualified names of 046 * classes which are allowed to have a {@code main} method. 047 * Type is {@code java.util.regex.Pattern}. 048 * Default value is {@code "^$"}. 049 * </li> 050 * </ul> 051 * <p> 052 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 053 * </p> 054 * <p> 055 * Violation Message Keys: 056 * </p> 057 * <ul> 058 * <li> 059 * {@code uncommented.main} 060 * </li> 061 * </ul> 062 * 063 * @since 3.2 064 */ 065@FileStatefulCheck 066public class UncommentedMainCheck 067 extends AbstractCheck { 068 069 /** 070 * A key is pointing to the warning message text in "messages.properties" 071 * file. 072 */ 073 public static final String MSG_KEY = "uncommented.main"; 074 075 /** Set of possible String array types. */ 076 private static final Set<String> STRING_PARAMETER_NAMES = Set.of( 077 String[].class.getCanonicalName(), 078 String.class.getCanonicalName(), 079 String[].class.getSimpleName(), 080 String.class.getSimpleName() 081 ); 082 083 /** 084 * Specify pattern for qualified names of classes which are allowed to 085 * have a {@code main} method. 086 */ 087 private Pattern excludedClasses = Pattern.compile("^$"); 088 /** Current class name. */ 089 private String currentClass; 090 /** Current package. */ 091 private FullIdent packageName; 092 /** Class definition depth. */ 093 private int classDepth; 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: 143 visitPackageDef(ast); 144 break; 145 case TokenTypes.RECORD_DEF: 146 case TokenTypes.CLASS_DEF: 147 visitClassOrRecordDef(ast); 148 break; 149 case TokenTypes.METHOD_DEF: 150 visitMethodDef(ast); 151 break; 152 default: 153 throw new IllegalStateException(ast.toString()); 154 } 155 } 156 157 /** 158 * Sets current package. 159 * 160 * @param packageDef node for package definition 161 */ 162 private void visitPackageDef(DetailAST packageDef) { 163 packageName = FullIdent.createFullIdent(packageDef.getLastChild() 164 .getPreviousSibling()); 165 } 166 167 /** 168 * If not inner class then change current class name. 169 * 170 * @param classOrRecordDef node for class or record definition 171 */ 172 private void visitClassOrRecordDef(DetailAST classOrRecordDef) { 173 // we are not use inner classes because they can not 174 // have static methods 175 if (classDepth == 0) { 176 final DetailAST ident = classOrRecordDef.findFirstToken(TokenTypes.IDENT); 177 currentClass = packageName.getText() + "." + ident.getText(); 178 classDepth++; 179 } 180 } 181 182 /** 183 * Checks method definition if this is 184 * {@code public static void main(String[])}. 185 * 186 * @param method method definition node 187 */ 188 private void visitMethodDef(DetailAST method) { 189 if (classDepth == 1 190 // method not in inner class or in interface definition 191 && checkClassName() 192 && checkName(method) 193 && checkModifiers(method) 194 && checkType(method) 195 && checkParams(method)) { 196 log(method, MSG_KEY); 197 } 198 } 199 200 /** 201 * Checks that current class is not excluded. 202 * 203 * @return true if check passed, false otherwise 204 */ 205 private boolean checkClassName() { 206 return !excludedClasses.matcher(currentClass).find(); 207 } 208 209 /** 210 * Checks that method name is @quot;main@quot;. 211 * 212 * @param method the METHOD_DEF node 213 * @return true if check passed, false otherwise 214 */ 215 private static boolean checkName(DetailAST method) { 216 final DetailAST ident = method.findFirstToken(TokenTypes.IDENT); 217 return "main".equals(ident.getText()); 218 } 219 220 /** 221 * Checks that method has final and static modifiers. 222 * 223 * @param method the METHOD_DEF node 224 * @return true if check passed, false otherwise 225 */ 226 private static boolean checkModifiers(DetailAST method) { 227 final DetailAST modifiers = 228 method.findFirstToken(TokenTypes.MODIFIERS); 229 230 return modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null 231 && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null; 232 } 233 234 /** 235 * Checks that return type is {@code void}. 236 * 237 * @param method the METHOD_DEF node 238 * @return true if check passed, false otherwise 239 */ 240 private static boolean checkType(DetailAST method) { 241 final DetailAST type = 242 method.findFirstToken(TokenTypes.TYPE).getFirstChild(); 243 return type.getType() == TokenTypes.LITERAL_VOID; 244 } 245 246 /** 247 * Checks that method has only {@code String[]} or only {@code String...} param. 248 * 249 * @param method the METHOD_DEF node 250 * @return true if check passed, false otherwise 251 */ 252 private static boolean checkParams(DetailAST method) { 253 boolean checkPassed = false; 254 final DetailAST params = method.findFirstToken(TokenTypes.PARAMETERS); 255 256 if (params.getChildCount() == 1) { 257 final DetailAST parameterType = params.getFirstChild().findFirstToken(TokenTypes.TYPE); 258 final boolean isArrayDeclaration = 259 parameterType.findFirstToken(TokenTypes.ARRAY_DECLARATOR) != null; 260 final Optional<DetailAST> varargs = Optional.ofNullable( 261 params.getFirstChild().findFirstToken(TokenTypes.ELLIPSIS)); 262 263 if (isArrayDeclaration || varargs.isPresent()) { 264 checkPassed = isStringType(parameterType.getFirstChild()); 265 } 266 } 267 return checkPassed; 268 } 269 270 /** 271 * Whether the type is java.lang.String. 272 * 273 * @param typeAst the type to check. 274 * @return true, if the type is java.lang.String. 275 */ 276 private static boolean isStringType(DetailAST typeAst) { 277 final FullIdent type = FullIdent.createFullIdent(typeAst); 278 return STRING_PARAMETER_NAMES.contains(type.getText()); 279 } 280 281}