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