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 }