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