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