CauseParameterInExceptionCheck.java
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2019 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.github.sevntu.checkstyle.checks.design;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import com.github.sevntu.checkstyle.SevntuUtil;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
/**
* Checks that any Exception class which matches the defined className regexp
* have at least one constructor with Exception cause as a parameter. <br><p>
* Rationale: <br><br>
* "A special form of exception translation called exception chaining is
* appropriate in cases where the lower-level exception might be helpful to
* someone debugging the problem that caused the higher-level exception. The
* lower-level exception (the cause) is passed to the higher-level.."
* <i>[Joshua Bloch - Effective Java 2nd Edition, Chapter 4, Item 61]</i>
* </p><p> Parameters: </p><ol>
* <li>Exception classNames regexp. ("classNamesRegexp" option).</li>
* <li>regexp to ignore classes by names ("ignoredClassNamesRegexp" option).
* </li><li>The names of classes which would be considered as Exception cause
* ("allowedCauseTypes" option).</li></ol><br>
* @author <a href="mailto:Daniil.Yaroslavtsev@gmail.com"> Daniil
* Yaroslavtsev</a>
* @since 1.8.0
*/
public class CauseParameterInExceptionCheck extends AbstractCheck {
/**
* A key is pointing to the warning message text in "messages.properties"
* file.
*/
public static final String MSG_KEY = "cause.parameter.in.exception";
/**
* Pattern object is used to store the regexp for the names of classes, that
* should be checked. Default value = ".+Exception".
*/
private Pattern classNamesRegexp = Pattern.compile(".+Exception");
/**
* Pattern object is used to store the regexp for the names of classes, that
* should be ignored by check.
*/
private Pattern ignoredClassNamesRegexp = Pattern.compile("");
/**
* List contains the names of classes which would be considered as Exception
* cause. Default value = "Throwable, Exception".
*/
private final Set<String> allowedCauseTypes = new HashSet<>();
/**
* List of DetailAST objects which are related to Exception classes that
* need to be warned.
*/
private final List<DetailAST> exceptionClassesToWarn =
new LinkedList<>();
/**
* Creates the new check instance.
*/
public CauseParameterInExceptionCheck() {
allowedCauseTypes.add("Exception");
allowedCauseTypes.add("Throwable");
}
/**
* Sets the regexp for the names of classes, that should be checked.
* @param classNamesRegexp
* String contains the regex to set for the names of classes, that
* should be checked.
*/
public void setClassNamesRegexp(String classNamesRegexp) {
final String regexp;
if (classNamesRegexp == null) {
regexp = "";
}
else {
regexp = classNamesRegexp;
}
this.classNamesRegexp = Pattern.compile(regexp);
}
/**
* Sets the regexp for the names of classes, that should be ignored by
* check.
* @param ignoredClassNamesRegexp
* String contains the regex to set for the names of classes, that
* should be ignored by check.
*/
public void setIgnoredClassNamesRegexp(String ignoredClassNamesRegexp) {
final String regexp;
if (ignoredClassNamesRegexp == null) {
regexp = "";
}
else {
regexp = ignoredClassNamesRegexp;
}
this.ignoredClassNamesRegexp = Pattern.compile(regexp);
}
/**
* Sets the names of classes which would be considered as Exception cause.
* @param allowedCauseTypes
* - the list of classNames separated by a comma. ClassName should be
* short, such as "NullpointerException", do not use full name -
* java.lang.NullpointerException;
*/
public void setAllowedCauseTypes(final String... allowedCauseTypes) {
this.allowedCauseTypes.clear();
for (String name : allowedCauseTypes) {
this.allowedCauseTypes.add(name);
}
}
@Override
public int[] getDefaultTokens() {
return new int[] {
TokenTypes.CLASS_DEF,
TokenTypes.CTOR_DEF,
};
}
@Override
public int[] getAcceptableTokens() {
return getDefaultTokens();
}
@Override
public int[] getRequiredTokens() {
return getDefaultTokens();
}
@Override
public void beginTree(DetailAST rootAST) {
exceptionClassesToWarn.clear();
}
@Override
public void visitToken(DetailAST ast) {
switch (ast.getType()) {
case TokenTypes.CLASS_DEF:
final String exceptionClassName = getName(ast);
if (classNamesRegexp.matcher(exceptionClassName).matches()
&& !ignoredClassNamesRegexp.matcher(exceptionClassName)
.matches()) {
exceptionClassesToWarn.add(ast);
}
break;
case TokenTypes.CTOR_DEF:
final DetailAST exceptionClass = getClassDef(ast);
if (exceptionClassesToWarn.contains(exceptionClass)
&& hasCauseAsParameter(ast)) {
exceptionClassesToWarn.remove(exceptionClass);
}
break;
default:
SevntuUtil.reportInvalidToken(ast.getType());
break;
}
}
@Override
public void finishTree(DetailAST treeRootAST) {
for (DetailAST classDefNode : exceptionClassesToWarn) {
log(classDefNode, MSG_KEY, getName(classDefNode));
}
}
/**
* Checks that the given constructor contains exception cause as a
* parameter.
* @param ctorDefNode
* The CTOR_DEF DetailAST node is related to the constructor
* definition.
* @return true if the given ctor contains exception cause as a parameter
* and false otherwise.
*/
private boolean hasCauseAsParameter(DetailAST ctorDefNode) {
boolean result = false;
final DetailAST parameters =
ctorDefNode.findFirstToken(TokenTypes.PARAMETERS);
for (String parameterType : getParameterTypes(parameters)) {
if (allowedCauseTypes.contains(parameterType)) {
result = true;
break;
}
}
return result;
}
/**
* Gets the list of classNames for given constructor parameters types.
* @param parametersAST - A PARAMETERS DetailAST.
* @return the list of classNames for given constructor parameters types.
*/
private static List<String> getParameterTypes(DetailAST parametersAST) {
final List<String> result = new LinkedList<>();
for (DetailAST parametersChild : getChildren(parametersAST)) {
if (parametersChild.getType() == TokenTypes.PARAMETER_DEF) {
final DetailAST parameterType = parametersChild
.findFirstToken(TokenTypes.TYPE);
final String parameter = parameterType.getFirstChild()
.getText();
result.add(parameter);
}
}
return result;
}
/**
* Gets the name of given class or constructor.
* @param classOrCtorDefNode
* the a CLASS_DEF or CTOR_DEF node
* @return the name of class or constructor is related to CLASS_DEF or
* CTOR_DEF node.
*/
private static String getName(final DetailAST classOrCtorDefNode) {
final DetailAST classNameIdent = classOrCtorDefNode
.findFirstToken(TokenTypes.IDENT);
return classNameIdent.getText();
}
/**
* Gets the parent CLASS_DEF DetailAST node for given DetailAST node.
* @param node
* The DetailAST node.
* @return The parent CLASS_DEF node for the class that owns a token is
* related to the given DetailAST node or null if given token is not
* located in any class.
*/
private static DetailAST getClassDef(final DetailAST node) {
DetailAST curNode = node;
while (curNode != null && curNode.getType() != TokenTypes.CLASS_DEF) {
curNode = curNode.getParent();
}
return curNode;
}
/**
* Gets all the children which are one level below on the current DetailAST
* parent node.
* @param node
* Current parent node.
* @return The list of children one level below on the current parent node.
*/
private static List<DetailAST> getChildren(final DetailAST node) {
final List<DetailAST> result = new LinkedList<>();
DetailAST curNode = node.getFirstChild();
while (curNode != null) {
result.add(curNode);
curNode = curNode.getNextSibling();
}
return result;
}
}