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 }