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