1 ////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code for adherence to a set of rules.
3 // Copyright (C) 2001-2015 the original author or authors.
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
66 /**
67 * A key is pointing to the warning message text in "messages.properties"
68 * file.
69 */
70 public static final String MSG_KEY = "instantiation.avoid";
71
72 /** Set of fully qualified classnames. E.g. "java.lang.Boolean" */
73 private final Set<String> illegalClasses = Sets.newHashSet();
74
75 /** name of the package */
76 private String pkgName;
77
78 /** the imports for the file */
79 private final Set<FullIdent> imports = Sets.newHashSet();
80
81 /** the class names defined in the file */
82 private final Set<String> classNames = Sets.newHashSet();
83
84 /** the instantiations in the file */
85 private final Set<DetailAST> instantiations = Sets.newHashSet();
86
87 @Override
88 public int[] getDefaultTokens()
89 {
90 return new int[] {
91 TokenTypes.IMPORT,
92 TokenTypes.LITERAL_NEW,
93 TokenTypes.PACKAGE_DEF,
94 TokenTypes.CLASS_DEF,
95 };
96 }
97
98 @Override
99 public int[] getAcceptableTokens()
100 {
101 // Return an empty array to not allow user to change configuration.
102 return new int[] {};
103 }
104
105 @Override
106 public int[] getRequiredTokens()
107 {
108 return new int[] {
109 TokenTypes.IMPORT,
110 TokenTypes.LITERAL_NEW,
111 TokenTypes.PACKAGE_DEF,
112 };
113 }
114
115 @Override
116 public void beginTree(DetailAST rootAST)
117 {
118 super.beginTree(rootAST);
119 pkgName = null;
120 imports.clear();
121 instantiations.clear();
122 classNames.clear();
123 }
124
125 @Override
126 public void visitToken(DetailAST ast)
127 {
128 switch (ast.getType()) {
129 case TokenTypes.LITERAL_NEW:
130 processLiteralNew(ast);
131 break;
132 case TokenTypes.PACKAGE_DEF:
133 processPackageDef(ast);
134 break;
135 case TokenTypes.IMPORT:
136 processImport(ast);
137 break;
138 case TokenTypes.CLASS_DEF:
139 processClassDef(ast);
140 break;
141 default:
142 throw new IllegalArgumentException("Unknown type " + ast);
143 }
144 }
145
146 @Override
147 public void finishTree(DetailAST rootAST)
148 {
149 for (DetailAST literalNewAST : instantiations) {
150 postprocessLiteralNew(literalNewAST);
151 }
152 }
153
154 /**
155 * Collects classes defined in the source file. Required
156 * to avoid false alarms for local vs. java.lang classes.
157 *
158 * @param ast the classdef token.
159 */
160 private void processClassDef(DetailAST ast)
161 {
162 final DetailAST identToken = ast.findFirstToken(TokenTypes.IDENT);
163 final String className = identToken.getText();
164 classNames.add(className);
165 }
166
167 /**
168 * Perform processing for an import token
169 * @param ast the import token
170 */
171 private void processImport(DetailAST ast)
172 {
173 final FullIdent name = FullIdent.createFullIdentBelow(ast);
174 if (name != null) {
175 // Note: different from UnusedImportsCheck.processImport(),
176 // '.*' imports are also added here
177 imports.add(name);
178 }
179 }
180
181 /**
182 * Perform processing for an package token
183 * @param ast the package token
184 */
185 private void processPackageDef(DetailAST ast)
186 {
187 final DetailAST packageNameAST = ast.getLastChild()
188 .getPreviousSibling();
189 final FullIdent packageIdent =
190 FullIdent.createFullIdent(packageNameAST);
191 pkgName = packageIdent.getText();
192 }
193
194 /**
195 * Collects a "new" token.
196 * @param ast the "new" token
197 */
198 private void processLiteralNew(DetailAST ast)
199 {
200 if (ast.getParent().getType() == TokenTypes.METHOD_REF) {
201 return;
202 }
203 instantiations.add(ast);
204 }
205
206 /**
207 * Processes one of the collected "new" tokens when treewalking
208 * has finished.
209 * @param ast the "new" token.
210 */
211 private void postprocessLiteralNew(DetailAST ast)
212 {
213 final DetailAST typeNameAST = ast.getFirstChild();
214 final AST nameSibling = typeNameAST.getNextSibling();
215 if (nameSibling != null
216 && nameSibling.getType() == TokenTypes.ARRAY_DECLARATOR)
217 {
218 // ast == "new Boolean[]"
219 return;
220 }
221
222 final FullIdent typeIdent = FullIdent.createFullIdent(typeNameAST);
223 final String typeName = typeIdent.getText();
224 final int lineNo = ast.getLineNo();
225 final int colNo = ast.getColumnNo();
226 final String fqClassName = getIllegalInstantiation(typeName);
227 if (fqClassName != null) {
228 log(lineNo, colNo, MSG_KEY, fqClassName);
229 }
230 }
231
232 /**
233 * Checks illegal instantiations.
234 * @param className instantiated class, may or may not be qualified
235 * @return the fully qualified class name of className
236 * or null if instantiation of className is OK
237 */
238 private String getIllegalInstantiation(String className)
239 {
240 final String javlang = "java.lang.";
241
242 if (illegalClasses.contains(className)) {
243 return className;
244 }
245
246 final int clsNameLen = className.length();
247 final int pkgNameLen = pkgName == null ? 0 : pkgName.length();
248
249 for (String illegal : illegalClasses) {
250 final int illegalLen = illegal.length();
251
252 // class from java.lang
253 if (illegalLen - javlang.length() == clsNameLen
254 && illegal.endsWith(className)
255 && illegal.startsWith(javlang))
256 {
257 // java.lang needs no import, but a class without import might
258 // also come from the same file or be in the same package.
259 // E.g. if a class defines an inner class "Boolean",
260 // the expression "new Boolean()" refers to that class,
261 // not to java.lang.Boolean
262
263 final boolean isSameFile = classNames.contains(className);
264
265 boolean isSamePackage = false;
266 try {
267 final ClassLoader classLoader = getClassLoader();
268 if (classLoader != null) {
269 final String fqName = pkgName + "." + className;
270 classLoader.loadClass(fqName);
271 // no ClassNotFoundException, fqName is a known class
272 isSamePackage = true;
273 }
274 }
275 catch (final ClassNotFoundException ex) {
276 // not a class from the same package
277 isSamePackage = false;
278 }
279
280 if (!(isSameFile || isSamePackage)) {
281 return illegal;
282 }
283 }
284
285 // class from same package
286
287 // the toplevel package (pkgName == null) is covered by the
288 // "illegalInsts.contains(className)" check above
289
290 // the test is the "no garbage" version of
291 // illegal.equals(pkgName + "." + className)
292 if (pkgName != null
293 && clsNameLen == illegalLen - pkgNameLen - 1
294 && illegal.charAt(pkgNameLen) == '.'
295 && illegal.endsWith(className)
296 && illegal.startsWith(pkgName))
297 {
298 return illegal;
299 }
300 // import statements
301 for (FullIdent importLineText : imports) {
302 final String importArg = importLineText.getText();
303 if (importArg.endsWith(".*")) {
304 final String fqClass =
305 importArg.substring(0, importArg.length() - 1)
306 + className;
307 // assume that illegalInsts only contain existing classes
308 // or else we might create a false alarm here
309 if (illegalClasses.contains(fqClass)) {
310 return fqClass;
311 }
312 }
313 else {
314 if (Utils.baseClassname(importArg).equals(className)
315 && illegalClasses.contains(importArg))
316 {
317 return importArg;
318 }
319 }
320 }
321 }
322 return null;
323 }
324
325 /**
326 * Sets the classes that are illegal to instantiate.
327 * @param classNames a comma seperate list of class names
328 */
329 public void setClasses(String classNames)
330 {
331 illegalClasses.clear();
332 final StringTokenizer tok = new StringTokenizer(classNames, ",");
333 while (tok.hasMoreTokens()) {
334 illegalClasses.add(tok.nextToken());
335 }
336 }
337 }