View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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.Optional;
25  import java.util.Set;
26  import java.util.stream.Collectors;
27  
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.CommonUtil;
34  
35  /**
36   * <div>
37   * Checks for illegal instantiations where a factory method is preferred.
38   * </div>
39   *
40   * <p>
41   * Rationale: Depending on the project, for some classes it might be
42   * preferable to create instances through factory methods rather than
43   * calling the constructor.
44   * </p>
45   *
46   * <p>
47   * A simple example is the {@code java.lang.Boolean} class.
48   * For performance reasons, it is preferable to use the predefined constants
49   * {@code TRUE} and {@code FALSE}.
50   * Constructor invocations should be replaced by calls to {@code Boolean.valueOf()}.
51   * </p>
52   *
53   * <p>
54   * Some extremely performance sensitive projects may require the use of factory
55   * methods for other classes as well, to enforce the usage of number caches or
56   * object pools.
57   * </p>
58   *
59   * <p>
60   * Notes:
61   * There is a limitation that it is currently not possible to specify array classes.
62   * </p>
63   *
64   * @since 3.0
65   */
66  @FileStatefulCheck
67  public class IllegalInstantiationCheck
68      extends AbstractCheck {
69  
70      /**
71       * A key is pointing to the warning message text in "messages.properties"
72       * file.
73       */
74      public static final String MSG_KEY = "instantiation.avoid";
75  
76      /** {@link java.lang} package as string. */
77      private static final String JAVA_LANG = "java.lang.";
78  
79      /** The imports for the file. */
80      private final Set<FullIdent> imports = new HashSet<>();
81  
82      /** The class names defined in the file. */
83      private final Set<String> classNames = new HashSet<>();
84  
85      /** The instantiations in the file. */
86      private final Set<DetailAST> instantiations = new HashSet<>();
87  
88      /** Specify fully qualified class names that should not be instantiated. */
89      private Set<String> classes = new HashSet<>();
90  
91      /** Name of the package. */
92      private String pkgName;
93  
94      @Override
95      public int[] getDefaultTokens() {
96          return getRequiredTokens();
97      }
98  
99      @Override
100     public int[] getAcceptableTokens() {
101         return getRequiredTokens();
102     }
103 
104     @Override
105     public int[] getRequiredTokens() {
106         return new int[] {
107             TokenTypes.IMPORT,
108             TokenTypes.LITERAL_NEW,
109             TokenTypes.PACKAGE_DEF,
110             TokenTypes.CLASS_DEF,
111         };
112     }
113 
114     @Override
115     public void beginTree(DetailAST rootAST) {
116         pkgName = null;
117         imports.clear();
118         instantiations.clear();
119         classNames.clear();
120     }
121 
122     @Override
123     public void visitToken(DetailAST ast) {
124         switch (ast.getType()) {
125             case TokenTypes.LITERAL_NEW -> processLiteralNew(ast);
126             case TokenTypes.PACKAGE_DEF -> processPackageDef(ast);
127             case TokenTypes.IMPORT -> processImport(ast);
128             case TokenTypes.CLASS_DEF -> processClassDef(ast);
129             default -> throw new IllegalArgumentException("Unknown type " + ast);
130         }
131     }
132 
133     @Override
134     public void finishTree(DetailAST rootAST) {
135         instantiations.forEach(this::postProcessLiteralNew);
136     }
137 
138     /**
139      * Collects classes defined in the source file. Required
140      * to avoid false alarms for local vs. java.lang classes.
141      *
142      * @param ast the class def token.
143      */
144     private void processClassDef(DetailAST ast) {
145         final DetailAST identToken = ast.findFirstToken(TokenTypes.IDENT);
146         final String className = identToken.getText();
147         classNames.add(className);
148     }
149 
150     /**
151      * Perform processing for an import token.
152      *
153      * @param ast the import token
154      */
155     private void processImport(DetailAST ast) {
156         final FullIdent name = FullIdent.createFullIdentBelow(ast);
157         // Note: different from UnusedImportsCheck.processImport(),
158         // '.*' imports are also added here
159         imports.add(name);
160     }
161 
162     /**
163      * Perform processing for an package token.
164      *
165      * @param ast the package token
166      */
167     private void processPackageDef(DetailAST ast) {
168         final DetailAST packageNameAST = ast.getLastChild()
169                 .getPreviousSibling();
170         final FullIdent packageIdent =
171                 FullIdent.createFullIdent(packageNameAST);
172         pkgName = packageIdent.getText();
173     }
174 
175     /**
176      * Collects a "new" token.
177      *
178      * @param ast the "new" token
179      */
180     private void processLiteralNew(DetailAST ast) {
181         if (ast.getParent().getType() != TokenTypes.METHOD_REF) {
182             instantiations.add(ast);
183         }
184     }
185 
186     /**
187      * Processes one of the collected "new" tokens when walking tree
188      * has finished.
189      *
190      * @param newTokenAst the "new" token.
191      */
192     private void postProcessLiteralNew(DetailAST newTokenAst) {
193         final DetailAST typeNameAst = newTokenAst.getFirstChild();
194         final DetailAST nameSibling = typeNameAst.getNextSibling();
195         if (nameSibling.getType() != TokenTypes.ARRAY_DECLARATOR) {
196             // ast != "new Boolean[]"
197             final FullIdent typeIdent = FullIdent.createFullIdent(typeNameAst);
198             final String typeName = typeIdent.getText();
199             final String fqClassName = getIllegalInstantiation(typeName);
200             if (fqClassName != null) {
201                 log(newTokenAst, MSG_KEY, fqClassName);
202             }
203         }
204     }
205 
206     /**
207      * Checks illegal instantiations.
208      *
209      * @param className instantiated class, may or may not be qualified
210      * @return the fully qualified class name of className
211      *     or null if instantiation of className is OK
212      */
213     private String getIllegalInstantiation(String className) {
214         final String fullClassName;
215 
216         if (classes.contains(className)) {
217             fullClassName = className;
218         }
219         else {
220             final Optional<String> importResult = checkImportStatements(className);
221             if (importResult.isPresent()) {
222                 fullClassName = importResult.get();
223             }
224             else {
225                 final int pkgNameLen;
226 
227                 if (pkgName == null) {
228                     pkgNameLen = 0;
229                 }
230                 else {
231                     pkgNameLen = pkgName.length();
232                 }
233 
234                 fullClassName = classes.stream()
235                         .filter(illegal -> {
236                             return isSamePackage(className, pkgNameLen, illegal)
237                                     || isStandardClass(className, illegal);
238                         })
239                         .findFirst()
240                         .orElse(null);
241             }
242         }
243         return fullClassName;
244     }
245 
246     /**
247      * Check import statements.
248      *
249      * @param className name of the class
250      * @return Optional containing value of illegal instantiated type, if found
251      */
252     private Optional<String> checkImportStatements(String className) {
253         Optional<String> result = Optional.empty();
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                 result = Optional.of(importArg);
263                 break;
264             }
265         }
266         return result;
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 }