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