001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2023 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 * To configure the check: 053 * </p> 054 * <pre> 055 * <module name="UncommentedMain"/> 056 * </pre> 057 * <p>Example:</p> 058 * <pre> 059 * public class Game { 060 * public static void main(String... args){} // violation 061 * } 062 * 063 * public class Main { 064 * public static void main(String[] args){} // violation 065 * } 066 * 067 * public class Launch { 068 * //public static void main(String[] args){} // OK 069 * } 070 * 071 * public class Start { 072 * public void main(){} // OK 073 * } 074 * 075 * public record MyRecord1 { 076 * public void main(){} // violation 077 * } 078 * 079 * public record MyRecord2 { 080 * //public void main(){} // OK 081 * } 082 * 083 * </pre> 084 * <p> 085 * To configure the check to allow the {@code main} method for all classes with "Main" name: 086 * </p> 087 * <pre> 088 * <module name="UncommentedMain"> 089 * <property name="excludedClasses" value="\.Main$"/> 090 * </module> 091 * </pre> 092 * <p>Example:</p> 093 * <pre> 094 * public class Game { 095 * public static void main(String... args){} // violation 096 * } 097 * 098 * public class Main { 099 * public static void main(String[] args){} // OK 100 * } 101 * 102 * public class Launch { 103 * //public static void main(String[] args){} // OK 104 * } 105 * 106 * public class Start { 107 * public void main(){} // OK 108 * } 109 * 110 * public record MyRecord1 { 111 * public void main(){} // OK 112 * } 113 * 114 * </pre> 115 * <p> 116 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 117 * </p> 118 * <p> 119 * Violation Message Keys: 120 * </p> 121 * <ul> 122 * <li> 123 * {@code uncommented.main} 124 * </li> 125 * </ul> 126 * 127 * @since 3.2 128 */ 129@FileStatefulCheck 130public class UncommentedMainCheck 131 extends AbstractCheck { 132 133 /** 134 * A key is pointing to the warning message text in "messages.properties" 135 * file. 136 */ 137 public static final String MSG_KEY = "uncommented.main"; 138 139 /** Set of possible String array types. */ 140 private static final Set<String> STRING_PARAMETER_NAMES = Set.of( 141 String[].class.getCanonicalName(), 142 String.class.getCanonicalName(), 143 String[].class.getSimpleName(), 144 String.class.getSimpleName() 145 ); 146 147 /** 148 * Specify pattern for qualified names of classes which are allowed to 149 * have a {@code main} method. 150 */ 151 private Pattern excludedClasses = Pattern.compile("^$"); 152 /** Current class name. */ 153 private String currentClass; 154 /** Current package. */ 155 private FullIdent packageName; 156 /** Class definition depth. */ 157 private int classDepth; 158 159 /** 160 * Setter to specify pattern for qualified names of classes which are allowed 161 * to have a {@code main} method. 162 * 163 * @param excludedClasses a pattern 164 */ 165 public void setExcludedClasses(Pattern excludedClasses) { 166 this.excludedClasses = excludedClasses; 167 } 168 169 @Override 170 public int[] getAcceptableTokens() { 171 return getRequiredTokens(); 172 } 173 174 @Override 175 public int[] getDefaultTokens() { 176 return getRequiredTokens(); 177 } 178 179 @Override 180 public int[] getRequiredTokens() { 181 return new int[] { 182 TokenTypes.METHOD_DEF, 183 TokenTypes.CLASS_DEF, 184 TokenTypes.PACKAGE_DEF, 185 TokenTypes.RECORD_DEF, 186 }; 187 } 188 189 @Override 190 public void beginTree(DetailAST rootAST) { 191 packageName = FullIdent.createFullIdent(null); 192 currentClass = null; 193 classDepth = 0; 194 } 195 196 @Override 197 public void leaveToken(DetailAST ast) { 198 if (ast.getType() == TokenTypes.CLASS_DEF) { 199 classDepth--; 200 } 201 } 202 203 @Override 204 public void visitToken(DetailAST ast) { 205 switch (ast.getType()) { 206 case TokenTypes.PACKAGE_DEF: 207 visitPackageDef(ast); 208 break; 209 case TokenTypes.RECORD_DEF: 210 case TokenTypes.CLASS_DEF: 211 visitClassOrRecordDef(ast); 212 break; 213 case TokenTypes.METHOD_DEF: 214 visitMethodDef(ast); 215 break; 216 default: 217 throw new IllegalStateException(ast.toString()); 218 } 219 } 220 221 /** 222 * Sets current package. 223 * 224 * @param packageDef node for package definition 225 */ 226 private void visitPackageDef(DetailAST packageDef) { 227 packageName = FullIdent.createFullIdent(packageDef.getLastChild() 228 .getPreviousSibling()); 229 } 230 231 /** 232 * If not inner class then change current class name. 233 * 234 * @param classOrRecordDef node for class or record definition 235 */ 236 private void visitClassOrRecordDef(DetailAST classOrRecordDef) { 237 // we are not use inner classes because they can not 238 // have static methods 239 if (classDepth == 0) { 240 final DetailAST ident = classOrRecordDef.findFirstToken(TokenTypes.IDENT); 241 currentClass = packageName.getText() + "." + ident.getText(); 242 classDepth++; 243 } 244 } 245 246 /** 247 * Checks method definition if this is 248 * {@code public static void main(String[])}. 249 * 250 * @param method method definition node 251 */ 252 private void visitMethodDef(DetailAST method) { 253 if (classDepth == 1 254 // method not in inner class or in interface definition 255 && checkClassName() 256 && checkName(method) 257 && checkModifiers(method) 258 && checkType(method) 259 && checkParams(method)) { 260 log(method, MSG_KEY); 261 } 262 } 263 264 /** 265 * Checks that current class is not excluded. 266 * 267 * @return true if check passed, false otherwise 268 */ 269 private boolean checkClassName() { 270 return !excludedClasses.matcher(currentClass).find(); 271 } 272 273 /** 274 * Checks that method name is @quot;main@quot;. 275 * 276 * @param method the METHOD_DEF node 277 * @return true if check passed, false otherwise 278 */ 279 private static boolean checkName(DetailAST method) { 280 final DetailAST ident = method.findFirstToken(TokenTypes.IDENT); 281 return "main".equals(ident.getText()); 282 } 283 284 /** 285 * Checks that method has final and static modifiers. 286 * 287 * @param method the METHOD_DEF node 288 * @return true if check passed, false otherwise 289 */ 290 private static boolean checkModifiers(DetailAST method) { 291 final DetailAST modifiers = 292 method.findFirstToken(TokenTypes.MODIFIERS); 293 294 return modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null 295 && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null; 296 } 297 298 /** 299 * Checks that return type is {@code void}. 300 * 301 * @param method the METHOD_DEF node 302 * @return true if check passed, false otherwise 303 */ 304 private static boolean checkType(DetailAST method) { 305 final DetailAST type = 306 method.findFirstToken(TokenTypes.TYPE).getFirstChild(); 307 return type.getType() == TokenTypes.LITERAL_VOID; 308 } 309 310 /** 311 * Checks that method has only {@code String[]} or only {@code String...} param. 312 * 313 * @param method the METHOD_DEF node 314 * @return true if check passed, false otherwise 315 */ 316 private static boolean checkParams(DetailAST method) { 317 boolean checkPassed = false; 318 final DetailAST params = method.findFirstToken(TokenTypes.PARAMETERS); 319 320 if (params.getChildCount() == 1) { 321 final DetailAST parameterType = params.getFirstChild().findFirstToken(TokenTypes.TYPE); 322 final boolean isArrayDeclaration = 323 parameterType.findFirstToken(TokenTypes.ARRAY_DECLARATOR) != null; 324 final Optional<DetailAST> varargs = Optional.ofNullable( 325 params.getFirstChild().findFirstToken(TokenTypes.ELLIPSIS)); 326 327 if (isArrayDeclaration || varargs.isPresent()) { 328 checkPassed = isStringType(parameterType.getFirstChild()); 329 } 330 } 331 return checkPassed; 332 } 333 334 /** 335 * Whether the type is java.lang.String. 336 * 337 * @param typeAst the type to check. 338 * @return true, if the type is java.lang.String. 339 */ 340 private static boolean isStringType(DetailAST typeAst) { 341 final FullIdent type = FullIdent.createFullIdent(typeAst); 342 return STRING_PARAMETER_NAMES.contains(type.getText()); 343 } 344 345}