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