AvoidHidingCauseExceptionCheck.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.LinkedList;
import java.util.List;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
/**
* <p>This check prevents new exception throwing inside try/catch
* blocks without providing current exception cause.
* New exception should propagate up to a higher-level handler with exact
* cause to provide a full stack trace for the problem.</p>
* <p>
* Rationale: When handling exceptions using try/catch blocks junior developers
* may lose the original/cause exception object and information associated
* with it.
* </p>
* Examples:
* <br> <br>
* 1. Cause exception will be lost because current catch block
* contains another exception throwing.
* <pre>
* public void foo() {
* RuntimeException r;
* catch (java.lang.Exception e) {
* //your code
* throw r;
* }
* }</pre>
* 2. Cause exception will be lost because current catch block
* doesn`t contains another exception throwing.
* <pre>
* catch (IllegalStateException e) {
* //your code
* throw new RuntimeException();
* }
* catch (IllegalStateException e) {
* //your code
* throw new RuntimeException("Runtime Exception!");
* }
* </pre>
* @author <a href="mailto:Daniil.Yaroslavtsev@gmail.com"> Daniil
* Yaroslavtsev</a>
* @author <a href="mailto:IliaDubinin91@gmail.com">Ilja Dubinin</a>
* @since 1.8.0
*/
public class AvoidHidingCauseExceptionCheck extends AbstractCheck {
/**
* A key is pointing to the warning message text in "messages.properties"
* file.
*/
public static final String MSG_KEY = "avoid.hiding.cause.exception";
@Override
public int[] getDefaultTokens() {
return new int[] {
TokenTypes.LITERAL_CATCH,
};
}
@Override
public int[] getAcceptableTokens() {
return getDefaultTokens();
}
@Override
public int[] getRequiredTokens() {
return getDefaultTokens();
}
@Override
public void visitToken(DetailAST detailAST) {
final String originExcName = detailAST
.findFirstToken(TokenTypes.PARAMETER_DEF).getLastChild()
.getText();
final List<DetailAST> throwList = makeThrowList(detailAST);
final List<String> wrapExcNames = new LinkedList<>();
wrapExcNames.add(originExcName);
wrapExcNames.addAll(makeExceptionsList(detailAST, detailAST,
originExcName));
for (DetailAST throwAST : throwList) {
final List<DetailAST> throwParamNamesList = new LinkedList<>();
buildThrowParamNamesList(throwAST, throwParamNamesList);
if (!isContainsCaughtExc(throwParamNamesList, wrapExcNames)) {
log(throwAST, MSG_KEY, originExcName);
}
}
}
/**
* Returns true when aThrowParamNamesList contains caught exception.
* @param throwParamNamesList List of throw parameter names.
* @param wrapExcNames List of caught exception names.
* @return true when aThrowParamNamesList contains caught exception
*/
private static boolean isContainsCaughtExc(List<DetailAST> throwParamNamesList,
List<String> wrapExcNames) {
boolean result = false;
for (DetailAST currentNode : throwParamNamesList) {
if (currentNode.getParent().getType() != TokenTypes.DOT
&& wrapExcNames.contains(currentNode.getText())) {
result = true;
break;
}
}
return result;
}
/**
* Returns a List of<code>DetailAST</code> that contains the names of
* parameters for current "throw" keyword.
* @param startNode The start node for exception name searching.
* @param paramNamesAST The list, that will be contain names of the
* parameters
* @return A null-safe list of tokens (<code>DetailAST</code>) contains the
* thrown exception name if it was found or null otherwise.
*/
private List<DetailAST> buildThrowParamNamesList(DetailAST startNode,
List<DetailAST> paramNamesAST) {
for (DetailAST currentNode : getChildNodes(startNode)) {
if (currentNode.getType() == TokenTypes.IDENT) {
paramNamesAST.add(currentNode);
}
if (currentNode.getType() != TokenTypes.PARAMETER_DEF
&& currentNode.getType() != TokenTypes.LITERAL_TRY
&& currentNode.getNumberOfChildren() > 0) {
buildThrowParamNamesList(currentNode, paramNamesAST);
}
}
return paramNamesAST;
}
/**
* Recursive method which searches for the <code>LITERAL_THROW</code>
* DetailASTs all levels below on the current <code>aParentAST</code> node
* without entering into nested try/catch blocks.
* @param parentAST A start node for "throw" keyword <code>DetailASTs
* </code> searching.
* @return null-safe list of <code>LITERAL_THROW</code> literals
*/
private List<DetailAST> makeThrowList(DetailAST parentAST) {
final List<DetailAST> throwList = new LinkedList<>();
for (DetailAST currentNode : getChildNodes(parentAST)) {
if (currentNode.getType() == TokenTypes.LITERAL_THROW) {
throwList.add(currentNode);
}
if (currentNode.getType() != TokenTypes.PARAMETER_DEF
&& currentNode.getType() != TokenTypes.LITERAL_THROW
&& currentNode.getType() != TokenTypes.LITERAL_TRY
&& currentNode.getNumberOfChildren() > 0) {
throwList.addAll(makeThrowList(currentNode));
}
}
return throwList;
}
/**
* Searches for all exceptions that wraps the original exception
* object (only in current "catch" block).
* @param currentCatchAST A LITERAL_CATCH node of the
* current "catch" block.
* @param parentAST Current parent node to start search.
* @param currentExcName The name of exception handled by
* current "catch" block.
* @return List contains exceptions that wraps the original
* exception object.
*/
private List<String> makeExceptionsList(DetailAST currentCatchAST,
DetailAST parentAST, String currentExcName) {
final List<String> wrapExcNames = new LinkedList<>();
for (DetailAST currentNode : getChildNodes(parentAST)) {
if (currentNode.getType() == TokenTypes.IDENT
&& currentNode.getText().equals(currentExcName)
&& currentNode.getParent().getType() != TokenTypes.DOT) {
DetailAST temp = currentNode;
while (!temp.equals(currentCatchAST)
&& temp.getType() != TokenTypes.ASSIGN) {
temp = temp.getParent();
}
if (temp.getType() == TokenTypes.ASSIGN) {
DetailAST convertedExc = null;
if (temp.getParent().getType() == TokenTypes.VARIABLE_DEF) {
convertedExc = temp.getParent().findFirstToken(TokenTypes.IDENT);
}
else {
convertedExc = temp.findFirstToken(TokenTypes.IDENT);
}
if (convertedExc != null) {
wrapExcNames.add(convertedExc.getText());
}
}
}
if (currentNode.getType() != TokenTypes.PARAMETER_DEF
&& currentNode.getNumberOfChildren() > 0) {
wrapExcNames.addAll(makeExceptionsList(currentCatchAST,
currentNode, currentExcName));
}
}
return wrapExcNames;
}
/**
* Gets all the children one level below on the current parent node.
* @param node Current parent node.
* @return List of children one level below on the current
* parent node (aNode).
*/
private static List<DetailAST> getChildNodes(DetailAST node) {
final List<DetailAST> result = new LinkedList<>();
DetailAST currNode = node.getFirstChild();
while (currNode != null) {
result.add(currNode);
currNode = currNode.getNextSibling();
}
return result;
}
}