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