ForbidThrowAnonymousExceptionsCheck.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.coding;
import java.util.ArrayList;
import java.util.List;
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;
/**
* <p>
* This Check warns on throwing anonymous exception.
* </p>
* Examples:
* <pre>
* catch (Exception e) {
* throw new RuntimeException() { //WARNING
* //some code
* };
* }
* <br>
* catch (Exception e) {
* RuntimeException run = new RuntimeException() {
* //some code
* };
* throw run; //WARNING
* }
* </pre> The distinguishing of <b>exception</b> types occurs by
* analyzing variable's class's name.<br>
* Check has an option which contains the regular expression for exception class name matching<br>
* Default value is "^.*Exception" because usually exception type ends with suffix "Exception".<br>
* Then, if we have an ObjBlock (distinguished by curly braces), it's anonymous<br>
* exception definition. It could be defined in <b>throw</b> statement
* immediately.<br>
* In that case, after literal new, there would be an expression type finishing
* with and ObjBlock.<br>
* <br>
* @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
* @author <a href="mailto:maxvetrenko2241@gmail.com">Max Vetrenko</a>
* @since 1.11.0
*/
public class ForbidThrowAnonymousExceptionsCheck extends AbstractCheck {
/**
* Warning message key.
*/
public static final String MSG_KEY = "forbid.throw.anonymous.exception";
/** Regular expression of exception naming. */
private static final String DEFAULT_EXCEPTION_CLASS_NAME_REGEX = "^.*Exception";
/** List of anonymous exceptions to ignore. */
private final List<String> anonymousExceptions = new ArrayList<>();
/** User set expression for exception names. */
private Pattern pattern = Pattern.compile(DEFAULT_EXCEPTION_CLASS_NAME_REGEX);
/**
* Setter for pattern.
* @param exceptionClassNameRegex The regular expression to set.
*/
public void setExceptionClassNameRegex(String exceptionClassNameRegex) {
this.pattern = Pattern.compile(exceptionClassNameRegex);
}
@Override
public int[] getDefaultTokens() {
return new int[] {
TokenTypes.LITERAL_THROW,
TokenTypes.VARIABLE_DEF,
};
}
@Override
public int[] getAcceptableTokens() {
return getDefaultTokens();
}
@Override
public int[] getRequiredTokens() {
return getDefaultTokens();
}
@Override
public void visitToken(DetailAST literalThrowOrVariableDefAst) {
switch (literalThrowOrVariableDefAst.getType()) {
case TokenTypes.LITERAL_THROW:
identifyThrowingAnonymousException(literalThrowOrVariableDefAst);
break;
case TokenTypes.VARIABLE_DEF:
lookForAnonymousExceptionDefinition(literalThrowOrVariableDefAst);
break;
default:
SevntuUtil.reportInvalidToken(literalThrowOrVariableDefAst.getType());
break;
}
}
/**
* Warns on throwing anonymous exception.
* @param throwDefAst The token to examine.
*/
private void identifyThrowingAnonymousException(DetailAST throwDefAst) {
final DetailAST throwingLiteralNewAst = getLiteralNew(throwDefAst);
if (throwingLiteralNewAst != null
&& hasObjectBlock(throwingLiteralNewAst)) {
log(throwDefAst, MSG_KEY);
}
else if (throwingLiteralNewAst == null) {
final DetailAST throwingExceptionNameAst = getThrowingExceptionNameAst(throwDefAst
.getFirstChild());
if (throwingExceptionNameAst != null
&& anonymousExceptions.contains(throwingExceptionNameAst
.getText())) {
log(throwDefAst, MSG_KEY);
}
}
}
/**
* Analyzes variable definition for anonymous exception definition. if found
* - adds it to list of anonymous exceptions
* @param variableDefAst The token to examine.
*/
private void
lookForAnonymousExceptionDefinition(DetailAST variableDefAst) {
DetailAST variableLiteralNewAst = null;
final DetailAST variableAssignment = variableDefAst.findFirstToken(TokenTypes.ASSIGN);
if (variableAssignment != null && variableAssignment.getFirstChild() != null) {
variableLiteralNewAst = getLiteralNew(variableAssignment);
}
final DetailAST variableNameAst = variableDefAst
.findFirstToken(TokenTypes.TYPE).getNextSibling();
if (isExceptionName(variableNameAst)) {
final String exceptionName = variableNameAst.getText();
if (anonymousExceptions.contains(exceptionName)) {
anonymousExceptions.remove(exceptionName);
}
if (variableLiteralNewAst != null
&& hasObjectBlock(variableLiteralNewAst)) {
anonymousExceptions.add(exceptionName);
}
}
}
/**
* Gets the literal new node from variable definition node or throw node.
* @param literalThrowOrVariableDefAst The token to examine.
* @return the specified node.
*/
private static DetailAST
getLiteralNew(DetailAST literalThrowOrVariableDefAst) {
return literalThrowOrVariableDefAst.getFirstChild().findFirstToken(
TokenTypes.LITERAL_NEW);
}
/**
* Retrieves the AST node which contains the name of throwing exception.
* @param expressionAst The token to examine.
* @return the specified node.
*/
private static DetailAST
getThrowingExceptionNameAst(DetailAST expressionAst) {
return expressionAst.findFirstToken(TokenTypes.IDENT);
}
/**
* Checks if definition with a literal new has an ObjBlock.
* @param literalNewAst The token to examine.
* @return true if the new has an object block.
*/
private static boolean hasObjectBlock(DetailAST literalNewAst) {
return literalNewAst.getLastChild().getType() == TokenTypes.OBJBLOCK;
}
/**
* Checks if variable name is definitely an exception name. It is so if
* variable type ends with "Exception" suffix
* @param variableNameAst The token to examine.
* @return true if the name is an exception.
*/
private boolean isExceptionName(DetailAST variableNameAst) {
final DetailAST typeAst = variableNameAst.getPreviousSibling();
final String typeName = typeAst.getFirstChild().getText();
return pattern.matcher(typeName).matches();
}
}