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