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 }