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.coding; 021 022import java.util.Arrays; 023import java.util.HashSet; 024import java.util.Set; 025import java.util.stream.Collectors; 026 027import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 028import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 029import com.puppycrawl.tools.checkstyle.api.DetailAST; 030import com.puppycrawl.tools.checkstyle.api.FullIdent; 031import com.puppycrawl.tools.checkstyle.api.TokenTypes; 032import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 033 034/** 035 * <p> 036 * Checks for illegal instantiations where a factory method is preferred. 037 * </p> 038 * <p> 039 * Rationale: Depending on the project, for some classes it might be 040 * preferable to create instances through factory methods rather than 041 * calling the constructor. 042 * </p> 043 * <p> 044 * A simple example is the {@code java.lang.Boolean} class. 045 * For performance reasons, it is preferable to use the predefined constants 046 * {@code TRUE} and {@code FALSE}. 047 * Constructor invocations should be replaced by calls to {@code Boolean.valueOf()}. 048 * </p> 049 * <p> 050 * Some extremely performance sensitive projects may require the use of factory 051 * methods for other classes as well, to enforce the usage of number caches or 052 * object pools. 053 * </p> 054 * <p> 055 * There is a limitation that it is currently not possible to specify array classes. 056 * </p> 057 * <ul> 058 * <li> 059 * Property {@code classes} - Specify fully qualified class names that should not be instantiated. 060 * Type is {@code java.lang.String[]}. 061 * Default value is {@code ""}. 062 * </li> 063 * <li> 064 * Property {@code tokens} - tokens to check 065 * Type is {@code java.lang.String[]}. 066 * Validation type is {@code tokenSet}. 067 * Default value is: 068 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CLASS_DEF"> 069 * CLASS_DEF</a>. 070 * </li> 071 * </ul> 072 * <p> 073 * To configure the check: 074 * </p> 075 * <pre> 076 * <module name="IllegalInstantiation"/> 077 * </pre> 078 * <p>Example:</p> 079 * <pre> 080 * public class MyTest { 081 * public class Boolean { 082 * boolean a; 083 * 084 * public Boolean (boolean a) { this.a = a; } 085 * } 086 * 087 * public void myTest (boolean a, int b) { 088 * Boolean c = new Boolean(a); // OK 089 * java.lang.Boolean d = new java.lang.Boolean(a); // OK 090 * 091 * Integer e = new Integer(b); // OK 092 * Integer f = Integer.valueOf(b); // OK 093 * } 094 * } 095 * </pre> 096 * <p> 097 * To configure the check to find instantiations of {@code java.lang.Boolean} 098 * and {@code java.lang.Integer}. NOTE: Even if property {@code tokens} 099 * is completely removed from the following configuration, Checkstyle will produce 100 * the same results for violation. This is because if property {@code tokens} is not 101 * defined in the configuration, Checkstyle will supply it with list of default tokens 102 * {@code CLASS_DEF, LITERAL_NEW, PACKAGE_DEF, IMPORT} for this check. The property is 103 * defined in this example only to provide clarity: 104 * </p> 105 * <pre> 106 * <module name="IllegalInstantiation"> 107 * <property name="classes" value="java.lang.Boolean, 108 * java.lang.Integer"/> 109 * <property name="tokens" value="CLASS_DEF, LITERAL_NEW, 110 * PACKAGE_DEF, IMPORT"/> 111 * </module> 112 * </pre> 113 * <p>Example:</p> 114 * <pre> 115 * public class MyTest { 116 * public class Boolean { 117 * boolean a; 118 * 119 * public Boolean (boolean a) { this.a = a; } 120 * } 121 * 122 * public void myTest (boolean a, int b) { 123 * Boolean c = new Boolean(a); // OK 124 * java.lang.Boolean d = new java.lang.Boolean(a); // violation, instantiation of 125 * // java.lang.Boolean should be avoided 126 * 127 * Integer e = new Integer(b); // violation, instantiation of 128 * // java.lang.Integer should be avoided 129 * Integer f = Integer.valueOf(b); // OK 130 * } 131 * } 132 * </pre> 133 * <p> 134 * To configure the check to allow violations for local classes vs classes 135 * defined in the check, for example {@code java.lang.Boolean}, property 136 * {@code tokens} must be defined to not mention {@code CLASS_DEF}, so its 137 * value should be {@code LITERAL_NEW, PACKAGE_DEF, IMPORT}: 138 * </p> 139 * <pre> 140 * <module name="IllegalInstantiation"> 141 * <property name="classes" value="java.lang.Boolean, 142 * java.lang.Integer"/> 143 * <property name="tokens" value="LITERAL_NEW, PACKAGE_DEF, 144 * IMPORT"/> 145 * </module> 146 * </pre> 147 * <p>Example:</p> 148 * <pre> 149 * public class MyTest { 150 * public class Boolean { 151 * boolean a; 152 * 153 * public Boolean (boolean a) { this.a = a; } 154 * } 155 * 156 * public void myTest (boolean a, int b) { 157 * Boolean c = new Boolean(a); // violation, instantiation of 158 * // java.lang.Boolean should be avoided 159 * java.lang.Boolean d = new java.lang.Boolean(a); // violation, instantiation of 160 * // java.lang.Boolean should be avoided 161 * 162 * Integer e = new Integer(b); // violation, instantiation of 163 * // java.lang.Integer should be avoided 164 * Integer f = Integer.valueOf(b); // OK 165 * } 166 * } 167 * </pre> 168 * <p> 169 * Finally, there is a limitation that it is currently not possible to specify array classes: 170 * </p> 171 * <pre> 172 * <module name="IllegalInstantiation"> 173 * <property name="classes" value="java.lang.Boolean[], 174 * Boolean[], java.lang.Integer[], Integer[]"/> 175 * </module> 176 * </pre> 177 * <p>Example:</p> 178 * <pre> 179 * public class MyTest { 180 * public void myTest () { 181 * Boolean[] newBoolArray = new Boolean[]{true,true,false}; // OK 182 * Integer[] newIntArray = new Integer[]{1,2,3}; // OK 183 * } 184 * } 185 * </pre> 186 * <p> 187 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 188 * </p> 189 * <p> 190 * Violation Message Keys: 191 * </p> 192 * <ul> 193 * <li> 194 * {@code instantiation.avoid} 195 * </li> 196 * </ul> 197 * 198 * @since 3.0 199 */ 200@FileStatefulCheck 201public class IllegalInstantiationCheck 202 extends AbstractCheck { 203 204 /** 205 * A key is pointing to the warning message text in "messages.properties" 206 * file. 207 */ 208 public static final String MSG_KEY = "instantiation.avoid"; 209 210 /** {@link java.lang} package as string. */ 211 private static final String JAVA_LANG = "java.lang."; 212 213 /** The imports for the file. */ 214 private final Set<FullIdent> imports = new HashSet<>(); 215 216 /** The class names defined in the file. */ 217 private final Set<String> classNames = new HashSet<>(); 218 219 /** The instantiations in the file. */ 220 private final Set<DetailAST> instantiations = new HashSet<>(); 221 222 /** Specify fully qualified class names that should not be instantiated. */ 223 private Set<String> classes = new HashSet<>(); 224 225 /** Name of the package. */ 226 private String pkgName; 227 228 @Override 229 public int[] getDefaultTokens() { 230 return getAcceptableTokens(); 231 } 232 233 @Override 234 public int[] getAcceptableTokens() { 235 return new int[] { 236 TokenTypes.IMPORT, 237 TokenTypes.LITERAL_NEW, 238 TokenTypes.PACKAGE_DEF, 239 TokenTypes.CLASS_DEF, 240 }; 241 } 242 243 @Override 244 public int[] getRequiredTokens() { 245 return new int[] { 246 TokenTypes.IMPORT, 247 TokenTypes.LITERAL_NEW, 248 TokenTypes.PACKAGE_DEF, 249 }; 250 } 251 252 @Override 253 public void beginTree(DetailAST rootAST) { 254 pkgName = null; 255 imports.clear(); 256 instantiations.clear(); 257 classNames.clear(); 258 } 259 260 @Override 261 public void visitToken(DetailAST ast) { 262 switch (ast.getType()) { 263 case TokenTypes.LITERAL_NEW: 264 processLiteralNew(ast); 265 break; 266 case TokenTypes.PACKAGE_DEF: 267 processPackageDef(ast); 268 break; 269 case TokenTypes.IMPORT: 270 processImport(ast); 271 break; 272 case TokenTypes.CLASS_DEF: 273 processClassDef(ast); 274 break; 275 default: 276 throw new IllegalArgumentException("Unknown type " + ast); 277 } 278 } 279 280 @Override 281 public void finishTree(DetailAST rootAST) { 282 instantiations.forEach(this::postProcessLiteralNew); 283 } 284 285 /** 286 * Collects classes defined in the source file. Required 287 * to avoid false alarms for local vs. java.lang classes. 288 * 289 * @param ast the class def token. 290 */ 291 private void processClassDef(DetailAST ast) { 292 final DetailAST identToken = ast.findFirstToken(TokenTypes.IDENT); 293 final String className = identToken.getText(); 294 classNames.add(className); 295 } 296 297 /** 298 * Perform processing for an import token. 299 * 300 * @param ast the import token 301 */ 302 private void processImport(DetailAST ast) { 303 final FullIdent name = FullIdent.createFullIdentBelow(ast); 304 // Note: different from UnusedImportsCheck.processImport(), 305 // '.*' imports are also added here 306 imports.add(name); 307 } 308 309 /** 310 * Perform processing for an package token. 311 * 312 * @param ast the package token 313 */ 314 private void processPackageDef(DetailAST ast) { 315 final DetailAST packageNameAST = ast.getLastChild() 316 .getPreviousSibling(); 317 final FullIdent packageIdent = 318 FullIdent.createFullIdent(packageNameAST); 319 pkgName = packageIdent.getText(); 320 } 321 322 /** 323 * Collects a "new" token. 324 * 325 * @param ast the "new" token 326 */ 327 private void processLiteralNew(DetailAST ast) { 328 if (ast.getParent().getType() != TokenTypes.METHOD_REF) { 329 instantiations.add(ast); 330 } 331 } 332 333 /** 334 * Processes one of the collected "new" tokens when walking tree 335 * has finished. 336 * 337 * @param newTokenAst the "new" token. 338 */ 339 private void postProcessLiteralNew(DetailAST newTokenAst) { 340 final DetailAST typeNameAst = newTokenAst.getFirstChild(); 341 final DetailAST nameSibling = typeNameAst.getNextSibling(); 342 if (nameSibling.getType() != TokenTypes.ARRAY_DECLARATOR) { 343 // ast != "new Boolean[]" 344 final FullIdent typeIdent = FullIdent.createFullIdent(typeNameAst); 345 final String typeName = typeIdent.getText(); 346 final String fqClassName = getIllegalInstantiation(typeName); 347 if (fqClassName != null) { 348 log(newTokenAst, MSG_KEY, fqClassName); 349 } 350 } 351 } 352 353 /** 354 * Checks illegal instantiations. 355 * 356 * @param className instantiated class, may or may not be qualified 357 * @return the fully qualified class name of className 358 * or null if instantiation of className is OK 359 */ 360 private String getIllegalInstantiation(String className) { 361 String fullClassName = null; 362 363 if (classes.contains(className)) { 364 fullClassName = className; 365 } 366 else { 367 final int pkgNameLen; 368 369 if (pkgName == null) { 370 pkgNameLen = 0; 371 } 372 else { 373 pkgNameLen = pkgName.length(); 374 } 375 376 for (String illegal : classes) { 377 if (isSamePackage(className, pkgNameLen, illegal) 378 || isStandardClass(className, illegal)) { 379 fullClassName = illegal; 380 } 381 else { 382 fullClassName = checkImportStatements(className); 383 } 384 385 if (fullClassName != null) { 386 break; 387 } 388 } 389 } 390 return fullClassName; 391 } 392 393 /** 394 * Check import statements. 395 * 396 * @param className name of the class 397 * @return value of illegal instantiated type 398 */ 399 private String checkImportStatements(String className) { 400 String illegalType = null; 401 // import statements 402 for (FullIdent importLineText : imports) { 403 String importArg = importLineText.getText(); 404 if (importArg.endsWith(".*")) { 405 importArg = importArg.substring(0, importArg.length() - 1) 406 + className; 407 } 408 if (CommonUtil.baseClassName(importArg).equals(className) 409 && classes.contains(importArg)) { 410 illegalType = importArg; 411 break; 412 } 413 } 414 return illegalType; 415 } 416 417 /** 418 * Check that type is of the same package. 419 * 420 * @param className class name 421 * @param pkgNameLen package name 422 * @param illegal illegal value 423 * @return true if type of the same package 424 */ 425 private boolean isSamePackage(String className, int pkgNameLen, String illegal) { 426 // class from same package 427 428 // the top level package (pkgName == null) is covered by the 429 // "illegalInstances.contains(className)" check above 430 431 // the test is the "no garbage" version of 432 // illegal.equals(pkgName + "." + className) 433 return pkgName != null 434 && className.length() == illegal.length() - pkgNameLen - 1 435 && illegal.charAt(pkgNameLen) == '.' 436 && illegal.endsWith(className) 437 && illegal.startsWith(pkgName); 438 } 439 440 /** 441 * Is Standard Class. 442 * 443 * @param className class name 444 * @param illegal illegal value 445 * @return true if type is standard 446 */ 447 private boolean isStandardClass(String className, String illegal) { 448 boolean isStandardClass = false; 449 // class from java.lang 450 if (illegal.length() - JAVA_LANG.length() == className.length() 451 && illegal.endsWith(className) 452 && illegal.startsWith(JAVA_LANG)) { 453 // java.lang needs no import, but a class without import might 454 // also come from the same file or be in the same package. 455 // E.g. if a class defines an inner class "Boolean", 456 // the expression "new Boolean()" refers to that class, 457 // not to java.lang.Boolean 458 459 final boolean isSameFile = classNames.contains(className); 460 461 if (!isSameFile) { 462 isStandardClass = true; 463 } 464 } 465 return isStandardClass; 466 } 467 468 /** 469 * Setter to specify fully qualified class names that should not be instantiated. 470 * 471 * @param names a comma separate list of class names 472 */ 473 public void setClasses(String... names) { 474 classes = Arrays.stream(names).collect(Collectors.toSet()); 475 } 476 477}