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