////////////////////////////////////////////////////////////////////////////////
// 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;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
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.LocalizedMessage;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
/**
* Abstract class that endeavours to maintain type information for the Java
* file being checked. It provides helper methods for performing type
* information functions.
*
* @author Oliver Burn
* @deprecated Checkstyle is not type aware tool and all Checks derived from this
* class are potentially unstable.
*/
@Deprecated
public abstract class AbstractTypeAwareCheck extends AbstractCheck {
/** Stack of maps for type params. */
private final Deque<Map<String, AbstractClassInfo>> typeParams = new ArrayDeque<>();
/** Imports details. **/
private final Set<String> imports = new HashSet<>();
/** Full identifier for package of the method. **/
private FullIdent packageFullIdent;
/** Name of current class. */
private String currentClassName;
/** {@code ClassResolver} instance for current tree. */
private ClassResolver classResolver;
/**
* Whether to log class loading errors to the checkstyle report
* instead of throwing a RTE.
*
* <p>Logging errors will avoid stopping checkstyle completely
* because of a typo in javadoc. However, with modern IDEs that
* support automated refactoring and generate javadoc this will
* occur rarely, so by default we assume a configuration problem
* in the checkstyle classpath and throw an exception.
*
* <p>This configuration option was triggered by bug 1422462.
*/
private boolean logLoadErrors = true;
/**
* Whether to show class loading errors in the checkstyle report.
* Request ID 1491630
*/
private boolean suppressLoadErrors;
/**
* Called to process an AST when visiting it.
* @param ast the AST to process. Guaranteed to not be PACKAGE_DEF or
* IMPORT tokens.
*/
protected abstract void processAST(DetailAST ast);
/**
* Logs error if unable to load class information.
* Abstract, should be overridden in subclasses.
* @param ident class name for which we can no load class.
*/
protected abstract void logLoadError(Token ident);
/**
* Controls whether to log class loading errors to the checkstyle report
* instead of throwing a RTE.
*
* @param logLoadErrors true if errors should be logged
*/
public final void setLogLoadErrors(boolean logLoadErrors) {
this.logLoadErrors = logLoadErrors;
}
/**
* Controls whether to show class loading errors in the checkstyle report.
*
* @param suppressLoadErrors true if errors shouldn't be shown
*/
public final void setSuppressLoadErrors(boolean suppressLoadErrors) {
this.suppressLoadErrors = suppressLoadErrors;
}
@Override
public final int[] getRequiredTokens() {
return new int[] {
TokenTypes.PACKAGE_DEF,
TokenTypes.IMPORT,
TokenTypes.CLASS_DEF,
TokenTypes.INTERFACE_DEF,
TokenTypes.ENUM_DEF,
};
}
@Override
public void beginTree(DetailAST rootAST) {
packageFullIdent = FullIdent.createFullIdent(null);
imports.clear();
// add java.lang.* since it's always imported
imports.add("java.lang.*");
classResolver = null;
currentClassName = "";
typeParams.clear();
}
@Override
public final void visitToken(DetailAST ast) {
if (ast.getType() == TokenTypes.PACKAGE_DEF) {
processPackage(ast);
}
else if (ast.getType() == TokenTypes.IMPORT) {
processImport(ast);
}
else if (ast.getType() == TokenTypes.CLASS_DEF
|| ast.getType() == TokenTypes.INTERFACE_DEF
|| ast.getType() == TokenTypes.ENUM_DEF) {
processClass(ast);
}
else {
if (ast.getType() == TokenTypes.METHOD_DEF) {
processTypeParams(ast);
}
processAST(ast);
}
}
@Override
public final void leaveToken(DetailAST ast) {
if (ast.getType() == TokenTypes.CLASS_DEF
|| ast.getType() == TokenTypes.INTERFACE_DEF
|| ast.getType() == TokenTypes.ENUM_DEF) {
// perhaps it was inner class
int dotIdx = currentClassName.lastIndexOf('$');
if (dotIdx == -1) {
// perhaps just a class
dotIdx = currentClassName.lastIndexOf('.');
}
if (dotIdx == -1) {
// looks like a topmost class
currentClassName = "";
}
else {
currentClassName = currentClassName.substring(0, dotIdx);
}
typeParams.pop();
}
else if (ast.getType() == TokenTypes.METHOD_DEF) {
typeParams.pop();
}
}
/**
* Is exception is unchecked (subclass of {@code RuntimeException}
* or {@code Error}.
*
* @param exception {@code Class} of exception to check
* @return true if exception is unchecked
* false if exception is checked
*/
protected static boolean isUnchecked(Class<?> exception) {
return isSubclass(exception, RuntimeException.class)
|| isSubclass(exception, Error.class);
}
/**
* Checks if one class is subclass of another.
*
* @param child {@code Class} of class
* which should be child
* @param parent {@code Class} of class
* which should be parent
* @return true if aChild is subclass of aParent
* false otherwise
*/
protected static boolean isSubclass(Class<?> child, Class<?> parent) {
return parent != null && child != null
&& parent.isAssignableFrom(child);
}
/**
* @return {@code ClassResolver} for current tree.
*/
private ClassResolver getClassResolver() {
if (classResolver == null) {
classResolver =
new ClassResolver(getClassLoader(),
packageFullIdent.getText(),
imports);
}
return classResolver;
}
/**
* Attempts to resolve the Class for a specified name.
* @param resolvableClassName name of the class to resolve
* @param className name of surrounding class.
* @return the resolved class or {@code null}
* if unable to resolve the class.
*/
// -@cs[ForbidWildcardAsReturnType] The class is deprecated and will be removed soon.
protected final Class<?> resolveClass(String resolvableClassName,
String className) {
Class<?> clazz;
try {
clazz = getClassResolver().resolve(resolvableClassName, className);
}
catch (final ClassNotFoundException ignored) {
clazz = null;
}
return clazz;
}
/**
* Tries to load class. Logs error if unable.
* @param ident name of class which we try to load.
* @param className name of surrounding class.
* @return {@code Class} for a ident.
*/
// -@cs[ForbidWildcardAsReturnType] The class is deprecated and will be removed soon.
protected final Class<?> tryLoadClass(Token ident, String className) {
final Class<?> clazz = resolveClass(ident.getText(), className);
if (clazz == null) {
logLoadError(ident);
}
return clazz;
}
/**
* Common implementation for logLoadError() method.
* @param lineNo line number of the problem.
* @param columnNo column number of the problem.
* @param msgKey message key to use.
* @param values values to fill the message out.
*/
protected final void logLoadErrorImpl(int lineNo, int columnNo,
String msgKey, Object... values) {
if (!logLoadErrors) {
final LocalizedMessage msg = new LocalizedMessage(lineNo,
columnNo,
getMessageBundle(),
msgKey,
values,
getSeverityLevel(),
getId(),
getClass(),
null);
throw new IllegalStateException(msg.getMessage());
}
if (!suppressLoadErrors) {
log(lineNo, columnNo, msgKey, values);
}
}
/**
* Collects the details of a package.
* @param ast node containing the package details
*/
private void processPackage(DetailAST ast) {
final DetailAST nameAST = ast.getLastChild().getPreviousSibling();
packageFullIdent = FullIdent.createFullIdent(nameAST);
}
/**
* Collects the details of imports.
* @param ast node containing the import details
*/
private void processImport(DetailAST ast) {
final FullIdent name = FullIdent.createFullIdentBelow(ast);
imports.add(name.getText());
}
/**
* Process type params (if any) for given class, enum or method.
* @param ast class, enum or method to process.
*/
private void processTypeParams(DetailAST ast) {
final DetailAST params =
ast.findFirstToken(TokenTypes.TYPE_PARAMETERS);
final Map<String, AbstractClassInfo> paramsMap = new HashMap<>();
typeParams.push(paramsMap);
if (params != null) {
for (DetailAST child = params.getFirstChild();
child != null;
child = child.getNextSibling()) {
if (child.getType() == TokenTypes.TYPE_PARAMETER) {
final DetailAST bounds =
child.findFirstToken(TokenTypes.TYPE_UPPER_BOUNDS);
if (bounds != null) {
final FullIdent name =
FullIdent.createFullIdentBelow(bounds);
final AbstractClassInfo classInfo =
createClassInfo(new Token(name), currentClassName);
final String alias =
child.findFirstToken(TokenTypes.IDENT).getText();
paramsMap.put(alias, classInfo);
}
}
}
}
}
/**
* Processes class definition.
* @param ast class definition to process.
*/
private void processClass(DetailAST ast) {
final DetailAST ident = ast.findFirstToken(TokenTypes.IDENT);
String innerClass = ident.getText();
if (!currentClassName.isEmpty()) {
innerClass = "$" + innerClass;
}
currentClassName += innerClass;
processTypeParams(ast);
}
/**
* Returns current class.
* @return name of current class.
*/
protected final String getCurrentClassName() {
return currentClassName;
}
/**
* Creates class info for given name.
* @param name name of type.
* @param surroundingClass name of surrounding class.
* @return class info for given name.
*/
protected final AbstractClassInfo createClassInfo(final Token name,
final String surroundingClass) {
final AbstractClassInfo result;
final AbstractClassInfo classInfo = findClassAlias(name.getText());
if (classInfo == null) {
result = new RegularClass(name, surroundingClass, this);
}
else {
result = new ClassAlias(name, classInfo);
}
return result;
}
/**
* Looking if a given name is alias.
* @param name given name
* @return ClassInfo for alias if it exists, null otherwise
*/
protected final AbstractClassInfo findClassAlias(final String name) {
AbstractClassInfo classInfo = null;
final Iterator<Map<String, AbstractClassInfo>> iterator = typeParams.descendingIterator();
while (iterator.hasNext()) {
final Map<String, AbstractClassInfo> paramMap = iterator.next();
classInfo = paramMap.get(name);
if (classInfo != null) {
break;
}
}
return classInfo;
}
/**
* Contains class's {@code Token}.
*/
protected abstract static class AbstractClassInfo {
/** {@code FullIdent} associated with this class. */
private final Token name;
/**
* Creates new instance of class information object.
* @param className token which represents class name.
*/
protected AbstractClassInfo(final Token className) {
if (className == null) {
throw new IllegalArgumentException(
"ClassInfo's name should be non-null");
}
name = className;
}
/**
* @return {@code Class} associated with an object.
*/
// -@cs[ForbidWildcardAsReturnType] The class is deprecated and will be removed soon.
public abstract Class<?> getClazz();
/**
* Gets class name.
* @return class name
*/
public final Token getName() {
return name;
}
}
/** Represents regular classes/enums. */
private static final class RegularClass extends AbstractClassInfo {
/** Name of surrounding class. */
private final String surroundingClass;
/** The check we use to resolve classes. */
private final AbstractTypeAwareCheck check;
/** Is class loadable. */
private boolean loadable = true;
/** {@code Class} object of this class if it's loadable. */
private Class<?> classObj;
/**
* Creates new instance of of class information object.
* @param name {@code FullIdent} associated with new object.
* @param surroundingClass name of current surrounding class.
* @param check the check we use to load class.
*/
RegularClass(final Token name,
final String surroundingClass,
final AbstractTypeAwareCheck check) {
super(name);
this.surroundingClass = surroundingClass;
this.check = check;
}
@Override
public Class<?> getClazz() {
if (loadable && classObj == null) {
setClazz(check.tryLoadClass(getName(), surroundingClass));
}
return classObj;
}
/**
* Associates {@code Class} with an object.
* @param clazz {@code Class} to associate with.
*/
private void setClazz(Class<?> clazz) {
classObj = clazz;
loadable = clazz != null;
}
@Override
public String toString() {
return "RegularClass[name=" + getName()
+ ", in class=" + surroundingClass
+ ", loadable=" + loadable
+ ", class=" + classObj + "]";
}
}
/** Represents type param which is "alias" for real type. */
private static class ClassAlias extends AbstractClassInfo {
/** Class information associated with the alias. */
private final AbstractClassInfo classInfo;
/**
* Creates new instance of the class.
* @param name token which represents name of class alias.
* @param classInfo class information associated with the alias.
*/
ClassAlias(final Token name, AbstractClassInfo classInfo) {
super(name);
this.classInfo = classInfo;
}
@Override
public final Class<?> getClazz() {
return classInfo.getClazz();
}
@Override
public String toString() {
return "ClassAlias[alias " + getName() + " for " + classInfo.getName() + "]";
}
}
/**
* Represents text element with location in the text.
*/
protected static class Token {
/** Token's column number. */
private final int columnNo;
/** Token's line number. */
private final int lineNo;
/** Token's text. */
private final String text;
/**
* Creates token.
* @param text token's text
* @param lineNo token's line number
* @param columnNo token's column number
*/
public Token(String text, int lineNo, int columnNo) {
this.text = text;
this.lineNo = lineNo;
this.columnNo = columnNo;
}
/**
* Converts FullIdent to Token.
* @param fullIdent full ident to convert.
*/
public Token(FullIdent fullIdent) {
text = fullIdent.getText();
lineNo = fullIdent.getLineNo();
columnNo = fullIdent.getColumnNo();
}
/**
* Gets line number of the token.
* @return line number of the token
*/
public int getLineNo() {
return lineNo;
}
/**
* Gets column number of the token.
* @return column number of the token
*/
public int getColumnNo() {
return columnNo;
}
/**
* Gets text of the token.
* @return text of the token
*/
public String getText() {
return text;
}
@Override
public String toString() {
return "Token[" + text + "(" + lineNo
+ "x" + columnNo + ")]";
}
}
}