View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2017 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  
20  package com.puppycrawl.tools.checkstyle.checks.coding;
21  
22  import java.util.Arrays;
23  import java.util.HashSet;
24  import java.util.Set;
25  import java.util.stream.Collectors;
26  
27  import antlr.collections.AST;
28  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
29  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
30  import com.puppycrawl.tools.checkstyle.api.DetailAST;
31  import com.puppycrawl.tools.checkstyle.api.FullIdent;
32  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
33  import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
34  
35  /**
36   * <p>
37   * Checks for illegal instantiations where a factory method is preferred.
38   * </p>
39   * <p>
40   * Rationale: Depending on the project, for some classes it might be
41   * preferable to create instances through factory methods rather than
42   * calling the constructor.
43   * </p>
44   * <p>
45   * A simple example is the java.lang.Boolean class, to save memory and CPU
46   * cycles it is preferable to use the predefined constants TRUE and FALSE.
47   * Constructor invocations should be replaced by calls to Boolean.valueOf().
48   * </p>
49   * <p>
50   * Some extremely performance sensitive projects may require the use of factory
51   * methods for other classes as well, to enforce the usage of number caches or
52   * object pools.
53   * </p>
54   * <p>
55   * Limitations: It is currently not possible to specify array classes.
56   * </p>
57   * <p>
58   * An example of how to configure the check is:
59   * </p>
60   * <pre>
61   * &lt;module name="IllegalInstantiation"/&gt;
62   * </pre>
63   * @author lkuehne
64   */
65  @FileStatefulCheck
66  public class IllegalInstantiationCheck
67      extends AbstractCheck {
68  
69      /**
70       * A key is pointing to the warning message text in "messages.properties"
71       * file.
72       */
73      public static final String MSG_KEY = "instantiation.avoid";
74  
75      /** {@link java.lang} package as string. */
76      private static final String JAVA_LANG = "java.lang.";
77  
78      /** The imports for the file. */
79      private final Set<FullIdent> imports = new HashSet<>();
80  
81      /** The class names defined in the file. */
82      private final Set<String> classNames = new HashSet<>();
83  
84      /** The instantiations in the file. */
85      private final Set<DetailAST> instantiations = new HashSet<>();
86  
87      /** Set of fully qualified class names. E.g. "java.lang.Boolean" */
88      private Set<String> illegalClasses = new HashSet<>();
89  
90      /** Name of the package. */
91      private String pkgName;
92  
93      @Override
94      public int[] getDefaultTokens() {
95          return getAcceptableTokens();
96      }
97  
98      @Override
99      public int[] getAcceptableTokens() {
100         return new int[] {
101             TokenTypes.IMPORT,
102             TokenTypes.LITERAL_NEW,
103             TokenTypes.PACKAGE_DEF,
104             TokenTypes.CLASS_DEF,
105         };
106     }
107 
108     @Override
109     public int[] getRequiredTokens() {
110         return new int[] {
111             TokenTypes.IMPORT,
112             TokenTypes.LITERAL_NEW,
113             TokenTypes.PACKAGE_DEF,
114         };
115     }
116 
117     @Override
118     public void beginTree(DetailAST rootAST) {
119         pkgName = null;
120         imports.clear();
121         instantiations.clear();
122         classNames.clear();
123     }
124 
125     @Override
126     public void visitToken(DetailAST ast) {
127         switch (ast.getType()) {
128             case TokenTypes.LITERAL_NEW:
129                 processLiteralNew(ast);
130                 break;
131             case TokenTypes.PACKAGE_DEF:
132                 processPackageDef(ast);
133                 break;
134             case TokenTypes.IMPORT:
135                 processImport(ast);
136                 break;
137             case TokenTypes.CLASS_DEF:
138                 processClassDef(ast);
139                 break;
140             default:
141                 throw new IllegalArgumentException("Unknown type " + ast);
142         }
143     }
144 
145     @Override
146     public void finishTree(DetailAST rootAST) {
147         instantiations.forEach(this::postProcessLiteralNew);
148     }
149 
150     /**
151      * Collects classes defined in the source file. Required
152      * to avoid false alarms for local vs. java.lang classes.
153      *
154      * @param ast the class def token.
155      */
156     private void processClassDef(DetailAST ast) {
157         final DetailAST identToken = ast.findFirstToken(TokenTypes.IDENT);
158         final String className = identToken.getText();
159         classNames.add(className);
160     }
161 
162     /**
163      * Perform processing for an import token.
164      * @param ast the import token
165      */
166     private void processImport(DetailAST ast) {
167         final FullIdent name = FullIdent.createFullIdentBelow(ast);
168         // Note: different from UnusedImportsCheck.processImport(),
169         // '.*' imports are also added here
170         imports.add(name);
171     }
172 
173     /**
174      * Perform processing for an package token.
175      * @param ast the package token
176      */
177     private void processPackageDef(DetailAST ast) {
178         final DetailAST packageNameAST = ast.getLastChild()
179                 .getPreviousSibling();
180         final FullIdent packageIdent =
181                 FullIdent.createFullIdent(packageNameAST);
182         pkgName = packageIdent.getText();
183     }
184 
185     /**
186      * Collects a "new" token.
187      * @param ast the "new" token
188      */
189     private void processLiteralNew(DetailAST ast) {
190         if (ast.getParent().getType() != TokenTypes.METHOD_REF) {
191             instantiations.add(ast);
192         }
193     }
194 
195     /**
196      * Processes one of the collected "new" tokens when walking tree
197      * has finished.
198      * @param newTokenAst the "new" token.
199      */
200     private void postProcessLiteralNew(DetailAST newTokenAst) {
201         final DetailAST typeNameAst = newTokenAst.getFirstChild();
202         final AST nameSibling = typeNameAst.getNextSibling();
203         if (nameSibling.getType() != TokenTypes.ARRAY_DECLARATOR) {
204             // ast != "new Boolean[]"
205             final FullIdent typeIdent = FullIdent.createFullIdent(typeNameAst);
206             final String typeName = typeIdent.getText();
207             final String fqClassName = getIllegalInstantiation(typeName);
208             if (fqClassName != null) {
209                 final int lineNo = newTokenAst.getLineNo();
210                 final int colNo = newTokenAst.getColumnNo();
211                 log(lineNo, colNo, MSG_KEY, fqClassName);
212             }
213         }
214     }
215 
216     /**
217      * Checks illegal instantiations.
218      * @param className instantiated class, may or may not be qualified
219      * @return the fully qualified class name of className
220      *     or null if instantiation of className is OK
221      */
222     private String getIllegalInstantiation(String className) {
223         String fullClassName = null;
224 
225         if (illegalClasses.contains(className)) {
226             fullClassName = className;
227         }
228         else {
229             final int pkgNameLen;
230 
231             if (pkgName == null) {
232                 pkgNameLen = 0;
233             }
234             else {
235                 pkgNameLen = pkgName.length();
236             }
237 
238             for (String illegal : illegalClasses) {
239                 if (isStandardClass(className, illegal)
240                         || isSamePackage(className, pkgNameLen, illegal)) {
241                     fullClassName = illegal;
242                 }
243                 else {
244                     fullClassName = checkImportStatements(className);
245                 }
246 
247                 if (fullClassName != null) {
248                     break;
249                 }
250             }
251         }
252         return fullClassName;
253     }
254 
255     /**
256      * Check import statements.
257      * @param className name of the class
258      * @return value of illegal instantiated type
259      */
260     private String checkImportStatements(String className) {
261         String illegalType = null;
262         // import statements
263         for (FullIdent importLineText : imports) {
264             String importArg = importLineText.getText();
265             if (importArg.endsWith(".*")) {
266                 importArg = importArg.substring(0, importArg.length() - 1)
267                         + className;
268             }
269             if (CommonUtils.baseClassName(importArg).equals(className)
270                     && illegalClasses.contains(importArg)) {
271                 illegalType = importArg;
272                 break;
273             }
274         }
275         return illegalType;
276     }
277 
278     /**
279      * Check that type is of the same package.
280      * @param className class name
281      * @param pkgNameLen package name
282      * @param illegal illegal value
283      * @return true if type of the same package
284      */
285     private boolean isSamePackage(String className, int pkgNameLen, String illegal) {
286         // class from same package
287 
288         // the top level package (pkgName == null) is covered by the
289         // "illegalInstances.contains(className)" check above
290 
291         // the test is the "no garbage" version of
292         // illegal.equals(pkgName + "." + className)
293         return pkgName != null
294                 && className.length() == illegal.length() - pkgNameLen - 1
295                 && illegal.charAt(pkgNameLen) == '.'
296                 && illegal.endsWith(className)
297                 && illegal.startsWith(pkgName);
298     }
299 
300     /**
301      * Is class of the same package.
302      * @param className class name
303      * @return true if same package class
304      */
305     private boolean isSamePackage(String className) {
306         boolean isSamePackage = false;
307         try {
308             final ClassLoader classLoader = getClassLoader();
309             if (classLoader != null) {
310                 final String fqName = pkgName + "." + className;
311                 classLoader.loadClass(fqName);
312                 // no ClassNotFoundException, fqName is a known class
313                 isSamePackage = true;
314             }
315         }
316         catch (final ClassNotFoundException ignored) {
317             // not a class from the same package
318             isSamePackage = false;
319         }
320         return isSamePackage;
321     }
322 
323     /**
324      * Is Standard Class.
325      * @param className class name
326      * @param illegal illegal value
327      * @return true if type is standard
328      */
329     private boolean isStandardClass(String className, String illegal) {
330         boolean isStandardClass = false;
331         // class from java.lang
332         if (illegal.length() - JAVA_LANG.length() == className.length()
333             && illegal.endsWith(className)
334             && illegal.startsWith(JAVA_LANG)) {
335             // java.lang needs no import, but a class without import might
336             // also come from the same file or be in the same package.
337             // E.g. if a class defines an inner class "Boolean",
338             // the expression "new Boolean()" refers to that class,
339             // not to java.lang.Boolean
340 
341             final boolean isSameFile = classNames.contains(className);
342             final boolean isSamePackage = isSamePackage(className);
343 
344             if (!isSameFile && !isSamePackage) {
345                 isStandardClass = true;
346             }
347         }
348         return isStandardClass;
349     }
350 
351     /**
352      * Sets the classes that are illegal to instantiate.
353      * @param names a comma separate list of class names
354      */
355     public void setClasses(String... names) {
356         illegalClasses = Arrays.stream(names).collect(Collectors.toSet());
357     }
358 }