View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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   * Type is {@code java.lang.String[]}.
61   * Default value is {@code ""}.
62   * </li>
63   * </ul>
64   * <p>
65   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
66   * </p>
67   * <p>
68   * Violation Message Keys:
69   * </p>
70   * <ul>
71   * <li>
72   * {@code instantiation.avoid}
73   * </li>
74   * </ul>
75   *
76   * @since 3.0
77   */
78  @FileStatefulCheck
79  public class IllegalInstantiationCheck
80      extends AbstractCheck {
81  
82      /**
83       * A key is pointing to the warning message text in "messages.properties"
84       * file.
85       */
86      public static final String MSG_KEY = "instantiation.avoid";
87  
88      /** {@link java.lang} package as string. */
89      private static final String JAVA_LANG = "java.lang.";
90  
91      /** The imports for the file. */
92      private final Set<FullIdent> imports = new HashSet<>();
93  
94      /** The class names defined in the file. */
95      private final Set<String> classNames = new HashSet<>();
96  
97      /** The instantiations in the file. */
98      private final Set<DetailAST> instantiations = new HashSet<>();
99  
100     /** Specify fully qualified class names that should not be instantiated. */
101     private Set<String> classes = new HashSet<>();
102 
103     /** Name of the package. */
104     private String pkgName;
105 
106     @Override
107     public int[] getDefaultTokens() {
108         return getRequiredTokens();
109     }
110 
111     @Override
112     public int[] getAcceptableTokens() {
113         return getRequiredTokens();
114     }
115 
116     @Override
117     public int[] getRequiredTokens() {
118         return new int[] {
119             TokenTypes.IMPORT,
120             TokenTypes.LITERAL_NEW,
121             TokenTypes.PACKAGE_DEF,
122             TokenTypes.CLASS_DEF,
123         };
124     }
125 
126     @Override
127     public void beginTree(DetailAST rootAST) {
128         pkgName = null;
129         imports.clear();
130         instantiations.clear();
131         classNames.clear();
132     }
133 
134     @Override
135     public void visitToken(DetailAST ast) {
136         switch (ast.getType()) {
137             case TokenTypes.LITERAL_NEW:
138                 processLiteralNew(ast);
139                 break;
140             case TokenTypes.PACKAGE_DEF:
141                 processPackageDef(ast);
142                 break;
143             case TokenTypes.IMPORT:
144                 processImport(ast);
145                 break;
146             case TokenTypes.CLASS_DEF:
147                 processClassDef(ast);
148                 break;
149             default:
150                 throw new IllegalArgumentException("Unknown type " + ast);
151         }
152     }
153 
154     @Override
155     public void finishTree(DetailAST rootAST) {
156         instantiations.forEach(this::postProcessLiteralNew);
157     }
158 
159     /**
160      * Collects classes defined in the source file. Required
161      * to avoid false alarms for local vs. java.lang classes.
162      *
163      * @param ast the class def token.
164      */
165     private void processClassDef(DetailAST ast) {
166         final DetailAST identToken = ast.findFirstToken(TokenTypes.IDENT);
167         final String className = identToken.getText();
168         classNames.add(className);
169     }
170 
171     /**
172      * Perform processing for an import token.
173      *
174      * @param ast the import token
175      */
176     private void processImport(DetailAST ast) {
177         final FullIdent name = FullIdent.createFullIdentBelow(ast);
178         // Note: different from UnusedImportsCheck.processImport(),
179         // '.*' imports are also added here
180         imports.add(name);
181     }
182 
183     /**
184      * Perform processing for an package token.
185      *
186      * @param ast the package token
187      */
188     private void processPackageDef(DetailAST ast) {
189         final DetailAST packageNameAST = ast.getLastChild()
190                 .getPreviousSibling();
191         final FullIdent packageIdent =
192                 FullIdent.createFullIdent(packageNameAST);
193         pkgName = packageIdent.getText();
194     }
195 
196     /**
197      * Collects a "new" token.
198      *
199      * @param ast the "new" token
200      */
201     private void processLiteralNew(DetailAST ast) {
202         if (ast.getParent().getType() != TokenTypes.METHOD_REF) {
203             instantiations.add(ast);
204         }
205     }
206 
207     /**
208      * Processes one of the collected "new" tokens when walking tree
209      * has finished.
210      *
211      * @param newTokenAst the "new" token.
212      */
213     private void postProcessLiteralNew(DetailAST newTokenAst) {
214         final DetailAST typeNameAst = newTokenAst.getFirstChild();
215         final DetailAST nameSibling = typeNameAst.getNextSibling();
216         if (nameSibling.getType() != TokenTypes.ARRAY_DECLARATOR) {
217             // ast != "new Boolean[]"
218             final FullIdent typeIdent = FullIdent.createFullIdent(typeNameAst);
219             final String typeName = typeIdent.getText();
220             final String fqClassName = getIllegalInstantiation(typeName);
221             if (fqClassName != null) {
222                 log(newTokenAst, MSG_KEY, fqClassName);
223             }
224         }
225     }
226 
227     /**
228      * Checks illegal instantiations.
229      *
230      * @param className instantiated class, may or may not be qualified
231      * @return the fully qualified class name of className
232      *     or null if instantiation of className is OK
233      */
234     private String getIllegalInstantiation(String className) {
235         String fullClassName = null;
236 
237         if (classes.contains(className)) {
238             fullClassName = className;
239         }
240         else {
241             final int pkgNameLen;
242 
243             if (pkgName == null) {
244                 pkgNameLen = 0;
245             }
246             else {
247                 pkgNameLen = pkgName.length();
248             }
249 
250             for (String illegal : classes) {
251                 if (isSamePackage(className, pkgNameLen, illegal)
252                         || isStandardClass(className, illegal)) {
253                     fullClassName = illegal;
254                 }
255                 else {
256                     fullClassName = checkImportStatements(className);
257                 }
258 
259                 if (fullClassName != null) {
260                     break;
261                 }
262             }
263         }
264         return fullClassName;
265     }
266 
267     /**
268      * Check import statements.
269      *
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      *
294      * @param className class name
295      * @param pkgNameLen package name
296      * @param illegal illegal value
297      * @return true if type of the same package
298      */
299     private boolean isSamePackage(String className, int pkgNameLen, String illegal) {
300         // class from same package
301 
302         // the top level package (pkgName == null) is covered by the
303         // "illegalInstances.contains(className)" check above
304 
305         // the test is the "no garbage" version of
306         // illegal.equals(pkgName + "." + className)
307         return pkgName != null
308                 && className.length() == illegal.length() - pkgNameLen - 1
309                 && illegal.charAt(pkgNameLen) == '.'
310                 && illegal.endsWith(className)
311                 && illegal.startsWith(pkgName);
312     }
313 
314     /**
315      * Is Standard Class.
316      *
317      * @param className class name
318      * @param illegal illegal value
319      * @return true if type is standard
320      */
321     private boolean isStandardClass(String className, String illegal) {
322         boolean isStandardClass = false;
323         // class from java.lang
324         if (illegal.length() - JAVA_LANG.length() == className.length()
325             && illegal.endsWith(className)
326             && illegal.startsWith(JAVA_LANG)) {
327             // java.lang needs no import, but a class without import might
328             // also come from the same file or be in the same package.
329             // E.g. if a class defines an inner class "Boolean",
330             // the expression "new Boolean()" refers to that class,
331             // not to java.lang.Boolean
332 
333             final boolean isSameFile = classNames.contains(className);
334 
335             if (!isSameFile) {
336                 isStandardClass = true;
337             }
338         }
339         return isStandardClass;
340     }
341 
342     /**
343      * Setter to specify fully qualified class names that should not be instantiated.
344      *
345      * @param names class names
346      * @since 3.0
347      */
348     public void setClasses(String... names) {
349         classes = Arrays.stream(names).collect(Collectors.toUnmodifiableSet());
350     }
351 
352 }