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 + ")";
}
}
}
}