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