IllegalInstantiationCheck.java
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2021 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 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.CommonUtil;
/**
* <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 {@code java.lang.Boolean} class.
* For performance reasons, it is preferable to use the predefined constants
* {@code TRUE} and {@code FALSE}.
* Constructor invocations should be replaced by calls to {@code 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>
* There is a limitation that it is currently not possible to specify array classes.
* </p>
* <ul>
* <li>
* Property {@code classes} - Specify fully qualified class names that should not be instantiated.
* Type is {@code java.lang.String[]}.
* Default value is {@code ""}.
* </li>
* <li>
* Property {@code tokens} - tokens to check
* Type is {@code java.lang.String[]}.
* Validation type is {@code tokenSet}.
* Default value is:
* <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CLASS_DEF">
* CLASS_DEF</a>.
* </li>
* </ul>
* <p>
* To configure the check:
* </p>
* <pre>
* <module name="IllegalInstantiation"/>
* </pre>
* <p>Example:</p>
* <pre>
* public class MyTest {
* public class Boolean {
* boolean a;
*
* public Boolean (boolean a) { this.a = a; }
* }
*
* public void myTest (boolean a, int b) {
* Boolean c = new Boolean(a); // OK
* java.lang.Boolean d = new java.lang.Boolean(a); // OK
*
* Integer e = new Integer(b); // OK
* Integer f = Integer.valueOf(b); // OK
* }
* }
* </pre>
* <p>
* To configure the check to find instantiations of {@code java.lang.Boolean}
* and {@code java.lang.Integer}. NOTE: Even if property {@code tokens}
* is completely removed from the following configuration, Checkstyle will produce
* the same results for violation. This is because if property {@code tokens} is not
* defined in the configuration, Checkstyle will supply it with list of default tokens
* {@code CLASS_DEF, LITERAL_NEW, PACKAGE_DEF, IMPORT} for this check. The property is
* defined in this example only to provide clarity:
* </p>
* <pre>
* <module name="IllegalInstantiation">
* <property name="classes" value="java.lang.Boolean,
* java.lang.Integer"/>
* <property name="tokens" value="CLASS_DEF, LITERAL_NEW,
* PACKAGE_DEF, IMPORT"/>
* </module>
* </pre>
* <p>Example:</p>
* <pre>
* public class MyTest {
* public class Boolean {
* boolean a;
*
* public Boolean (boolean a) { this.a = a; }
* }
*
* public void myTest (boolean a, int b) {
* Boolean c = new Boolean(a); // OK
* java.lang.Boolean d = new java.lang.Boolean(a); // violation, instantiation of
* // java.lang.Boolean should be avoided
*
* Integer e = new Integer(b); // violation, instantiation of
* // java.lang.Integer should be avoided
* Integer f = Integer.valueOf(b); // OK
* }
* }
* </pre>
* <p>
* To configure the check to allow violations for local classes vs classes
* defined in the check, for example {@code java.lang.Boolean}, property
* {@code tokens} must be defined to not mention {@code CLASS_DEF}, so its
* value should be {@code LITERAL_NEW, PACKAGE_DEF, IMPORT}:
* </p>
* <pre>
* <module name="IllegalInstantiation">
* <property name="classes" value="java.lang.Boolean,
* java.lang.Integer"/>
* <property name="tokens" value="LITERAL_NEW, PACKAGE_DEF,
* IMPORT"/>
* </module>
* </pre>
* <p>Example:</p>
* <pre>
* public class MyTest {
* public class Boolean {
* boolean a;
*
* public Boolean (boolean a) { this.a = a; }
* }
*
* public void myTest (boolean a, int b) {
* Boolean c = new Boolean(a); // violation, instantiation of
* // java.lang.Boolean should be avoided
* java.lang.Boolean d = new java.lang.Boolean(a); // violation, instantiation of
* // java.lang.Boolean should be avoided
*
* Integer e = new Integer(b); // violation, instantiation of
* // java.lang.Integer should be avoided
* Integer f = Integer.valueOf(b); // OK
* }
* }
* </pre>
* <p>
* Finally, there is a limitation that it is currently not possible to specify array classes:
* </p>
* <pre>
* <module name="IllegalInstantiation">
* <property name="classes" value="java.lang.Boolean[],
* Boolean[], java.lang.Integer[], Integer[]"/>
* </module>
* </pre>
* <p>Example:</p>
* <pre>
* public class MyTest {
* public void myTest () {
* Boolean[] newBoolArray = new Boolean[]{true,true,false}; // OK
* Integer[] newIntArray = new Integer[]{1,2,3}; // OK
* }
* }
* </pre>
* <p>
* Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
* </p>
* <p>
* Violation Message Keys:
* </p>
* <ul>
* <li>
* {@code instantiation.avoid}
* </li>
* </ul>
*
* @since 3.0
*/
@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<>();
/** Specify fully qualified class names that should not be instantiated. */
private Set<String> classes = 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 DetailAST 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) {
log(newTokenAst, 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 (classes.contains(className)) {
fullClassName = className;
}
else {
final int pkgNameLen;
if (pkgName == null) {
pkgNameLen = 0;
}
else {
pkgNameLen = pkgName.length();
}
for (String illegal : classes) {
if (isSamePackage(className, pkgNameLen, illegal)
|| isStandardClass(className, 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 (CommonUtil.baseClassName(importArg).equals(className)
&& classes.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 Standard Class.
*
* @param className class name
* @param illegal illegal value
* @return true if type is standard
*/
private boolean isStandardClass(String className, String illegal) {
boolean isStandardClass = 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);
if (!isSameFile) {
isStandardClass = true;
}
}
return isStandardClass;
}
/**
* Setter to specify fully qualified class names that should not be instantiated.
*
* @param names a comma separate list of class names
*/
public void setClasses(String... names) {
classes = Arrays.stream(names).collect(Collectors.toSet());
}
}