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