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