001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2014  Oliver Burn
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////////////////////////////////////////////////////////////////////////////////
019package com.puppycrawl.tools.checkstyle.checks.coding;
020
021import antlr.collections.AST;
022import com.google.common.collect.Sets;
023import com.puppycrawl.tools.checkstyle.api.Check;
024import com.puppycrawl.tools.checkstyle.api.DetailAST;
025import com.puppycrawl.tools.checkstyle.api.FullIdent;
026import com.puppycrawl.tools.checkstyle.api.TokenTypes;
027import com.puppycrawl.tools.checkstyle.api.Utils;
028import java.util.Set;
029import java.util.StringTokenizer;
030
031// TODO: Clean up potential duplicate code here and in UnusedImportsCheck
032/**
033 * <p>
034 * Checks for illegal instantiations where a factory method is preferred.
035 * </p>
036 * <p>
037 * Rationale: Depending on the project, for some classes it might be
038 * preferable to create instances through factory methods rather than
039 * calling the constructor.
040 * </p>
041 * <p>
042 * A simple example is the java.lang.Boolean class, to save memory and CPU
043 * cycles it is preferable to use the predeifined constants TRUE and FALSE.
044 * Constructor invocations should be replaced by calls to Boolean.valueOf().
045 * </p>
046 * <p>
047 * Some extremely performance sensitive projects may require the use of factory
048 * methods for other classes as well, to enforce the usage of number caches or
049 * object pools.
050 * </p>
051 * <p>
052 * Limitations: It is currently not possible to specify array classes.
053 * </p>
054 * <p>
055 * An example of how to configure the check is:
056 * </p>
057 * <pre>
058 * &lt;module name="IllegalInstantiation"/&gt;
059 * </pre>
060 * @author lkuehne
061 */
062public class IllegalInstantiationCheck
063    extends Check
064{
065    /** Set of fully qualified classnames. E.g. "java.lang.Boolean" */
066    private final Set<String> illegalClasses = Sets.newHashSet();
067
068    /** name of the package */
069    private String pkgName;
070
071    /** the imports for the file */
072    private final Set<FullIdent> imports = Sets.newHashSet();
073
074    /** the class names defined in the file */
075    private final Set<String> classNames = Sets.newHashSet();
076
077    /** the instantiations in the file */
078    private final Set<DetailAST> instantiations = Sets.newHashSet();
079
080    @Override
081    public int[] getDefaultTokens()
082    {
083        return new int[] {
084            TokenTypes.IMPORT,
085            TokenTypes.LITERAL_NEW,
086            TokenTypes.PACKAGE_DEF,
087            TokenTypes.CLASS_DEF,
088        };
089    }
090
091    @Override
092    public int[] getAcceptableTokens()
093    {
094        // Return an empty array to not allow user to change configuration.
095        return new int[] {};
096    }
097
098    @Override
099    public int[] getRequiredTokens()
100    {
101        return new int[] {
102            TokenTypes.IMPORT,
103            TokenTypes.LITERAL_NEW,
104            TokenTypes.PACKAGE_DEF,
105        };
106    }
107
108    @Override
109    public void beginTree(DetailAST rootAST)
110    {
111        super.beginTree(rootAST);
112        pkgName = null;
113        imports.clear();
114        instantiations.clear();
115        classNames.clear();
116    }
117
118    @Override
119    public void visitToken(DetailAST ast)
120    {
121        switch (ast.getType()) {
122            case TokenTypes.LITERAL_NEW:
123                processLiteralNew(ast);
124                break;
125            case TokenTypes.PACKAGE_DEF:
126                processPackageDef(ast);
127                break;
128            case TokenTypes.IMPORT:
129                processImport(ast);
130                break;
131            case TokenTypes.CLASS_DEF:
132                processClassDef(ast);
133                break;
134            default:
135                throw new IllegalArgumentException("Unknown type " + ast);
136        }
137    }
138
139    @Override
140    public void finishTree(DetailAST rootAST)
141    {
142        for (DetailAST literalNewAST : instantiations) {
143            postprocessLiteralNew(literalNewAST);
144        }
145    }
146
147    /**
148     * Collects classes defined in the source file. Required
149     * to avoid false alarms for local vs. java.lang classes.
150     *
151     * @param ast the classdef token.
152     */
153    private void processClassDef(DetailAST ast)
154    {
155        final DetailAST identToken = ast.findFirstToken(TokenTypes.IDENT);
156        final String className = identToken.getText();
157        classNames.add(className);
158    }
159
160    /**
161     * Perform processing for an import token
162     * @param ast the import token
163     */
164    private void processImport(DetailAST ast)
165    {
166        final FullIdent name = FullIdent.createFullIdentBelow(ast);
167        if (name != null) {
168            // Note: different from UnusedImportsCheck.processImport(),
169            // '.*' imports are also added here
170            imports.add(name);
171        }
172    }
173
174    /**
175     * Perform processing for an package token
176     * @param ast the package token
177     */
178    private void processPackageDef(DetailAST ast)
179    {
180        final DetailAST packageNameAST = ast.getLastChild()
181                .getPreviousSibling();
182        final FullIdent packageIdent =
183                FullIdent.createFullIdent(packageNameAST);
184        pkgName = packageIdent.getText();
185    }
186
187    /**
188     * Collects a "new" token.
189     * @param ast the "new" token
190     */
191    private void processLiteralNew(DetailAST ast)
192    {
193        if (ast.getParent().getType() == TokenTypes.METHOD_REF) {
194            return;
195        }
196        instantiations.add(ast);
197    }
198
199    /**
200     * Processes one of the collected "new" tokens when treewalking
201     * has finished.
202     * @param ast the "new" token.
203     */
204    private void postprocessLiteralNew(DetailAST ast)
205    {
206        final DetailAST typeNameAST = ast.getFirstChild();
207        final AST nameSibling = typeNameAST.getNextSibling();
208        if ((nameSibling != null)
209                && (nameSibling.getType() == TokenTypes.ARRAY_DECLARATOR))
210        {
211            // ast == "new Boolean[]"
212            return;
213        }
214
215        final FullIdent typeIdent = FullIdent.createFullIdent(typeNameAST);
216        final String typeName = typeIdent.getText();
217        final int lineNo = ast.getLineNo();
218        final int colNo = ast.getColumnNo();
219        final String fqClassName = getIllegalInstantiation(typeName);
220        if (fqClassName != null) {
221            log(lineNo, colNo, "instantiation.avoid", fqClassName);
222        }
223    }
224
225    /**
226     * Checks illegal instantiations.
227     * @param className instantiated class, may or may not be qualified
228     * @return the fully qualified class name of className
229     * or null if instantiation of className is OK
230     */
231    private String getIllegalInstantiation(String className)
232    {
233        final String javlang = "java.lang.";
234
235        if (illegalClasses.contains(className)) {
236            return className;
237        }
238
239        final int clsNameLen = className.length();
240        final int pkgNameLen = (pkgName == null) ? 0 : pkgName.length();
241
242        for (String illegal : illegalClasses) {
243            final int illegalLen = illegal.length();
244
245            // class from java.lang
246            if (((illegalLen - javlang.length()) == clsNameLen)
247                && illegal.endsWith(className)
248                && illegal.startsWith(javlang))
249            {
250                // java.lang needs no import, but a class without import might
251                // also come from the same file or be in the same package.
252                // E.g. if a class defines an inner class "Boolean",
253                // the expression "new Boolean()" refers to that class,
254                // not to java.lang.Boolean
255
256                final boolean isSameFile = classNames.contains(className);
257
258                boolean isSamePackage = false;
259                try {
260                    final ClassLoader classLoader = getClassLoader();
261                    if (classLoader != null) {
262                        final String fqName = pkgName + "." + className;
263                        classLoader.loadClass(fqName);
264                        // no ClassNotFoundException, fqName is a known class
265                        isSamePackage = true;
266                    }
267                }
268                catch (final ClassNotFoundException ex) {
269                    // not a class from the same package
270                    isSamePackage = false;
271                }
272
273                if (!(isSameFile || isSamePackage)) {
274                    return illegal;
275                }
276            }
277
278            // class from same package
279
280            // the toplevel package (pkgName == null) is covered by the
281            // "illegalInsts.contains(className)" check above
282
283            // the test is the "no garbage" version of
284            // illegal.equals(pkgName + "." + className)
285            if ((pkgName != null)
286                && (clsNameLen == illegalLen - pkgNameLen - 1)
287                && (illegal.charAt(pkgNameLen) == '.')
288                && illegal.endsWith(className)
289                && illegal.startsWith(pkgName))
290            {
291                return illegal;
292            }
293            // import statements
294            for (FullIdent importLineText : imports) {
295                final String importArg = importLineText.getText();
296                if (importArg.endsWith(".*")) {
297                    final String fqClass =
298                        importArg.substring(0, importArg.length() - 1)
299                        + className;
300                    // assume that illegalInsts only contain existing classes
301                    // or else we might create a false alarm here
302                    if (illegalClasses.contains(fqClass)) {
303                        return fqClass;
304                    }
305                }
306                else {
307                    if (Utils.baseClassname(importArg).equals(className)
308                        && illegalClasses.contains(importArg))
309                    {
310                        return importArg;
311                    }
312                }
313            }
314        }
315        return null;
316    }
317
318    /**
319     * Sets the classes that are illegal to instantiate.
320     * @param classNames a comma seperate list of class names
321     */
322    public void setClasses(String classNames)
323    {
324        illegalClasses.clear();
325        final StringTokenizer tok = new StringTokenizer(classNames, ",");
326        while (tok.hasMoreTokens()) {
327            illegalClasses.add(tok.nextToken());
328        }
329    }
330}