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