View Javadoc
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   * &lt;module name="IllegalInstantiation"/&gt;
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 }