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