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.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 * </ul> 064 * <p> 065 * To configure the check: 066 * </p> 067 * <pre> 068 * <module name="IllegalInstantiation"/> 069 * </pre> 070 * <p>Example:</p> 071 * <pre> 072 * public class MyTest { 073 * public class Boolean { 074 * boolean a; 075 * 076 * public Boolean (boolean a) { this.a = a; } 077 * } 078 * 079 * public void myTest (boolean a, int b) { 080 * Boolean c = new Boolean(a); // OK 081 * java.lang.Boolean d = new java.lang.Boolean(a); // OK 082 * 083 * Integer e = new Integer(b); // OK 084 * Integer f = Integer.valueOf(b); // OK 085 * } 086 * } 087 * </pre> 088 * <p> 089 * To configure the check to find instantiations of {@code java.lang.Boolean} 090 * and {@code java.lang.Integer}: 091 * </p> 092 * <pre> 093 * <module name="IllegalInstantiation"> 094 * <property name="classes" value="java.lang.Boolean, 095 * java.lang.Integer"/> 096 * </module> 097 * </pre> 098 * <p>Example:</p> 099 * <pre> 100 * public class MyTest { 101 * public class Boolean { 102 * boolean a; 103 * 104 * public Boolean (boolean a) { this.a = a; } 105 * } 106 * 107 * public void myTest (boolean a, int b) { 108 * Boolean c = new Boolean(a); // OK 109 * java.lang.Boolean d = new java.lang.Boolean(a); // violation, instantiation of 110 * // java.lang.Boolean should be avoided 111 * 112 * Integer e = new Integer(b); // violation, instantiation of 113 * // java.lang.Integer should be avoided 114 * Integer f = Integer.valueOf(b); // OK 115 * } 116 * } 117 * </pre> 118 * <p> 119 * Finally, there is a limitation that it is currently not possible to specify array classes: 120 * </p> 121 * <pre> 122 * <module name="IllegalInstantiation"> 123 * <property name="classes" value="java.lang.Boolean[], 124 * Boolean[], java.lang.Integer[], Integer[]"/> 125 * </module> 126 * </pre> 127 * <p>Example:</p> 128 * <pre> 129 * public class MyTest { 130 * public void myTest () { 131 * Boolean[] newBoolArray = new Boolean[]{true,true,false}; // OK 132 * Integer[] newIntArray = new Integer[]{1,2,3}; // OK 133 * } 134 * } 135 * </pre> 136 * <p> 137 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 138 * </p> 139 * <p> 140 * Violation Message Keys: 141 * </p> 142 * <ul> 143 * <li> 144 * {@code instantiation.avoid} 145 * </li> 146 * </ul> 147 * 148 * @since 3.0 149 */ 150@FileStatefulCheck 151public class IllegalInstantiationCheck 152 extends AbstractCheck { 153 154 /** 155 * A key is pointing to the warning message text in "messages.properties" 156 * file. 157 */ 158 public static final String MSG_KEY = "instantiation.avoid"; 159 160 /** {@link java.lang} package as string. */ 161 private static final String JAVA_LANG = "java.lang."; 162 163 /** The imports for the file. */ 164 private final Set<FullIdent> imports = new HashSet<>(); 165 166 /** The class names defined in the file. */ 167 private final Set<String> classNames = new HashSet<>(); 168 169 /** The instantiations in the file. */ 170 private final Set<DetailAST> instantiations = new HashSet<>(); 171 172 /** Specify fully qualified class names that should not be instantiated. */ 173 private Set<String> classes = new HashSet<>(); 174 175 /** Name of the package. */ 176 private String pkgName; 177 178 @Override 179 public int[] getDefaultTokens() { 180 return getRequiredTokens(); 181 } 182 183 @Override 184 public int[] getAcceptableTokens() { 185 return getRequiredTokens(); 186 } 187 188 @Override 189 public int[] getRequiredTokens() { 190 return new int[] { 191 TokenTypes.IMPORT, 192 TokenTypes.LITERAL_NEW, 193 TokenTypes.PACKAGE_DEF, 194 TokenTypes.CLASS_DEF, 195 }; 196 } 197 198 @Override 199 public void beginTree(DetailAST rootAST) { 200 pkgName = null; 201 imports.clear(); 202 instantiations.clear(); 203 classNames.clear(); 204 } 205 206 @Override 207 public void visitToken(DetailAST ast) { 208 switch (ast.getType()) { 209 case TokenTypes.LITERAL_NEW: 210 processLiteralNew(ast); 211 break; 212 case TokenTypes.PACKAGE_DEF: 213 processPackageDef(ast); 214 break; 215 case TokenTypes.IMPORT: 216 processImport(ast); 217 break; 218 case TokenTypes.CLASS_DEF: 219 processClassDef(ast); 220 break; 221 default: 222 throw new IllegalArgumentException("Unknown type " + ast); 223 } 224 } 225 226 @Override 227 public void finishTree(DetailAST rootAST) { 228 instantiations.forEach(this::postProcessLiteralNew); 229 } 230 231 /** 232 * Collects classes defined in the source file. Required 233 * to avoid false alarms for local vs. java.lang classes. 234 * 235 * @param ast the class def token. 236 */ 237 private void processClassDef(DetailAST ast) { 238 final DetailAST identToken = ast.findFirstToken(TokenTypes.IDENT); 239 final String className = identToken.getText(); 240 classNames.add(className); 241 } 242 243 /** 244 * Perform processing for an import token. 245 * 246 * @param ast the import token 247 */ 248 private void processImport(DetailAST ast) { 249 final FullIdent name = FullIdent.createFullIdentBelow(ast); 250 // Note: different from UnusedImportsCheck.processImport(), 251 // '.*' imports are also added here 252 imports.add(name); 253 } 254 255 /** 256 * Perform processing for an package token. 257 * 258 * @param ast the package token 259 */ 260 private void processPackageDef(DetailAST ast) { 261 final DetailAST packageNameAST = ast.getLastChild() 262 .getPreviousSibling(); 263 final FullIdent packageIdent = 264 FullIdent.createFullIdent(packageNameAST); 265 pkgName = packageIdent.getText(); 266 } 267 268 /** 269 * Collects a "new" token. 270 * 271 * @param ast the "new" token 272 */ 273 private void processLiteralNew(DetailAST ast) { 274 if (ast.getParent().getType() != TokenTypes.METHOD_REF) { 275 instantiations.add(ast); 276 } 277 } 278 279 /** 280 * Processes one of the collected "new" tokens when walking tree 281 * has finished. 282 * 283 * @param newTokenAst the "new" token. 284 */ 285 private void postProcessLiteralNew(DetailAST newTokenAst) { 286 final DetailAST typeNameAst = newTokenAst.getFirstChild(); 287 final DetailAST nameSibling = typeNameAst.getNextSibling(); 288 if (nameSibling.getType() != TokenTypes.ARRAY_DECLARATOR) { 289 // ast != "new Boolean[]" 290 final FullIdent typeIdent = FullIdent.createFullIdent(typeNameAst); 291 final String typeName = typeIdent.getText(); 292 final String fqClassName = getIllegalInstantiation(typeName); 293 if (fqClassName != null) { 294 log(newTokenAst, MSG_KEY, fqClassName); 295 } 296 } 297 } 298 299 /** 300 * Checks illegal instantiations. 301 * 302 * @param className instantiated class, may or may not be qualified 303 * @return the fully qualified class name of className 304 * or null if instantiation of className is OK 305 */ 306 private String getIllegalInstantiation(String className) { 307 String fullClassName = null; 308 309 if (classes.contains(className)) { 310 fullClassName = className; 311 } 312 else { 313 final int pkgNameLen; 314 315 if (pkgName == null) { 316 pkgNameLen = 0; 317 } 318 else { 319 pkgNameLen = pkgName.length(); 320 } 321 322 for (String illegal : classes) { 323 if (isSamePackage(className, pkgNameLen, illegal) 324 || isStandardClass(className, illegal)) { 325 fullClassName = illegal; 326 } 327 else { 328 fullClassName = checkImportStatements(className); 329 } 330 331 if (fullClassName != null) { 332 break; 333 } 334 } 335 } 336 return fullClassName; 337 } 338 339 /** 340 * Check import statements. 341 * 342 * @param className name of the class 343 * @return value of illegal instantiated type 344 */ 345 private String checkImportStatements(String className) { 346 String illegalType = null; 347 // import statements 348 for (FullIdent importLineText : imports) { 349 String importArg = importLineText.getText(); 350 if (importArg.endsWith(".*")) { 351 importArg = importArg.substring(0, importArg.length() - 1) 352 + className; 353 } 354 if (CommonUtil.baseClassName(importArg).equals(className) 355 && classes.contains(importArg)) { 356 illegalType = importArg; 357 break; 358 } 359 } 360 return illegalType; 361 } 362 363 /** 364 * Check that type is of the same package. 365 * 366 * @param className class name 367 * @param pkgNameLen package name 368 * @param illegal illegal value 369 * @return true if type of the same package 370 */ 371 private boolean isSamePackage(String className, int pkgNameLen, String illegal) { 372 // class from same package 373 374 // the top level package (pkgName == null) is covered by the 375 // "illegalInstances.contains(className)" check above 376 377 // the test is the "no garbage" version of 378 // illegal.equals(pkgName + "." + className) 379 return pkgName != null 380 && className.length() == illegal.length() - pkgNameLen - 1 381 && illegal.charAt(pkgNameLen) == '.' 382 && illegal.endsWith(className) 383 && illegal.startsWith(pkgName); 384 } 385 386 /** 387 * Is Standard Class. 388 * 389 * @param className class name 390 * @param illegal illegal value 391 * @return true if type is standard 392 */ 393 private boolean isStandardClass(String className, String illegal) { 394 boolean isStandardClass = false; 395 // class from java.lang 396 if (illegal.length() - JAVA_LANG.length() == className.length() 397 && illegal.endsWith(className) 398 && illegal.startsWith(JAVA_LANG)) { 399 // java.lang needs no import, but a class without import might 400 // also come from the same file or be in the same package. 401 // E.g. if a class defines an inner class "Boolean", 402 // the expression "new Boolean()" refers to that class, 403 // not to java.lang.Boolean 404 405 final boolean isSameFile = classNames.contains(className); 406 407 if (!isSameFile) { 408 isStandardClass = true; 409 } 410 } 411 return isStandardClass; 412 } 413 414 /** 415 * Setter to specify fully qualified class names that should not be instantiated. 416 * 417 * @param names class names 418 */ 419 public void setClasses(String... names) { 420 classes = Arrays.stream(names).collect(Collectors.toSet()); 421 } 422 423}