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