| //////////////////////////////////////////////////////////////////////////////// |
| // checkstyle: Checks Java source code for adherence to a set of rules. |
| // Copyright (C) 2001-2017 the original author or authors. |
| // |
| // This library is free software; you can redistribute it and/or |
| // modify it under the terms of the GNU Lesser General Public |
| // License as published by the Free Software Foundation; either |
| // version 2.1 of the License, or (at your option) any later version. |
| // |
| // This library is distributed in the hope that it will be useful, |
| // but WITHOUT ANY WARRANTY; without even the implied warranty of |
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| // Lesser General Public License for more details. |
| // |
| // You should have received a copy of the GNU Lesser General Public |
| // License along with this library; if not, write to the Free Software |
| // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| package com.puppycrawl.tools.checkstyle.checks.coding; |
| |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| |
| import antlr.collections.AST; |
| import com.puppycrawl.tools.checkstyle.FileStatefulCheck; |
| import com.puppycrawl.tools.checkstyle.api.AbstractCheck; |
| import com.puppycrawl.tools.checkstyle.api.DetailAST; |
| import com.puppycrawl.tools.checkstyle.api.FullIdent; |
| import com.puppycrawl.tools.checkstyle.api.TokenTypes; |
| import com.puppycrawl.tools.checkstyle.utils.CommonUtils; |
| |
| /** |
| * <p> |
| * Checks for illegal instantiations where a factory method is preferred. |
| * </p> |
| * <p> |
| * Rationale: Depending on the project, for some classes it might be |
| * preferable to create instances through factory methods rather than |
| * calling the constructor. |
| * </p> |
| * <p> |
| * A simple example is the java.lang.Boolean class, to save memory and CPU |
| * cycles it is preferable to use the predefined constants TRUE and FALSE. |
| * Constructor invocations should be replaced by calls to Boolean.valueOf(). |
| * </p> |
| * <p> |
| * Some extremely performance sensitive projects may require the use of factory |
| * methods for other classes as well, to enforce the usage of number caches or |
| * object pools. |
| * </p> |
| * <p> |
| * Limitations: It is currently not possible to specify array classes. |
| * </p> |
| * <p> |
| * An example of how to configure the check is: |
| * </p> |
| * <pre> |
| * <module name="IllegalInstantiation"/> |
| * </pre> |
| * @author lkuehne |
| */ |
| @FileStatefulCheck |
| public class IllegalInstantiationCheck |
| extends AbstractCheck { |
| |
| /** |
| * A key is pointing to the warning message text in "messages.properties" |
| * file. |
| */ |
| public static final String MSG_KEY = "instantiation.avoid"; |
| |
| /** {@link java.lang} package as string. */ |
| private static final String JAVA_LANG = "java.lang."; |
| |
| /** The imports for the file. */ |
| private final Set<FullIdent> imports = new HashSet<>(); |
| |
| /** The class names defined in the file. */ |
| private final Set<String> classNames = new HashSet<>(); |
| |
| /** The instantiations in the file. */ |
| private final Set<DetailAST> instantiations = new HashSet<>(); |
| |
| /** Set of fully qualified class names. E.g. "java.lang.Boolean" */ |
| private Set<String> illegalClasses = new HashSet<>(); |
| |
| /** Name of the package. */ |
| private String pkgName; |
| |
| @Override |
| public int[] getDefaultTokens() { |
| return getAcceptableTokens(); |
| } |
| |
| @Override |
| public int[] getAcceptableTokens() { |
| return new int[] { |
| TokenTypes.IMPORT, |
| TokenTypes.LITERAL_NEW, |
| TokenTypes.PACKAGE_DEF, |
| TokenTypes.CLASS_DEF, |
| }; |
| } |
| |
| @Override |
| public int[] getRequiredTokens() { |
| return new int[] { |
| TokenTypes.IMPORT, |
| TokenTypes.LITERAL_NEW, |
| TokenTypes.PACKAGE_DEF, |
| }; |
| } |
| |
| @Override |
| public void beginTree(DetailAST rootAST) { |
| pkgName = null; |
| imports.clear(); |
| instantiations.clear(); |
| classNames.clear(); |
| } |
| |
| @Override |
| public void visitToken(DetailAST ast) { |
| switch (ast.getType()) { |
| case TokenTypes.LITERAL_NEW: |
| processLiteralNew(ast); |
| break; |
| case TokenTypes.PACKAGE_DEF: |
| processPackageDef(ast); |
| break; |
| case TokenTypes.IMPORT: |
| processImport(ast); |
| break; |
| case TokenTypes.CLASS_DEF: |
| processClassDef(ast); |
| break; |
| default: |
| throw new IllegalArgumentException("Unknown type " + ast); |
| } |
| } |
| |
| @Override |
| public void finishTree(DetailAST rootAST) { |
| instantiations.forEach(this::postProcessLiteralNew); |
| } |
| |
| /** |
| * Collects classes defined in the source file. Required |
| * to avoid false alarms for local vs. java.lang classes. |
| * |
| * @param ast the class def token. |
| */ |
| private void processClassDef(DetailAST ast) { |
| final DetailAST identToken = ast.findFirstToken(TokenTypes.IDENT); |
| final String className = identToken.getText(); |
| classNames.add(className); |
| } |
| |
| /** |
| * Perform processing for an import token. |
| * @param ast the import token |
| */ |
| private void processImport(DetailAST ast) { |
| final FullIdent name = FullIdent.createFullIdentBelow(ast); |
| // Note: different from UnusedImportsCheck.processImport(), |
| // '.*' imports are also added here |
| imports.add(name); |
| } |
| |
| /** |
| * Perform processing for an package token. |
| * @param ast the package token |
| */ |
| private void processPackageDef(DetailAST ast) { |
| final DetailAST packageNameAST = ast.getLastChild() |
| .getPreviousSibling(); |
| final FullIdent packageIdent = |
| FullIdent.createFullIdent(packageNameAST); |
| pkgName = packageIdent.getText(); |
| } |
| |
| /** |
| * Collects a "new" token. |
| * @param ast the "new" token |
| */ |
| private void processLiteralNew(DetailAST ast) { |
| if (ast.getParent().getType() != TokenTypes.METHOD_REF) { |
| instantiations.add(ast); |
| } |
| } |
| |
| /** |
| * Processes one of the collected "new" tokens when walking tree |
| * has finished. |
| * @param newTokenAst the "new" token. |
| */ |
| private void postProcessLiteralNew(DetailAST newTokenAst) { |
| final DetailAST typeNameAst = newTokenAst.getFirstChild(); |
| final AST nameSibling = typeNameAst.getNextSibling(); |
| if (nameSibling.getType() != TokenTypes.ARRAY_DECLARATOR) { |
| // ast != "new Boolean[]" |
| final FullIdent typeIdent = FullIdent.createFullIdent(typeNameAst); |
| final String typeName = typeIdent.getText(); |
| final String fqClassName = getIllegalInstantiation(typeName); |
| if (fqClassName != null) { |
| final int lineNo = newTokenAst.getLineNo(); |
| final int colNo = newTokenAst.getColumnNo(); |
| log(lineNo, colNo, MSG_KEY, fqClassName); |
| } |
| } |
| } |
| |
| /** |
| * Checks illegal instantiations. |
| * @param className instantiated class, may or may not be qualified |
| * @return the fully qualified class name of className |
| * or null if instantiation of className is OK |
| */ |
| private String getIllegalInstantiation(String className) { |
| String fullClassName = null; |
| |
| if (illegalClasses.contains(className)) { |
| fullClassName = className; |
| } |
| else { |
| final int pkgNameLen; |
| |
| if (pkgName == null) { |
| pkgNameLen = 0; |
| } |
| else { |
| pkgNameLen = pkgName.length(); |
| } |
| |
| for (String illegal : illegalClasses) { |
| if (isStandardClass(className, illegal) |
| || isSamePackage(className, pkgNameLen, illegal)) { |
| fullClassName = illegal; |
| } |
| else { |
| fullClassName = checkImportStatements(className); |
| } |
| |
| if (fullClassName != null) { |
| break; |
| } |
| } |
| } |
| return fullClassName; |
| } |
| |
| /** |
| * Check import statements. |
| * @param className name of the class |
| * @return value of illegal instantiated type |
| */ |
| private String checkImportStatements(String className) { |
| String illegalType = null; |
| // import statements |
| for (FullIdent importLineText : imports) { |
| String importArg = importLineText.getText(); |
| if (importArg.endsWith(".*")) { |
| importArg = importArg.substring(0, importArg.length() - 1) |
| + className; |
| } |
| if (CommonUtils.baseClassName(importArg).equals(className) |
| && illegalClasses.contains(importArg)) { |
| illegalType = importArg; |
| break; |
| } |
| } |
| return illegalType; |
| } |
| |
| /** |
| * Check that type is of the same package. |
| * @param className class name |
| * @param pkgNameLen package name |
| * @param illegal illegal value |
| * @return true if type of the same package |
| */ |
| private boolean isSamePackage(String className, int pkgNameLen, String illegal) { |
| // class from same package |
| |
| // the top level package (pkgName == null) is covered by the |
| // "illegalInstances.contains(className)" check above |
| |
| // the test is the "no garbage" version of |
| // illegal.equals(pkgName + "." + className) |
| return pkgName != null |
| && className.length() == illegal.length() - pkgNameLen - 1 |
| && illegal.charAt(pkgNameLen) == '.' |
| && illegal.endsWith(className) |
| && illegal.startsWith(pkgName); |
| } |
| |
| /** |
| * Is class of the same package. |
| * @param className class name |
| * @return true if same package class |
| */ |
| private boolean isSamePackage(String className) { |
| boolean isSamePackage = false; |
| try { |
| final ClassLoader classLoader = getClassLoader(); |
| if (classLoader != null) { |
| final String fqName = pkgName + "." + className; |
| classLoader.loadClass(fqName); |
| // no ClassNotFoundException, fqName is a known class |
| isSamePackage = true; |
| } |
| } |
| catch (final ClassNotFoundException ignored) { |
| // not a class from the same package |
| isSamePackage = false; |
| } |
| return isSamePackage; |
| } |
| |
| /** |
| * Is Standard Class. |
| * @param className class name |
| * @param illegal illegal value |
| * @return true if type is standard |
| */ |
| private boolean isStandardClass(String className, String illegal) { |
| boolean isStandardCalss = false; |
| // class from java.lang |
| if (illegal.length() - JAVA_LANG.length() == className.length() |
| && illegal.endsWith(className) |
| && illegal.startsWith(JAVA_LANG)) { |
| // java.lang needs no import, but a class without import might |
| // also come from the same file or be in the same package. |
| // E.g. if a class defines an inner class "Boolean", |
| // the expression "new Boolean()" refers to that class, |
| // not to java.lang.Boolean |
| |
| final boolean isSameFile = classNames.contains(className); |
| final boolean isSamePackage = isSamePackage(className); |
| |
| if (!isSameFile && !isSamePackage) { |
| isStandardCalss = true; |
| } |
| } |
| return isStandardCalss; |
| } |
| |
| /** |
| * Sets the classes that are illegal to instantiate. |
| * @param names a comma separate list of class names |
| */ |
| public void setClasses(String... names) { |
| illegalClasses = Arrays.stream(names).collect(Collectors.toSet()); |
| } |
| } |