ReturnCountExtendedCheck.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.Collection;
import java.util.HashSet;
import java.util.Set;

import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.checks.coding.ReturnCountCheck;

/**
 * Checks that method/ctor "return" literal count is not greater than the given value
 * ("maxReturnCount" property).<br>
 * <br>
 * Rationale:<br>
 * <br>
 * One return per method is a good practice as its ease understanding of method logic. <br>
 * <br>
 * Reasoning is that:
 * <ul>
 * <li>It is easier to understand control flow when you know exactly where the method returns.
 * <li>Methods with 2-3 or many "return" statements are much more difficult to understand,
 * debug and refactor.
 * </ul>
 * Setting up the check options will make it to ignore:
 * <ol>
 * <li>Methods by name ("ignoreMethodsNames" property). Note, that the "ignoreMethodsNames"
 * property type is a RegExp:
 * using this property you can list the names of ignored methods separated by comma (but you
 * can also use '|' to separate different method names in usual for RegExp style).
 * If the violation is on a lambda, since it has no method name, you can specify the string
 * {@code null} to ignore all lambda violations for now. It should be noted, that ignoring lambdas
 * this way may not always be supported as it is a hack and giving all lambdas the same name. It
 * could be changed if a better way to single out individual lambdas if found.
 * </li>
 * <li>Methods which linelength less than given value ("linesLimit" property).
 * <li>"return" statements which depth is greater or equal to the given value ("returnDepthLimit"
 * property). There are few supported <br>
 * coding blocks when depth counting: "if-else", "for", "while"/"do-while" and "switch".
 * <li>"Empty" return statements = return statements in void methods and ctors that have not
 * any expression ("ignoreEmptyReturns" property).
 * <li>Return statements, which are located in the top lines of method/ctor (you can specify
 * the count of top method/ctor lines that will be ignored using "rowsToIgnoreCount" property).
 * </ol>
 * So, this is much improved version of the existing {@link ReturnCountCheck}. <br>
 * <br>
 *
 * @author <a href="mailto:Daniil.Yaroslavtsev@gmail.com"> Daniil Yaroslavtsev</a>
 * @since 1.8.0
 */
public class ReturnCountExtendedCheck extends AbstractCheck {

    /**
     * A key is pointing to the warning message text in "messages.properties"
     * file.
     */
    public static final String MSG_KEY_METHOD =
            "return.count.extended.method";

    /**
     * A key is pointing to the warning message text in "messages.properties"
     * file.
     */
    public static final String MSG_KEY_CTOR =
            "return.count.extended.ctor";

    /**
     * A key is pointing to the warning message text in "messages.properties"
     * file.
     */
    public static final String MSG_KEY_LAMBDA =
            "return.count.extended.lambda";

    /**
     * Default maximum allowed "return" literals count per method/ctor/lambda.
     */
    private static final int DEFAULT_MAX_RETURN_COUNT = 1;

    /**
     * Default number of lines of which method/ctor/lambda body may consist to be
     * skipped by check.
     */
    private static final int DEFAULT_IGNORE_METHOD_LINES_COUNT = 20;

    /**
     * Default minimum "return" statement depth when current "return statement"
     * will be skipped by check.
     */
    private static final int DEFAULT_MIN_IGNORE_RETURN_DEPTH = 4;

    /**
     * Number which defines, how many lines of code on the top of current
     * processed method/ctor/lambda will be ignored by check.
     */
    private static final int DEFAULT_TOP_LINES_TO_IGNORE_COUNT = 5;

    /**
     * List contains RegExp patterns for methods' names which would be ignored by check.
     */
    private final Set<String> ignoreMethodsNames = new HashSet<>();

    /**
     * Maximum allowed "return" literals count per method/ctor/lambda (1 by default).
     */
    private int maxReturnCount = DEFAULT_MAX_RETURN_COUNT;

    /**
     * Maximum number of lines of which method/ctor/lambda body may consist to be
     * skipped by check. 20 by default.
     */
    private int ignoreMethodLinesCount = DEFAULT_IGNORE_METHOD_LINES_COUNT;

    /**
     * Minimum "return" statement depth to be skipped by check. 4 by default.
     */
    private int minIgnoreReturnDepth = DEFAULT_MIN_IGNORE_RETURN_DEPTH;

    /**
     * Option to ignore "empty" return statements in void methods and ctors and lambdas.
     * "true" by default.
     */
    private boolean ignoreEmptyReturns = true;

    /**
     * Number which defines, how many lines of code on the top of each
     * processed method/ctor/lambda will be ignored by check. 5 by default.
     */
    private int topLinesToIgnoreCount = DEFAULT_TOP_LINES_TO_IGNORE_COUNT;

    /**
     * Creates the new check instance.
     */
    public ReturnCountExtendedCheck() {
        ignoreMethodsNames.add("equals");
    }

    /**
     * Sets the RegExp patterns for methods' names which would be ignored by check.
     *
     * @param ignoreMethodNames
     *            list of the RegExp patterns for methods' names which should be ignored by check
     */
    public void setIgnoreMethodsNames(String... ignoreMethodNames) {
        ignoreMethodsNames.clear();
        if (ignoreMethodNames != null) {
            for (String name : ignoreMethodNames) {
                ignoreMethodsNames.add(name);
            }
        }
    }

    /**
     * Sets maximum allowed "return" literals count per method/ctor/lambda.
     * @param maxReturnCount - the new "maxReturnCount" property value.
     * @see ReturnCountExtendedCheck#maxReturnCount
     */
    public void setMaxReturnCount(int maxReturnCount) {
        this.maxReturnCount = maxReturnCount;
    }

    /**
     * Sets the maximum number of lines of which method/ctor/lambda body may consist to
     * be skipped by check.
     * @param ignoreMethodLinesCount
     *        - the new value of "ignoreMethodLinesCount" property.
     * @see ReturnCountExtendedCheck#ignoreMethodLinesCount
     */
    public void setIgnoreMethodLinesCount(int ignoreMethodLinesCount) {
        this.ignoreMethodLinesCount = ignoreMethodLinesCount;
    }

    /**
     * Sets the minimum "return" statement depth with that will be skipped by
     * check.
     * @param minIgnoreReturnDepth
     *        - the new "minIgnoreReturnDepth" property value.
     */
    public void setMinIgnoreReturnDepth(int minIgnoreReturnDepth) {
        this.minIgnoreReturnDepth = minIgnoreReturnDepth;
    }

    /**
     * Sets the "ignoring empty return statements in void methods and ctors and lambdas"
     * option state.
     * @param ignoreEmptyReturns
     *        the new "allowEmptyReturns" property value.
     * @see ReturnCountExtendedCheck#ignoreEmptyReturns
     */
    public void setIgnoreEmptyReturns(boolean ignoreEmptyReturns) {
        this.ignoreEmptyReturns = ignoreEmptyReturns;
    }

    /**
     * Sets the count of code lines on the top of each
     * processed method/ctor that will be ignored by check.
     * @param topLinesToIgnoreCount
     *        the new "rowsToIgnoreCount" property value.
     * @see ReturnCountExtendedCheck#topLinesToIgnoreCount
     */
    public void setTopLinesToIgnoreCount(int topLinesToIgnoreCount) {
        this.topLinesToIgnoreCount = topLinesToIgnoreCount;
    }

    @Override
    public int[] getDefaultTokens() {
        return new int[] {
            TokenTypes.METHOD_DEF,
            TokenTypes.CTOR_DEF,
            TokenTypes.LAMBDA,
        };
    }

    @Override
    public int[] getAcceptableTokens() {
        return getDefaultTokens();
    }

    @Override
    public int[] getRequiredTokens() {
        return getDefaultTokens();
    }

    @Override
    public void visitToken(final DetailAST node) {
        final DetailAST openingBrace = node
                .findFirstToken(TokenTypes.SLIST);
        final String nodeName = getMethodName(node);
        if (openingBrace != null
                && !matches(nodeName, ignoreMethodsNames)) {
            final DetailAST closingBrace = openingBrace.getLastChild();

            int curMethodLinesCount = getLinesCount(openingBrace,
                    closingBrace);

            if (curMethodLinesCount != 0) {
                curMethodLinesCount--;
            }

            if (curMethodLinesCount >= ignoreMethodLinesCount) {
                final int mCurReturnCount = getReturnCount(node,
                        openingBrace);

                if (mCurReturnCount > maxReturnCount) {
                    logViolation(node, nodeName, mCurReturnCount);
                }
            }
        }
    }

    /**
     * Reports violation to user based on the parameters given.
     * @param node The node that the violation is on.
     * @param nodeName The name given to the node.
     * @param mCurReturnCount The return count violation amount.
     */
    private void logViolation(DetailAST node, String nodeName, int mCurReturnCount) {
        if (node.getType() == TokenTypes.LAMBDA) {
            // lambdas have no name
            log(node, MSG_KEY_LAMBDA, mCurReturnCount, maxReturnCount);
        }
        else {
            final DetailAST nodeNameToken = node
                    .findFirstToken(TokenTypes.IDENT);
            final String mKey;

            if (node.getType() == TokenTypes.METHOD_DEF) {
                mKey = MSG_KEY_METHOD;
            }
            else {
                mKey = MSG_KEY_CTOR;
            }

            log(nodeNameToken, mKey,
                    nodeName, mCurReturnCount,
                    maxReturnCount);
        }
    }

    /**
     * Gets the "return" statements count for given method/ctor/lambda and saves the
     * last "return" statement DetailAST node for given method/ctor/lambda body. Uses
     * an iterative algorithm.
     * @param methodOpeningBrace
     *        a DetailAST node that points to the current method`s opening
     *        brace.
     * @param methodDefNode
     *        DetailAST node is pointing to current method definition is being
     *        processed.
     * @return "return" literals count for given method.
     */
    private int getReturnCount(final DetailAST methodDefNode,
            final DetailAST methodOpeningBrace) {
        int result = 0;

        DetailAST curNode = methodOpeningBrace;

        // stop at closing brace
        while (curNode.getType() != TokenTypes.RCURLY
                || curNode.getParent() != methodOpeningBrace) {
            if (curNode.getType() == TokenTypes.LITERAL_RETURN
                    && getDepth(methodDefNode, curNode) < minIgnoreReturnDepth
                    && shouldEmptyReturnStatementBeCounted(curNode)
                    && getLinesCount(methodOpeningBrace,
                            curNode) > topLinesToIgnoreCount) {
                result++;
            }

            // before node leaving
            DetailAST nextNode = curNode.getFirstChild();

            final int type = curNode.getType();
            // skip nested methods (UI listeners, Runnable.run(), etc.)
            if (type == TokenTypes.METHOD_DEF
                  // skip anonymous classes
                  || type == TokenTypes.CLASS_DEF
                  // skip lambdas which is like an anonymous class/method
                  || type == TokenTypes.LAMBDA) {
                nextNode = curNode.getNextSibling();
            }

            while (nextNode == null) {
                // leave the visited Node
                nextNode = curNode.getNextSibling();
                if (nextNode == null) {
                    curNode = curNode.getParent();
                }
            }
            curNode = nextNode;
        }
        return result;
    }

    /**
     * Checks that the current processed "return" statement is "empty" and
     * should to be counted.
     * @param returnNode
     *        the DetailAST node is pointing to the current "return" statement.
     *        is being processed.
     * @return true if current processed "return" statement is empty or if
     *         mIgnoreEmptyReturns option has "false" value.
     */
    private boolean shouldEmptyReturnStatementBeCounted(DetailAST returnNode) {
        final DetailAST returnChildNode = returnNode.getFirstChild();
        return !ignoreEmptyReturns
                || returnChildNode.getType() != TokenTypes.SEMI;
    }

    /**
     * Gets the depth level of given "return" statement. There are few supported
     * coding blocks when depth counting: "if-else", "for", "while"/"do-while"
     * and "switch".
     * @param methodDefNode
     *        a DetailAST node that points to the current method`s definition.
     * @param returnStmtNode
     *        given "return" statement node.
     * @return the depth of given
     */
    private static int getDepth(DetailAST methodDefNode,
            DetailAST returnStmtNode) {
        int result = 0;

        DetailAST curNode = returnStmtNode;

        while (!curNode.equals(methodDefNode)) {
            curNode = curNode.getParent();
            final int type = curNode.getType();
            if (type == TokenTypes.LITERAL_IF
                    || type == TokenTypes.LITERAL_SWITCH
                    || type == TokenTypes.LITERAL_FOR
                    || type == TokenTypes.LITERAL_DO
                    || type == TokenTypes.LITERAL_WHILE
                    || type == TokenTypes.LITERAL_TRY) {
                result++;
            }
        }
        return result;
    }

    /**
     * Gets the name of given method by DetailAST node is pointing to desired
     * method definition.
     * @param methodDefNode
     *        a DetailAST node that points to the current method`s definition.
     * @return the method name.
     */
    private static String getMethodName(DetailAST methodDefNode) {
        String result = null;
        final DetailAST ident = methodDefNode.findFirstToken(TokenTypes.IDENT);

        // lambdas don't have a name
        if (ident != null && methodDefNode.getType() != TokenTypes.LAMBDA) {
            result = ident.getText();
        }

        return result;
    }

    /**
     * Gets the line count between the two DetailASTs which are related to the
     * given "begin" and "end" tokens.
     * @param beginAst
     *        the "begin" token AST node.
     * @param endAST
     *        the "end" token AST node.
     * @return the line count between "begin" and "end" tokens.
     */
    private static int getLinesCount(DetailAST beginAst, DetailAST endAST) {
        return endAST.getLineNo() - beginAst.getLineNo();
    }

    /**
     * Matches string to given list of RegExp patterns.
     *
     * @param string
     *            String to be matched.
     * @param patterns
     *            Collection of RegExp patterns to match with.
     * @return true if given string could be fully matched by one of given patterns, false otherwise
     */
    private static boolean matches(String string, Collection<String> patterns) {
        String match = string;

        if (match == null) {
            match = "null";
        }

        boolean result = false;
        if (!patterns.isEmpty()) {
            for (String pattern : patterns) {
                if (match.matches(pattern)) {
                    result = true;
                    break;
                }
            }
        }
        return result;
    }

}