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