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