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