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 calling the constructor.
043 * </p>
044 *
045 * <p>
046 * A simple example is the {@code java.lang.Boolean} class.
047 * For performance reasons, it is preferable to use the predefined constants
048 * {@code TRUE} and {@code FALSE}.
049 * Constructor invocations should be replaced by calls to {@code Boolean.valueOf()}.
050 * </p>
051 *
052 * <p>
053 * Some extremely performance sensitive projects may require the use of factory
054 * methods for other classes as well, to enforce the usage of number caches or object pools.
055 * </p>
056 *
057 * <p>
058 * Notes:
059 * There is a limitation that it is currently not possible to specify array classes.
060 * </p>
061 *
062 * @since 3.0
063 */
064@FileStatefulCheck
065public class IllegalInstantiationCheck extends AbstractCheck {
066
067    /**
068     * A key is pointing to the warning message text in "messages.properties" file.
069     */
070    public static final String MSG_KEY = "instantiation.avoid";
071
072    /** {@link java.lang} package as string. */
073    private static final String JAVA_LANG = "java.lang.";
074
075    /** The imports for the file. */
076    private final Set<FullIdent> imports = new HashSet<>();
077
078    /** The class names defined in the file. */
079    private final Set<String> classNames = new HashSet<>();
080
081    /** The instantiations in the file. */
082    private final Set<DetailAST> instantiations = new HashSet<>();
083
084    /** Specify fully qualified class names that should not be instantiated. */
085    private Set<String> classes = new HashSet<>();
086
087    /** Name of the package. */
088    private String pkgName;
089
090    /**
091     * Creates a new {@code IllegalInstantiationCheck} instance.
092     */
093    public IllegalInstantiationCheck() {
094        // no code by default
095    }
096
097    @Override
098    public int[] getDefaultTokens() {
099        return getRequiredTokens();
100    }
101
102    @Override
103    public int[] getAcceptableTokens() {
104        return getRequiredTokens();
105    }
106
107    @Override
108    public int[] getRequiredTokens() {
109        return new int[] {
110            TokenTypes.IMPORT,
111            TokenTypes.LITERAL_NEW,
112            TokenTypes.PACKAGE_DEF,
113            TokenTypes.CLASS_DEF,
114            TokenTypes.RECORD_DEF,
115        };
116    }
117
118    @Override
119    public void beginTree(DetailAST rootAST) {
120        pkgName = null;
121        imports.clear();
122        instantiations.clear();
123        classNames.clear();
124    }
125
126    @Override
127    public void visitToken(DetailAST ast) {
128        switch (ast.getType()) {
129            case TokenTypes.LITERAL_NEW -> processLiteralNew(ast);
130            case TokenTypes.PACKAGE_DEF -> processPackageDef(ast);
131            case TokenTypes.IMPORT -> processImport(ast);
132            case TokenTypes.CLASS_DEF, TokenTypes.RECORD_DEF -> processClassDef(ast);
133            default -> throw new IllegalArgumentException("Unknown type " + ast);
134        }
135    }
136
137    @Override
138    public void finishTree(DetailAST rootAST) {
139        instantiations.forEach(this::postProcessLiteralNew);
140    }
141
142    /**
143     * Collects classes and records defined in the source file. Required
144     * to avoid false alarms for local vs. java.lang classes.
145     *
146     * @param ast the class or record def token.
147     */
148    private void processClassDef(DetailAST ast) {
149        final DetailAST identToken = ast.findFirstToken(TokenTypes.IDENT);
150        final String className = identToken.getText();
151        classNames.add(className);
152    }
153
154    /**
155     * Perform processing for an import token.
156     *
157     * @param ast the import token
158     */
159    private void processImport(DetailAST ast) {
160        final FullIdent name = FullIdent.createFullIdentBelow(ast);
161        // Note: different from UnusedImportsCheck.processImport(),
162        // '.*' imports are also added here
163        imports.add(name);
164    }
165
166    /**
167     * Perform processing for an package token.
168     *
169     * @param ast the package token
170     */
171    private void processPackageDef(DetailAST ast) {
172        final DetailAST packageNameAST = ast.getLastChild()
173                .getPreviousSibling();
174        final FullIdent packageIdent =
175                FullIdent.createFullIdent(packageNameAST);
176        pkgName = packageIdent.getText();
177    }
178
179    /**
180     * Collects a "new" token.
181     *
182     * @param ast the "new" token
183     */
184    private void processLiteralNew(DetailAST ast) {
185        if (ast.getParent().getType() != TokenTypes.METHOD_REF) {
186            instantiations.add(ast);
187        }
188    }
189
190    /**
191     * Processes one of the collected "new" tokens when walking tree has finished.
192     *
193     * @param newTokenAst the "new" token.
194     */
195    private void postProcessLiteralNew(DetailAST newTokenAst) {
196        final DetailAST typeNameAst = newTokenAst.getFirstChild();
197        final DetailAST nameSibling = typeNameAst.getNextSibling();
198        if (nameSibling.getType() != TokenTypes.ARRAY_DECLARATOR) {
199            // ast != "new Boolean[]"
200            final FullIdent typeIdent = FullIdent.createFullIdent(typeNameAst);
201            final String typeName = typeIdent.getText();
202            final String fqClassName = getIllegalInstantiation(typeName);
203            if (fqClassName != null) {
204                log(newTokenAst, MSG_KEY, fqClassName);
205            }
206        }
207    }
208
209    /**
210     * Checks illegal instantiations.
211     *
212     * @param className instantiated class, may or may not be qualified
213     * @return the fully qualified class name of className
214     *     or null if instantiation of className is OK
215     */
216    private String getIllegalInstantiation(String className) {
217        final String fullClassName;
218
219        if (classes.contains(className)) {
220            fullClassName = className;
221        }
222        else {
223            final Optional<String> importResult = checkImportStatements(className);
224            if (importResult.isPresent()) {
225                fullClassName = importResult.get();
226            }
227            else {
228                final int pkgNameLen;
229
230                if (pkgName == null) {
231                    pkgNameLen = 0;
232                }
233                else {
234                    pkgNameLen = pkgName.length();
235                }
236
237                fullClassName = classes.stream()
238                        .filter(illegal -> {
239                            return isSamePackage(className, pkgNameLen, illegal)
240                                    || isStandardClass(className, illegal);
241                        })
242                        .findFirst()
243                        .orElse(null);
244            }
245        }
246        return fullClassName;
247    }
248
249    /**
250     * Check import statements.
251     *
252     * @param className name of the class
253     * @return Optional containing value of illegal instantiated type, if found
254     */
255    private Optional<String> checkImportStatements(String className) {
256        Optional<String> result = Optional.empty();
257        for (FullIdent importLineText : imports) {
258            String importArg = importLineText.getText();
259            if (importArg.endsWith(".*")) {
260                importArg = importArg.substring(0, importArg.length() - 1)
261                        + className;
262            }
263            if (CommonUtil.baseClassName(importArg).equals(className)
264                    && classes.contains(importArg)) {
265                result = Optional.of(importArg);
266                break;
267            }
268        }
269        return result;
270    }
271
272    /**
273     * Check that type is of the same package.
274     *
275     * @param className class name
276     * @param pkgNameLen package name
277     * @param illegal illegal value
278     * @return true if type of the same package
279     */
280    private boolean isSamePackage(String className, int pkgNameLen, String illegal) {
281        // class from same package
282
283        // the top level package (pkgName == null) is covered by the
284        // "illegalInstances.contains(className)" check above
285
286        // the test is the "no garbage" version of
287        // illegal.equals(pkgName + "." + className)
288        return pkgName != null
289                && className.length() == illegal.length() - pkgNameLen - 1
290                && illegal.charAt(pkgNameLen) == '.'
291                && illegal.endsWith(className)
292                && illegal.startsWith(pkgName);
293    }
294
295    /**
296     * Is Standard Class.
297     *
298     * @param className class name
299     * @param illegal illegal value
300     * @return true if type is standard
301     */
302    private boolean isStandardClass(String className, String illegal) {
303        boolean isStandardClass = false;
304        // class from java.lang
305        if (illegal.length() - JAVA_LANG.length() == className.length()
306            && illegal.endsWith(className)
307            && illegal.startsWith(JAVA_LANG)) {
308            // java.lang needs no import, but a class without import might
309            // also come from the same file or be in the same package.
310            // E.g. if a class defines an inner class "Boolean",
311            // the expression "new Boolean()" refers to that class,
312            // not to java.lang.Boolean
313
314            final boolean isSameFile = classNames.contains(className);
315
316            if (!isSameFile) {
317                isStandardClass = true;
318            }
319        }
320        return isStandardClass;
321    }
322
323    /**
324     * Setter to specify fully qualified class names that should not be instantiated.
325     *
326     * @param names class names
327     * @since 3.0
328     */
329    public void setClasses(String... names) {
330        classes = Arrays.stream(names).collect(Collectors.toUnmodifiableSet());
331    }
332
333}