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