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   *
63   * @since 3.0
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      /** Specify fully qualified class names that should not be instantiated. */
88      private Set<String> classes = new HashSet<>();
89  
90      /** Name of the package. */
91      private String pkgName;
92  
93      @Override
94      public int[] getDefaultTokens() {
95          return getRequiredTokens();
96      }
97  
98      @Override
99      public int[] getAcceptableTokens() {
100         return getRequiredTokens();
101     }
102 
103     @Override
104     public int[] getRequiredTokens() {
105         return new int[] {
106             TokenTypes.IMPORT,
107             TokenTypes.LITERAL_NEW,
108             TokenTypes.PACKAGE_DEF,
109             TokenTypes.CLASS_DEF,
110         };
111     }
112 
113     @Override
114     public void beginTree(DetailAST rootAST) {
115         pkgName = null;
116         imports.clear();
117         instantiations.clear();
118         classNames.clear();
119     }
120 
121     @Override
122     public void visitToken(DetailAST ast) {
123         switch (ast.getType()) {
124             case TokenTypes.LITERAL_NEW -> processLiteralNew(ast);
125             case TokenTypes.PACKAGE_DEF -> processPackageDef(ast);
126             case TokenTypes.IMPORT -> processImport(ast);
127             case TokenTypes.CLASS_DEF -> processClassDef(ast);
128             default -> throw new IllegalArgumentException("Unknown type " + ast);
129         }
130     }
131 
132     @Override
133     public void finishTree(DetailAST rootAST) {
134         instantiations.forEach(this::postProcessLiteralNew);
135     }
136 
137     /**
138      * Collects classes defined in the source file. Required
139      * to avoid false alarms for local vs. java.lang classes.
140      *
141      * @param ast the class def token.
142      */
143     private void processClassDef(DetailAST ast) {
144         final DetailAST identToken = ast.findFirstToken(TokenTypes.IDENT);
145         final String className = identToken.getText();
146         classNames.add(className);
147     }
148 
149     /**
150      * Perform processing for an import token.
151      *
152      * @param ast the import token
153      */
154     private void processImport(DetailAST ast) {
155         final FullIdent name = FullIdent.createFullIdentBelow(ast);
156         // Note: different from UnusedImportsCheck.processImport(),
157         // '.*' imports are also added here
158         imports.add(name);
159     }
160 
161     /**
162      * Perform processing for an package token.
163      *
164      * @param ast the package token
165      */
166     private void processPackageDef(DetailAST ast) {
167         final DetailAST packageNameAST = ast.getLastChild()
168                 .getPreviousSibling();
169         final FullIdent packageIdent =
170                 FullIdent.createFullIdent(packageNameAST);
171         pkgName = packageIdent.getText();
172     }
173 
174     /**
175      * Collects a "new" token.
176      *
177      * @param ast the "new" token
178      */
179     private void processLiteralNew(DetailAST ast) {
180         if (ast.getParent().getType() != TokenTypes.METHOD_REF) {
181             instantiations.add(ast);
182         }
183     }
184 
185     /**
186      * Processes one of the collected "new" tokens when walking tree
187      * has finished.
188      *
189      * @param newTokenAst the "new" token.
190      */
191     private void postProcessLiteralNew(DetailAST newTokenAst) {
192         final DetailAST typeNameAst = newTokenAst.getFirstChild();
193         final DetailAST nameSibling = typeNameAst.getNextSibling();
194         if (nameSibling.getType() != TokenTypes.ARRAY_DECLARATOR) {
195             // ast != "new Boolean[]"
196             final FullIdent typeIdent = FullIdent.createFullIdent(typeNameAst);
197             final String typeName = typeIdent.getText();
198             final String fqClassName = getIllegalInstantiation(typeName);
199             if (fqClassName != null) {
200                 log(newTokenAst, MSG_KEY, fqClassName);
201             }
202         }
203     }
204 
205     /**
206      * Checks illegal instantiations.
207      *
208      * @param className instantiated class, may or may not be qualified
209      * @return the fully qualified class name of className
210      *     or null if instantiation of className is OK
211      */
212     private String getIllegalInstantiation(String className) {
213         String fullClassName = null;
214 
215         if (classes.contains(className)) {
216             fullClassName = className;
217         }
218         else {
219             final int pkgNameLen;
220 
221             if (pkgName == null) {
222                 pkgNameLen = 0;
223             }
224             else {
225                 pkgNameLen = pkgName.length();
226             }
227 
228             for (String illegal : classes) {
229                 if (isSamePackage(className, pkgNameLen, illegal)
230                         || isStandardClass(className, illegal)) {
231                     fullClassName = illegal;
232                 }
233                 else {
234                     fullClassName = checkImportStatements(className);
235                 }
236 
237                 if (fullClassName != null) {
238                     break;
239                 }
240             }
241         }
242         return fullClassName;
243     }
244 
245     /**
246      * Check import statements.
247      *
248      * @param className name of the class
249      * @return value of illegal instantiated type
250      */
251     private String checkImportStatements(String className) {
252         String illegalType = null;
253         // import statements
254         for (FullIdent importLineText : imports) {
255             String importArg = importLineText.getText();
256             if (importArg.endsWith(".*")) {
257                 importArg = importArg.substring(0, importArg.length() - 1)
258                         + className;
259             }
260             if (CommonUtil.baseClassName(importArg).equals(className)
261                     && classes.contains(importArg)) {
262                 illegalType = importArg;
263                 break;
264             }
265         }
266         return illegalType;
267     }
268 
269     /**
270      * Check that type is of the same package.
271      *
272      * @param className class name
273      * @param pkgNameLen package name
274      * @param illegal illegal value
275      * @return true if type of the same package
276      */
277     private boolean isSamePackage(String className, int pkgNameLen, String illegal) {
278         // class from same package
279 
280         // the top level package (pkgName == null) is covered by the
281         // "illegalInstances.contains(className)" check above
282 
283         // the test is the "no garbage" version of
284         // illegal.equals(pkgName + "." + className)
285         return pkgName != null
286                 && className.length() == illegal.length() - pkgNameLen - 1
287                 && illegal.charAt(pkgNameLen) == '.'
288                 && illegal.endsWith(className)
289                 && illegal.startsWith(pkgName);
290     }
291 
292     /**
293      * Is Standard Class.
294      *
295      * @param className class name
296      * @param illegal illegal value
297      * @return true if type is standard
298      */
299     private boolean isStandardClass(String className, String illegal) {
300         boolean isStandardClass = false;
301         // class from java.lang
302         if (illegal.length() - JAVA_LANG.length() == className.length()
303             && illegal.endsWith(className)
304             && illegal.startsWith(JAVA_LANG)) {
305             // java.lang needs no import, but a class without import might
306             // also come from the same file or be in the same package.
307             // E.g. if a class defines an inner class "Boolean",
308             // the expression "new Boolean()" refers to that class,
309             // not to java.lang.Boolean
310 
311             final boolean isSameFile = classNames.contains(className);
312 
313             if (!isSameFile) {
314                 isStandardClass = true;
315             }
316         }
317         return isStandardClass;
318     }
319 
320     /**
321      * Setter to specify fully qualified class names that should not be instantiated.
322      *
323      * @param names class names
324      * @since 3.0
325      */
326     public void setClasses(String... names) {
327         classes = Arrays.stream(names).collect(Collectors.toUnmodifiableSet());
328     }
329 
330 }