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