001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 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 * Default value is {@code {}}.
061 * </li>
062 * <li>
063 * Property {@code tokens} - tokens to check
064 * Default value is:
065 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CLASS_DEF">
066 * CLASS_DEF</a>.
067 * </li>
068 * </ul>
069 * <p>
070 * To configure the check to find instantiations of {@code java.lang.Boolean}:
071 * </p>
072 * <pre>
073 * &lt;module name=&quot;IllegalInstantiation&quot;&gt;
074 *   &lt;property name=&quot;classes&quot; value=&quot;java.lang.Boolean&quot;/&gt;
075 * &lt;/module&gt;
076 * </pre>
077 *
078 * @since 3.0
079 */
080@FileStatefulCheck
081public class IllegalInstantiationCheck
082    extends AbstractCheck {
083
084    /**
085     * A key is pointing to the warning message text in "messages.properties"
086     * file.
087     */
088    public static final String MSG_KEY = "instantiation.avoid";
089
090    /** {@link java.lang} package as string. */
091    private static final String JAVA_LANG = "java.lang.";
092
093    /** The imports for the file. */
094    private final Set<FullIdent> imports = new HashSet<>();
095
096    /** The class names defined in the file. */
097    private final Set<String> classNames = new HashSet<>();
098
099    /** The instantiations in the file. */
100    private final Set<DetailAST> instantiations = new HashSet<>();
101
102    /** Specify fully qualified class names that should not be instantiated. */
103    private Set<String> classes = new HashSet<>();
104
105    /** Name of the package. */
106    private String pkgName;
107
108    @Override
109    public int[] getDefaultTokens() {
110        return getAcceptableTokens();
111    }
112
113    @Override
114    public int[] getAcceptableTokens() {
115        return new int[] {
116            TokenTypes.IMPORT,
117            TokenTypes.LITERAL_NEW,
118            TokenTypes.PACKAGE_DEF,
119            TokenTypes.CLASS_DEF,
120        };
121    }
122
123    @Override
124    public int[] getRequiredTokens() {
125        return new int[] {
126            TokenTypes.IMPORT,
127            TokenTypes.LITERAL_NEW,
128            TokenTypes.PACKAGE_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     * @param ast the import token
180     */
181    private void processImport(DetailAST ast) {
182        final FullIdent name = FullIdent.createFullIdentBelow(ast);
183        // Note: different from UnusedImportsCheck.processImport(),
184        // '.*' imports are also added here
185        imports.add(name);
186    }
187
188    /**
189     * Perform processing for an package token.
190     * @param ast the package token
191     */
192    private void processPackageDef(DetailAST ast) {
193        final DetailAST packageNameAST = ast.getLastChild()
194                .getPreviousSibling();
195        final FullIdent packageIdent =
196                FullIdent.createFullIdent(packageNameAST);
197        pkgName = packageIdent.getText();
198    }
199
200    /**
201     * Collects a "new" token.
202     * @param ast the "new" token
203     */
204    private void processLiteralNew(DetailAST ast) {
205        if (ast.getParent().getType() != TokenTypes.METHOD_REF) {
206            instantiations.add(ast);
207        }
208    }
209
210    /**
211     * Processes one of the collected "new" tokens when walking tree
212     * has finished.
213     * @param newTokenAst the "new" token.
214     */
215    private void postProcessLiteralNew(DetailAST newTokenAst) {
216        final DetailAST typeNameAst = newTokenAst.getFirstChild();
217        final DetailAST nameSibling = typeNameAst.getNextSibling();
218        if (nameSibling.getType() != TokenTypes.ARRAY_DECLARATOR) {
219            // ast != "new Boolean[]"
220            final FullIdent typeIdent = FullIdent.createFullIdent(typeNameAst);
221            final String typeName = typeIdent.getText();
222            final String fqClassName = getIllegalInstantiation(typeName);
223            if (fqClassName != null) {
224                log(newTokenAst, MSG_KEY, fqClassName);
225            }
226        }
227    }
228
229    /**
230     * Checks illegal instantiations.
231     * @param className instantiated class, may or may not be qualified
232     * @return the fully qualified class name of className
233     *     or null if instantiation of className is OK
234     */
235    private String getIllegalInstantiation(String className) {
236        String fullClassName = null;
237
238        if (classes.contains(className)) {
239            fullClassName = className;
240        }
241        else {
242            final int pkgNameLen;
243
244            if (pkgName == null) {
245                pkgNameLen = 0;
246            }
247            else {
248                pkgNameLen = pkgName.length();
249            }
250
251            for (String illegal : classes) {
252                if (isSamePackage(className, pkgNameLen, illegal)
253                        || isStandardClass(className, illegal)) {
254                    fullClassName = illegal;
255                }
256                else {
257                    fullClassName = checkImportStatements(className);
258                }
259
260                if (fullClassName != null) {
261                    break;
262                }
263            }
264        }
265        return fullClassName;
266    }
267
268    /**
269     * Check import statements.
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     * @param className class name
294     * @param pkgNameLen package name
295     * @param illegal illegal value
296     * @return true if type of the same package
297     */
298    private boolean isSamePackage(String className, int pkgNameLen, String illegal) {
299        // class from same package
300
301        // the top level package (pkgName == null) is covered by the
302        // "illegalInstances.contains(className)" check above
303
304        // the test is the "no garbage" version of
305        // illegal.equals(pkgName + "." + className)
306        return pkgName != null
307                && className.length() == illegal.length() - pkgNameLen - 1
308                && illegal.charAt(pkgNameLen) == '.'
309                && illegal.endsWith(className)
310                && illegal.startsWith(pkgName);
311    }
312
313    /**
314     * Is Standard Class.
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     * @param names a comma separate list of class names
343     */
344    public void setClasses(String... names) {
345        classes = Arrays.stream(names).collect(Collectors.toSet());
346    }
347
348}