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();
    }

}