001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2015 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.Set;
023import java.util.StringTokenizer;
024
025import antlr.collections.AST;
026
027import com.google.common.collect.Sets;
028import com.puppycrawl.tools.checkstyle.api.Check;
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.CommonUtils;
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 java.lang.Boolean class, to save memory and CPU
045 * cycles it is preferable to use the predefined constants TRUE and FALSE.
046 * Constructor invocations should be replaced by calls to Boolean.valueOf().
047 * </p>
048 * <p>
049 * Some extremely performance sensitive projects may require the use of factory
050 * methods for other classes as well, to enforce the usage of number caches or
051 * object pools.
052 * </p>
053 * <p>
054 * Limitations: It is currently not possible to specify array classes.
055 * </p>
056 * <p>
057 * An example of how to configure the check is:
058 * </p>
059 * <pre>
060 * &lt;module name="IllegalInstantiation"/&gt;
061 * </pre>
062 * @author lkuehne
063 */
064public class IllegalInstantiationCheck
065    extends Check {
066
067    /**
068     * A key is pointing to the warning message text in "messages.properties"
069     * file.
070     */
071    public static final String MSG_KEY = "instantiation.avoid";
072
073    /** {@link java.lang} package as string */
074    private static final String JAVA_LANG = "java.lang.";
075
076    /** Set of fully qualified class names. E.g. "java.lang.Boolean" */
077    private final Set<String> illegalClasses = Sets.newHashSet();
078
079    /** Name of the package. */
080    private String pkgName;
081
082    /** The imports for the file. */
083    private final Set<FullIdent> imports = Sets.newHashSet();
084
085    /** The class names defined in the file. */
086    private final Set<String> classNames = Sets.newHashSet();
087
088    /** The instantiations in the file. */
089    private final Set<DetailAST> instantiations = Sets.newHashSet();
090
091    @Override
092    public int[] getDefaultTokens() {
093        return getAcceptableTokens();
094    }
095
096    @Override
097    public int[] getAcceptableTokens() {
098        return new int[] {
099            TokenTypes.IMPORT,
100            TokenTypes.LITERAL_NEW,
101            TokenTypes.PACKAGE_DEF,
102            TokenTypes.CLASS_DEF,
103        };
104    }
105
106    @Override
107    public int[] getRequiredTokens() {
108        return new int[] {
109            TokenTypes.IMPORT,
110            TokenTypes.LITERAL_NEW,
111            TokenTypes.PACKAGE_DEF,
112        };
113    }
114
115    @Override
116    public void beginTree(DetailAST rootAST) {
117        super.beginTree(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        for (DetailAST literalNewAST : instantiations) {
147            postProcessLiteralNew(literalNewAST);
148        }
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            return;
207        }
208
209        final FullIdent typeIdent = FullIdent.createFullIdent(typeNameAst);
210        final String typeName = typeIdent.getText();
211        final int lineNo = newTokenAst.getLineNo();
212        final int colNo = newTokenAst.getColumnNo();
213        final String fqClassName = getIllegalInstantiation(typeName);
214        if (fqClassName != null) {
215            log(lineNo, colNo, MSG_KEY, fqClassName);
216        }
217    }
218
219    /**
220     * Checks illegal instantiations.
221     * @param className instantiated class, may or may not be qualified
222     * @return the fully qualified class name of className
223     *     or null if instantiation of className is OK
224     */
225    private String getIllegalInstantiation(String className) {
226        String fullClassName = null;
227
228        if (illegalClasses.contains(className)) {
229            fullClassName = className;
230        }
231        else {
232            final int pkgNameLen;
233
234            if (pkgName == null) {
235                pkgNameLen = 0;
236            }
237            else {
238                pkgNameLen = pkgName.length();
239            }
240
241            for (String illegal : illegalClasses) {
242                if (isStandardClass(className, illegal)
243                        || isSamePackage(className, pkgNameLen, illegal)) {
244                    fullClassName = illegal;
245                }
246                else {
247                    fullClassName = checkImportStatements(className);
248                }
249
250                if (fullClassName != null) {
251                    break;
252                }
253            }
254        }
255        return fullClassName;
256    }
257
258    /**
259     * Check import statements.
260     * @param className name of the class
261     * @return value of illegal instantiated type
262     * @noinspection StringContatenationInLoop
263     */
264    private String checkImportStatements(String className) {
265        String illegalType = null;
266        // import statements
267        for (FullIdent importLineText : imports) {
268            String importArg = importLineText.getText();
269            if (importArg.endsWith(".*")) {
270                importArg = importArg.substring(0, importArg.length() - 1)
271                        + className;
272            }
273            if (CommonUtils.baseClassName(importArg).equals(className)
274                    && illegalClasses.contains(importArg)) {
275                illegalType = importArg;
276                break;
277            }
278        }
279        return illegalType;
280    }
281
282    /**
283     * Check that type is of the same package.
284     * @param className class name
285     * @param pkgNameLen package name
286     * @param illegal illegal value
287     * @return true if type of the same package
288     */
289    private boolean isSamePackage(String className, int pkgNameLen, String illegal) {
290        // class from same package
291
292        // the top level package (pkgName == null) is covered by the
293        // "illegalInstances.contains(className)" check above
294
295        // the test is the "no garbage" version of
296        // illegal.equals(pkgName + "." + className)
297        return pkgName != null
298                && className.length() == illegal.length() - pkgNameLen - 1
299                && illegal.charAt(pkgNameLen) == '.'
300                && illegal.endsWith(className)
301                && illegal.startsWith(pkgName);
302    }
303
304    /**
305     * Is class of the same package.
306     * @param className class name
307     * @return true if same package class
308     */
309    private boolean isSamePackage(String className) {
310        boolean isSamePackage = false;
311        try {
312            final ClassLoader classLoader = getClassLoader();
313            if (classLoader != null) {
314                final String fqName = pkgName + "." + className;
315                classLoader.loadClass(fqName);
316                // no ClassNotFoundException, fqName is a known class
317                isSamePackage = true;
318            }
319        }
320        catch (final ClassNotFoundException ignored) {
321            // not a class from the same package
322            isSamePackage = false;
323        }
324        return isSamePackage;
325    }
326
327    /**
328     * Is Standard Class.
329     * @param className class name
330     * @param illegal illegal value
331     * @return true if type is standard
332     */
333    private boolean isStandardClass(String className, String illegal) {
334        // class from java.lang
335        if (illegal.length() - JAVA_LANG.length() == className.length()
336            && illegal.endsWith(className)
337            && illegal.startsWith(JAVA_LANG)) {
338            // java.lang needs no import, but a class without import might
339            // also come from the same file or be in the same package.
340            // E.g. if a class defines an inner class "Boolean",
341            // the expression "new Boolean()" refers to that class,
342            // not to java.lang.Boolean
343
344            final boolean isSameFile = classNames.contains(className);
345            final boolean isSamePackage = isSamePackage(className);
346
347            if (!(isSameFile || isSamePackage)) {
348                return true;
349            }
350        }
351        return false;
352    }
353
354    /**
355     * Sets the classes that are illegal to instantiate.
356     * @param names a comma separate list of class names
357     */
358    public void setClasses(String names) {
359        illegalClasses.clear();
360        final StringTokenizer tok = new StringTokenizer(names, ",");
361        while (tok.hasMoreTokens()) {
362            illegalClasses.add(tok.nextToken());
363        }
364    }
365}