001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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.Set;
025import java.util.stream.Collectors;
026
027import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
028import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
029import com.puppycrawl.tools.checkstyle.api.DetailAST;
030import com.puppycrawl.tools.checkstyle.api.FullIdent;
031import com.puppycrawl.tools.checkstyle.api.TokenTypes;
032import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
033
034/**
035 * <div>
036 * Checks for illegal instantiations where a factory method is preferred.
037 * </div>
038 *
039 * <p>
040 * Rationale: Depending on the project, for some classes it might be
041 * preferable to create instances through factory methods rather than
042 * 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
055 * object pools.
056 * </p>
057 *
058 * <p>
059 * Notes:
060 * There is a limitation that it is currently not possible to specify array classes.
061 * </p>
062 *
063 * @since 3.0
064 */
065@FileStatefulCheck
066public class IllegalInstantiationCheck
067    extends AbstractCheck {
068
069    /**
070     * A key is pointing to the warning message text in "messages.properties"
071     * file.
072     */
073    public static final String MSG_KEY = "instantiation.avoid";
074
075    /** {@link java.lang} package as string. */
076    private static final String JAVA_LANG = "java.lang.";
077
078    /** The imports for the file. */
079    private final Set<FullIdent> imports = new HashSet<>();
080
081    /** The class names defined in the file. */
082    private final Set<String> classNames = new HashSet<>();
083
084    /** The instantiations in the file. */
085    private final Set<DetailAST> instantiations = new HashSet<>();
086
087    /** Specify fully qualified class names that should not be instantiated. */
088    private Set<String> classes = new HashSet<>();
089
090    /** Name of the package. */
091    private String pkgName;
092
093    @Override
094    public int[] getDefaultTokens() {
095        return getRequiredTokens();
096    }
097
098    @Override
099    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}