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:
145                processLiteralNew(ast);
146                break;
147            case TokenTypes.PACKAGE_DEF:
148                processPackageDef(ast);
149                break;
150            case TokenTypes.IMPORT:
151                processImport(ast);
152                break;
153            case TokenTypes.CLASS_DEF:
154                processClassDef(ast);
155                break;
156            default:
157                throw new IllegalArgumentException("Unknown type " + ast);
158        }
159    }
160
161    @Override
162    public void finishTree(DetailAST rootAST) {
163        instantiations.forEach(this::postProcessLiteralNew);
164    }
165
166    /**
167     * Collects classes defined in the source file. Required
168     * to avoid false alarms for local vs. java.lang classes.
169     *
170     * @param ast the class def token.
171     */
172    private void processClassDef(DetailAST ast) {
173        final DetailAST identToken = ast.findFirstToken(TokenTypes.IDENT);
174        final String className = identToken.getText();
175        classNames.add(className);
176    }
177
178    /**
179     * Perform processing for an import token.
180     *
181     * @param ast the import token
182     */
183    private void processImport(DetailAST ast) {
184        final FullIdent name = FullIdent.createFullIdentBelow(ast);
185        // Note: different from UnusedImportsCheck.processImport(),
186        // '.*' imports are also added here
187        imports.add(name);
188    }
189
190    /**
191     * Perform processing for an package token.
192     *
193     * @param ast the package token
194     */
195    private void processPackageDef(DetailAST ast) {
196        final DetailAST packageNameAST = ast.getLastChild()
197                .getPreviousSibling();
198        final FullIdent packageIdent =
199                FullIdent.createFullIdent(packageNameAST);
200        pkgName = packageIdent.getText();
201    }
202
203    /**
204     * Collects a "new" token.
205     *
206     * @param ast the "new" token
207     */
208    private void processLiteralNew(DetailAST ast) {
209        if (ast.getParent().getType() != TokenTypes.METHOD_REF) {
210            instantiations.add(ast);
211        }
212    }
213
214    /**
215     * Processes one of the collected "new" tokens when walking tree
216     * has finished.
217     *
218     * @param newTokenAst the "new" token.
219     */
220    private void postProcessLiteralNew(DetailAST newTokenAst) {
221        final DetailAST typeNameAst = newTokenAst.getFirstChild();
222        final DetailAST nameSibling = typeNameAst.getNextSibling();
223        if (nameSibling.getType() != TokenTypes.ARRAY_DECLARATOR) {
224            // ast != "new Boolean[]"
225            final FullIdent typeIdent = FullIdent.createFullIdent(typeNameAst);
226            final String typeName = typeIdent.getText();
227            final String fqClassName = getIllegalInstantiation(typeName);
228            if (fqClassName != null) {
229                log(newTokenAst, MSG_KEY, fqClassName);
230            }
231        }
232    }
233
234    /**
235     * Checks illegal instantiations.
236     *
237     * @param className instantiated class, may or may not be qualified
238     * @return the fully qualified class name of className
239     *     or null if instantiation of className is OK
240     */
241    private String getIllegalInstantiation(String className) {
242        String fullClassName = null;
243
244        if (classes.contains(className)) {
245            fullClassName = className;
246        }
247        else {
248            final int pkgNameLen;
249
250            if (pkgName == null) {
251                pkgNameLen = 0;
252            }
253            else {
254                pkgNameLen = pkgName.length();
255            }
256
257            for (String illegal : classes) {
258                if (isSamePackage(className, pkgNameLen, illegal)
259                        || isStandardClass(className, illegal)) {
260                    fullClassName = illegal;
261                }
262                else {
263                    fullClassName = checkImportStatements(className);
264                }
265
266                if (fullClassName != null) {
267                    break;
268                }
269            }
270        }
271        return fullClassName;
272    }
273
274    /**
275     * Check import statements.
276     *
277     * @param className name of the class
278     * @return value of illegal instantiated type
279     */
280    private String checkImportStatements(String className) {
281        String illegalType = null;
282        // import statements
283        for (FullIdent importLineText : imports) {
284            String importArg = importLineText.getText();
285            if (importArg.endsWith(".*")) {
286                importArg = importArg.substring(0, importArg.length() - 1)
287                        + className;
288            }
289            if (CommonUtil.baseClassName(importArg).equals(className)
290                    && classes.contains(importArg)) {
291                illegalType = importArg;
292                break;
293            }
294        }
295        return illegalType;
296    }
297
298    /**
299     * Check that type is of the same package.
300     *
301     * @param className class name
302     * @param pkgNameLen package name
303     * @param illegal illegal value
304     * @return true if type of the same package
305     */
306    private boolean isSamePackage(String className, int pkgNameLen, String illegal) {
307        // class from same package
308
309        // the top level package (pkgName == null) is covered by the
310        // "illegalInstances.contains(className)" check above
311
312        // the test is the "no garbage" version of
313        // illegal.equals(pkgName + "." + className)
314        return pkgName != null
315                && className.length() == illegal.length() - pkgNameLen - 1
316                && illegal.charAt(pkgNameLen) == '.'
317                && illegal.endsWith(className)
318                && illegal.startsWith(pkgName);
319    }
320
321    /**
322     * Is Standard Class.
323     *
324     * @param className class name
325     * @param illegal illegal value
326     * @return true if type is standard
327     */
328    private boolean isStandardClass(String className, String illegal) {
329        boolean isStandardClass = false;
330        // class from java.lang
331        if (illegal.length() - JAVA_LANG.length() == className.length()
332            && illegal.endsWith(className)
333            && illegal.startsWith(JAVA_LANG)) {
334            // java.lang needs no import, but a class without import might
335            // also come from the same file or be in the same package.
336            // E.g. if a class defines an inner class "Boolean",
337            // the expression "new Boolean()" refers to that class,
338            // not to java.lang.Boolean
339
340            final boolean isSameFile = classNames.contains(className);
341
342            if (!isSameFile) {
343                isStandardClass = true;
344            }
345        }
346        return isStandardClass;
347    }
348
349    /**
350     * Setter to specify fully qualified class names that should not be instantiated.
351     *
352     * @param names class names
353     * @since 3.0
354     */
355    public void setClasses(String... names) {
356        classes = Arrays.stream(names).collect(Collectors.toUnmodifiableSet());
357    }
358
359}