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