001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.coding;
021
022import java.util.Arrays;
023import java.util.HashSet;
024import java.util.Optional;
025import java.util.Set;
026import java.util.stream.Collectors;
027
028import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
029import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
030import com.puppycrawl.tools.checkstyle.api.DetailAST;
031import com.puppycrawl.tools.checkstyle.api.FullIdent;
032import com.puppycrawl.tools.checkstyle.api.TokenTypes;
033import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
034
035/**
036 * <div>
037 * Checks for illegal instantiations where a factory method is preferred.
038 * </div>
039 *
040 * <p>
041 * Rationale: Depending on the project, for some classes it might be
042 * preferable to create instances through factory methods rather than
043 * calling the constructor.
044 * </p>
045 *
046 * <p>
047 * A simple example is the {@code java.lang.Boolean} class.
048 * For performance reasons, it is preferable to use the predefined constants
049 * {@code TRUE} and {@code FALSE}.
050 * Constructor invocations should be replaced by calls to {@code Boolean.valueOf()}.
051 * </p>
052 *
053 * <p>
054 * Some extremely performance sensitive projects may require the use of factory
055 * methods for other classes as well, to enforce the usage of number caches or
056 * object pools.
057 * </p>
058 *
059 * <p>
060 * Notes:
061 * There is a limitation that it is currently not possible to specify array classes.
062 * </p>
063 *
064 * @since 3.0
065 */
066@FileStatefulCheck
067public class IllegalInstantiationCheck
068    extends AbstractCheck {
069
070    /**
071     * A key is pointing to the warning message text in "messages.properties"
072     * file.
073     */
074    public static final String MSG_KEY = "instantiation.avoid";
075
076    /** {@link java.lang} package as string. */
077    private static final String JAVA_LANG = "java.lang.";
078
079    /** The imports for the file. */
080    private final Set<FullIdent> imports = new HashSet<>();
081
082    /** The class names defined in the file. */
083    private final Set<String> classNames = new HashSet<>();
084
085    /** The instantiations in the file. */
086    private final Set<DetailAST> instantiations = new HashSet<>();
087
088    /** Specify fully qualified class names that should not be instantiated. */
089    private Set<String> classes = new HashSet<>();
090
091    /** Name of the package. */
092    private String pkgName;
093
094    /**
095     * Creates a new {@code IllegalInstantiationCheck} instance.
096     */
097    public IllegalInstantiationCheck() {
098        // no code by default
099    }
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}