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