001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2022 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:
074 * </p>
075 * <pre>
076 * &lt;module name=&quot;IllegalInstantiation&quot;/&gt;
077 * </pre>
078 * <p>Example:</p>
079 * <pre>
080 * public class MyTest {
081 *   public class Boolean {
082 *     boolean a;
083 *
084 *     public Boolean (boolean a) { this.a = a; }
085 *   }
086 *
087 *   public void myTest (boolean a, int b) {
088 *     Boolean c = new Boolean(a); // OK
089 *     java.lang.Boolean d = new java.lang.Boolean(a); // OK
090 *
091 *     Integer e = new Integer(b); // OK
092 *     Integer f = Integer.valueOf(b); // OK
093 *   }
094 * }
095 * </pre>
096 * <p>
097 * To configure the check to find instantiations of {@code java.lang.Boolean}
098 * and {@code java.lang.Integer}. NOTE: Even if property {@code tokens}
099 * is completely removed from the following configuration, Checkstyle will produce
100 * the same results for violation. This is because if property {@code tokens} is not
101 * defined in the configuration, Checkstyle will supply it with list of default tokens
102 * {@code CLASS_DEF, LITERAL_NEW, PACKAGE_DEF, IMPORT} for this check. The property is
103 * defined in this example only to provide clarity:
104 * </p>
105 * <pre>
106 * &lt;module name=&quot;IllegalInstantiation&quot;&gt;
107 *   &lt;property name=&quot;classes&quot; value=&quot;java.lang.Boolean,
108 *     java.lang.Integer&quot;/&gt;
109 *   &lt;property name=&quot;tokens&quot; value=&quot;CLASS_DEF, LITERAL_NEW,
110 *     PACKAGE_DEF, IMPORT&quot;/&gt;
111 * &lt;/module&gt;
112 * </pre>
113 * <p>Example:</p>
114 * <pre>
115 * public class MyTest {
116 *   public class Boolean {
117 *     boolean a;
118 *
119 *     public Boolean (boolean a) { this.a = a; }
120 *   }
121 *
122 *   public void myTest (boolean a, int b) {
123 *     Boolean c = new Boolean(a); // OK
124 *     java.lang.Boolean d = new java.lang.Boolean(a); // violation, instantiation of
125 *                                                     // java.lang.Boolean should be avoided
126 *
127 *     Integer e = new Integer(b); // violation, instantiation of
128 *                                 // java.lang.Integer should be avoided
129 *     Integer f = Integer.valueOf(b); // OK
130 *   }
131 * }
132 * </pre>
133 * <p>
134 * To configure the check to allow violations for local classes vs classes
135 * defined in the check, for example {@code java.lang.Boolean}, property
136 * {@code tokens} must be defined to not mention {@code CLASS_DEF}, so its
137 * value should be {@code LITERAL_NEW, PACKAGE_DEF, IMPORT}:
138 * </p>
139 * <pre>
140 * &lt;module name=&quot;IllegalInstantiation&quot;&gt;
141 *   &lt;property name=&quot;classes&quot; value=&quot;java.lang.Boolean,
142 *     java.lang.Integer&quot;/&gt;
143 *   &lt;property name=&quot;tokens&quot; value=&quot;LITERAL_NEW, PACKAGE_DEF,
144 *     IMPORT&quot;/&gt;
145 * &lt;/module&gt;
146 * </pre>
147 * <p>Example:</p>
148 * <pre>
149 * public class MyTest {
150 *   public class Boolean {
151 *     boolean a;
152 *
153 *     public Boolean (boolean a) { this.a = a; }
154 *   }
155 *
156 *   public void myTest (boolean a, int b) {
157 *     Boolean c = new Boolean(a); // violation, instantiation of
158 *                                 // java.lang.Boolean should be avoided
159 *     java.lang.Boolean d = new java.lang.Boolean(a); // violation, instantiation of
160 *                                                     // java.lang.Boolean should be avoided
161 *
162 *     Integer e = new Integer(b); // violation, instantiation of
163 *                                 // java.lang.Integer should be avoided
164 *     Integer f = Integer.valueOf(b); // OK
165 *   }
166 * }
167 * </pre>
168 * <p>
169 * Finally, there is a limitation that it is currently not possible to specify array classes:
170 * </p>
171 * <pre>
172 * &lt;module name=&quot;IllegalInstantiation&quot;&gt;
173 *   &lt;property name=&quot;classes&quot; value=&quot;java.lang.Boolean[],
174 *      Boolean[], java.lang.Integer[], Integer[]&quot;/&gt;
175 * &lt;/module&gt;
176 * </pre>
177 * <p>Example:</p>
178 * <pre>
179 * public class MyTest {
180 *   public void myTest () {
181 *     Boolean[] newBoolArray = new Boolean[]{true,true,false}; // OK
182 *     Integer[] newIntArray = new Integer[]{1,2,3}; // OK
183 *   }
184 * }
185 * </pre>
186 * <p>
187 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
188 * </p>
189 * <p>
190 * Violation Message Keys:
191 * </p>
192 * <ul>
193 * <li>
194 * {@code instantiation.avoid}
195 * </li>
196 * </ul>
197 *
198 * @since 3.0
199 */
200@FileStatefulCheck
201public class IllegalInstantiationCheck
202    extends AbstractCheck {
203
204    /**
205     * A key is pointing to the warning message text in "messages.properties"
206     * file.
207     */
208    public static final String MSG_KEY = "instantiation.avoid";
209
210    /** {@link java.lang} package as string. */
211    private static final String JAVA_LANG = "java.lang.";
212
213    /** The imports for the file. */
214    private final Set<FullIdent> imports = new HashSet<>();
215
216    /** The class names defined in the file. */
217    private final Set<String> classNames = new HashSet<>();
218
219    /** The instantiations in the file. */
220    private final Set<DetailAST> instantiations = new HashSet<>();
221
222    /** Specify fully qualified class names that should not be instantiated. */
223    private Set<String> classes = new HashSet<>();
224
225    /** Name of the package. */
226    private String pkgName;
227
228    @Override
229    public int[] getDefaultTokens() {
230        return getAcceptableTokens();
231    }
232
233    @Override
234    public int[] getAcceptableTokens() {
235        return new int[] {
236            TokenTypes.IMPORT,
237            TokenTypes.LITERAL_NEW,
238            TokenTypes.PACKAGE_DEF,
239            TokenTypes.CLASS_DEF,
240        };
241    }
242
243    @Override
244    public int[] getRequiredTokens() {
245        return new int[] {
246            TokenTypes.IMPORT,
247            TokenTypes.LITERAL_NEW,
248            TokenTypes.PACKAGE_DEF,
249        };
250    }
251
252    @Override
253    public void beginTree(DetailAST rootAST) {
254        pkgName = null;
255        imports.clear();
256        instantiations.clear();
257        classNames.clear();
258    }
259
260    @Override
261    public void visitToken(DetailAST ast) {
262        switch (ast.getType()) {
263            case TokenTypes.LITERAL_NEW:
264                processLiteralNew(ast);
265                break;
266            case TokenTypes.PACKAGE_DEF:
267                processPackageDef(ast);
268                break;
269            case TokenTypes.IMPORT:
270                processImport(ast);
271                break;
272            case TokenTypes.CLASS_DEF:
273                processClassDef(ast);
274                break;
275            default:
276                throw new IllegalArgumentException("Unknown type " + ast);
277        }
278    }
279
280    @Override
281    public void finishTree(DetailAST rootAST) {
282        instantiations.forEach(this::postProcessLiteralNew);
283    }
284
285    /**
286     * Collects classes defined in the source file. Required
287     * to avoid false alarms for local vs. java.lang classes.
288     *
289     * @param ast the class def token.
290     */
291    private void processClassDef(DetailAST ast) {
292        final DetailAST identToken = ast.findFirstToken(TokenTypes.IDENT);
293        final String className = identToken.getText();
294        classNames.add(className);
295    }
296
297    /**
298     * Perform processing for an import token.
299     *
300     * @param ast the import token
301     */
302    private void processImport(DetailAST ast) {
303        final FullIdent name = FullIdent.createFullIdentBelow(ast);
304        // Note: different from UnusedImportsCheck.processImport(),
305        // '.*' imports are also added here
306        imports.add(name);
307    }
308
309    /**
310     * Perform processing for an package token.
311     *
312     * @param ast the package token
313     */
314    private void processPackageDef(DetailAST ast) {
315        final DetailAST packageNameAST = ast.getLastChild()
316                .getPreviousSibling();
317        final FullIdent packageIdent =
318                FullIdent.createFullIdent(packageNameAST);
319        pkgName = packageIdent.getText();
320    }
321
322    /**
323     * Collects a "new" token.
324     *
325     * @param ast the "new" token
326     */
327    private void processLiteralNew(DetailAST ast) {
328        if (ast.getParent().getType() != TokenTypes.METHOD_REF) {
329            instantiations.add(ast);
330        }
331    }
332
333    /**
334     * Processes one of the collected "new" tokens when walking tree
335     * has finished.
336     *
337     * @param newTokenAst the "new" token.
338     */
339    private void postProcessLiteralNew(DetailAST newTokenAst) {
340        final DetailAST typeNameAst = newTokenAst.getFirstChild();
341        final DetailAST nameSibling = typeNameAst.getNextSibling();
342        if (nameSibling.getType() != TokenTypes.ARRAY_DECLARATOR) {
343            // ast != "new Boolean[]"
344            final FullIdent typeIdent = FullIdent.createFullIdent(typeNameAst);
345            final String typeName = typeIdent.getText();
346            final String fqClassName = getIllegalInstantiation(typeName);
347            if (fqClassName != null) {
348                log(newTokenAst, MSG_KEY, fqClassName);
349            }
350        }
351    }
352
353    /**
354     * Checks illegal instantiations.
355     *
356     * @param className instantiated class, may or may not be qualified
357     * @return the fully qualified class name of className
358     *     or null if instantiation of className is OK
359     */
360    private String getIllegalInstantiation(String className) {
361        String fullClassName = null;
362
363        if (classes.contains(className)) {
364            fullClassName = className;
365        }
366        else {
367            final int pkgNameLen;
368
369            if (pkgName == null) {
370                pkgNameLen = 0;
371            }
372            else {
373                pkgNameLen = pkgName.length();
374            }
375
376            for (String illegal : classes) {
377                if (isSamePackage(className, pkgNameLen, illegal)
378                        || isStandardClass(className, illegal)) {
379                    fullClassName = illegal;
380                }
381                else {
382                    fullClassName = checkImportStatements(className);
383                }
384
385                if (fullClassName != null) {
386                    break;
387                }
388            }
389        }
390        return fullClassName;
391    }
392
393    /**
394     * Check import statements.
395     *
396     * @param className name of the class
397     * @return value of illegal instantiated type
398     */
399    private String checkImportStatements(String className) {
400        String illegalType = null;
401        // import statements
402        for (FullIdent importLineText : imports) {
403            String importArg = importLineText.getText();
404            if (importArg.endsWith(".*")) {
405                importArg = importArg.substring(0, importArg.length() - 1)
406                        + className;
407            }
408            if (CommonUtil.baseClassName(importArg).equals(className)
409                    && classes.contains(importArg)) {
410                illegalType = importArg;
411                break;
412            }
413        }
414        return illegalType;
415    }
416
417    /**
418     * Check that type is of the same package.
419     *
420     * @param className class name
421     * @param pkgNameLen package name
422     * @param illegal illegal value
423     * @return true if type of the same package
424     */
425    private boolean isSamePackage(String className, int pkgNameLen, String illegal) {
426        // class from same package
427
428        // the top level package (pkgName == null) is covered by the
429        // "illegalInstances.contains(className)" check above
430
431        // the test is the "no garbage" version of
432        // illegal.equals(pkgName + "." + className)
433        return pkgName != null
434                && className.length() == illegal.length() - pkgNameLen - 1
435                && illegal.charAt(pkgNameLen) == '.'
436                && illegal.endsWith(className)
437                && illegal.startsWith(pkgName);
438    }
439
440    /**
441     * Is Standard Class.
442     *
443     * @param className class name
444     * @param illegal illegal value
445     * @return true if type is standard
446     */
447    private boolean isStandardClass(String className, String illegal) {
448        boolean isStandardClass = false;
449        // class from java.lang
450        if (illegal.length() - JAVA_LANG.length() == className.length()
451            && illegal.endsWith(className)
452            && illegal.startsWith(JAVA_LANG)) {
453            // java.lang needs no import, but a class without import might
454            // also come from the same file or be in the same package.
455            // E.g. if a class defines an inner class "Boolean",
456            // the expression "new Boolean()" refers to that class,
457            // not to java.lang.Boolean
458
459            final boolean isSameFile = classNames.contains(className);
460
461            if (!isSameFile) {
462                isStandardClass = true;
463            }
464        }
465        return isStandardClass;
466    }
467
468    /**
469     * Setter to specify fully qualified class names that should not be instantiated.
470     *
471     * @param names a comma separate list of class names
472     */
473    public void setClasses(String... names) {
474        classes = Arrays.stream(names).collect(Collectors.toSet());
475    }
476
477}