ExceptionAsSurprise.java

package de.schegge.errorprone;

import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
import com.google.errorprone.BugPattern.SeverityLevel;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.IfTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.BinaryTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IfTree;
import com.sun.source.tree.LiteralTree;
import com.sun.source.tree.ParenthesizedTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.UnaryTree;

import java.util.List;
import java.util.Map;

import static java.util.stream.Collectors.joining;

@AutoService(BugChecker.class)
@BugPattern(name = "ExceptionAsSurprise", severity = SeverityLevel.WARNING,
        summary = "Throwing an Exception in the else statement is much harder to read."
)
public class ExceptionAsSurprise extends BugChecker implements IfTreeMatcher {

    private static final Map<Kind, String> REVERSE = Map.of(
            Kind.LESS_THAN, " >= ",
            Kind.LESS_THAN_EQUAL, " > ",
            Kind.GREATER_THAN, " <= ",
            Kind.GREATER_THAN_EQUAL, " < ",
            Kind.EQUAL_TO, " != ",
            Kind.NOT_EQUAL_TO, " == "
    );

    private static final Map<Kind, String> DE_MORGAN = Map.of(
            Kind.AND, " | ",
            Kind.OR, " & ",
            Kind.CONDITIONAL_OR, " && ",
            Kind.CONDITIONAL_AND, " || "
    );

    private final int maximumThrowBlockLength;

    public ExceptionAsSurprise(ErrorProneFlags flags) {
        this.maximumThrowBlockLength = flags.getInteger("ExceptionAsSurprise:Lines").orElse(3);
    }

    public ExceptionAsSurprise() {
        this.maximumThrowBlockLength = 3;
    }

    @Override
    public Description matchIf(IfTree tree, VisitorState state) {
        StatementTree elseStatement = tree.getElseStatement();
        if (elseStatement == null) {
            return Description.NO_MATCH;
        }
        if (elseStatement.getKind() == Kind.BLOCK) {
            List<? extends StatementTree> statements = ((BlockTree) elseStatement).getStatements();
            if (!statements.isEmpty() && statements.size() <= maximumThrowBlockLength && statements.get(statements.size() - 1).getKind() == Kind.THROW) {
                return switchThenAndElse(tree, elseStatement, state);
            }
        } else if (elseStatement.getKind() == Kind.THROW) {
            return switchThenAndElse(tree, elseStatement, state);
        }
        return Description.NO_MATCH;
    }

    private Description switchThenAndElse(IfTree tree, StatementTree throwTree, VisitorState state) {
        ExpressionTree condition = removeParenthesis(tree.getCondition());
        Description.Builder description = buildDescription(tree)
                .setLinkUrl("https://schegge.de/2018/04/refactoring-mit-guard-clauses/")
                .addFix(SuggestedFix.replace(tree,
                        "if (" + invert(condition) + ") {\n" + withoutOuterBlock(throwTree, state) + "\n}\n" +
                                withoutOuterBlock(tree.getThenStatement(), state)));
        return description.build();
    }

    private String withoutOuterBlock(StatementTree tree, VisitorState state) {
        if (tree.getKind() == Kind.EMPTY_STATEMENT) {
            return "";
        }
        if (tree.getKind() != Kind.BLOCK) {
            return state.getSourceForNode(tree);
        }
        return ((BlockTree) tree).getStatements().stream().map(state::getSourceForNode).collect(joining("\n", "", "\n"));
    }

    private String invert(ExpressionTree condition) {
        if (condition.getKind() == Kind.LOGICAL_COMPLEMENT) {
            return ((UnaryTree) condition).getExpression().toString();
        }
        if (condition.getKind() == Kind.BOOLEAN_LITERAL) {
            return String.valueOf(!(Boolean) ((LiteralTree) condition).getValue());
        }
        String operator = REVERSE.get(condition.getKind());
        if (operator != null) {
            BinaryTree binaryTree = (BinaryTree) condition;
            return binaryTree.getLeftOperand() + operator + binaryTree.getRightOperand();
        }
        operator = DE_MORGAN.get(condition.getKind());
        if (operator != null) {
            BinaryTree binaryTree = (BinaryTree) condition;
            return invert(binaryTree.getLeftOperand(), binaryTree.getKind()) + operator + invert(binaryTree.getRightOperand(), binaryTree.getKind());
        }
        return addComplement(condition);
    }

    private String invert(ExpressionTree condition, Kind operator) {
        ExpressionTree withoutParenthesis = removeParenthesis(condition);
        String result = invert(withoutParenthesis);
        if ((operator == Kind.OR || operator == Kind.CONDITIONAL_OR) && (withoutParenthesis.getKind() == Kind.AND || withoutParenthesis.getKind() == Kind.CONDITIONAL_AND)) {
            return "(" + result + ")";
        }
        return result;
    }

    private ExpressionTree removeParenthesis(ExpressionTree ifCondition) {
        ExpressionTree result = ifCondition;
        while (result instanceof ParenthesizedTree parenthesizedTree) {
            result = parenthesizedTree.getExpression();
        }
        return result;
    }

    private String addComplement(ExpressionTree condition) {
        switch (condition.getKind()) {
            case PARENTHESIZED, IDENTIFIER -> {
                return "!" + condition;
            }
            case BOOLEAN_LITERAL -> {
                return String.valueOf(!(Boolean) ((LiteralTree) condition).getValue());
            }
            default -> {
                return "!(" + condition + ")";
            }
        }
    }
}