1 ////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code for adherence to a set of rules.
3 // Copyright (C) 2001-2014 Oliver Burn
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ////////////////////////////////////////////////////////////////////////////////
19 package com.puppycrawl.tools.checkstyle.checks.coding;
20
21 import antlr.collections.AST;
22 import com.google.common.collect.Sets;
23 import com.puppycrawl.tools.checkstyle.api.Check;
24 import com.puppycrawl.tools.checkstyle.api.DetailAST;
25 import com.puppycrawl.tools.checkstyle.api.FullIdent;
26 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
27 import com.puppycrawl.tools.checkstyle.api.Utils;
28 import java.util.Set;
29 import java.util.StringTokenizer;
30
31 // TODO: Clean up potential duplicate code here and in UnusedImportsCheck
32 /**
33 * <p>
34 * Checks for illegal instantiations where a factory method is preferred.
35 * </p>
36 * <p>
37 * Rationale: Depending on the project, for some classes it might be
38 * preferable to create instances through factory methods rather than
39 * calling the constructor.
40 * </p>
41 * <p>
42 * A simple example is the java.lang.Boolean class, to save memory and CPU
43 * cycles it is preferable to use the predeifined constants TRUE and FALSE.
44 * Constructor invocations should be replaced by calls to Boolean.valueOf().
45 * </p>
46 * <p>
47 * Some extremely performance sensitive projects may require the use of factory
48 * methods for other classes as well, to enforce the usage of number caches or
49 * object pools.
50 * </p>
51 * <p>
52 * Limitations: It is currently not possible to specify array classes.
53 * </p>
54 * <p>
55 * An example of how to configure the check is:
56 * </p>
57 * <pre>
58 * <module name="IllegalInstantiation"/>
59 * </pre>
60 * @author lkuehne
61 */
62 public class IllegalInstantiationCheck
63 extends Check
64 {
65 /** Set of fully qualified classnames. E.g. "java.lang.Boolean" */
66 private final Set<String> illegalClasses = Sets.newHashSet();
67
68 /** name of the package */
69 private String pkgName;
70
71 /** the imports for the file */
72 private final Set<FullIdent> imports = Sets.newHashSet();
73
74 /** the class names defined in the file */
75 private final Set<String> classNames = Sets.newHashSet();
76
77 /** the instantiations in the file */
78 private final Set<DetailAST> instantiations = Sets.newHashSet();
79
80 @Override
81 public int[] getDefaultTokens()
82 {
83 return new int[] {
84 TokenTypes.IMPORT,
85 TokenTypes.LITERAL_NEW,
86 TokenTypes.PACKAGE_DEF,
87 TokenTypes.CLASS_DEF,
88 };
89 }
90
91 @Override
92 public int[] getAcceptableTokens()
93 {
94 // Return an empty array to not allow user to change configuration.
95 return new int[] {};
96 }
97
98 @Override
99 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 }