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