/*
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.errorprone.bugpatterns;
import static com.google.common.collect.Iterables.getLast;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.JUnitMatchers;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionStatementTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCAssign;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCIdent;
import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
import com.sun.tools.javac.tree.JCTree.Tag;
import java.util.List;
/** @author cushon@google.com (Liam Miller-Cushon) */
public abstract class AbstractTestExceptionChecker extends BugChecker implements MethodTreeMatcher {
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
if (tree.getBody() == null) {
return NO_MATCH;
}
SuggestedFix.Builder baseFixBuilder = SuggestedFix.builder();
JCExpression expectedException =
deleteExpectedException(
baseFixBuilder, ((JCMethodDecl) tree).getModifiers().getAnnotations(), state);
SuggestedFix baseFix = baseFixBuilder.build();
if (expectedException == null) {
return NO_MATCH;
}
return handleStatements(tree, state, expectedException, baseFix);
}
/**
* Handle a method annotated with {@code @Test(expected=...}.
*
* @param tree the method
* @param state the visitor state
* @param expectedException the type of expected exception
* @param baseFix the base fix
*/
protected abstract Description handleStatements(
MethodTree tree, VisitorState state, JCExpression expectedException, SuggestedFix baseFix);
// TODO(cushon): extracting one statement into a lambda may not compile if the statement has
// side effects (e.g. it references a variable in the method that isn't effectively final).
// If this is a problem, consider trying to detect and avoid that case.
protected static SuggestedFix buildFix(
VisitorState state,
SuggestedFix.Builder fix,
JCExpression expectedException,
List<? extends StatementTree> statements) {
fix.addStaticImport("org.junit.Assert.assertThrows");
StringBuilder prefix = new StringBuilder();
prefix.append(
String.format("assertThrows(%s, () -> ", state.getSourceForNode(expectedException)));
if (statements.size() == 1 && getOnlyElement(statements) instanceof ExpressionStatementTree) {
ExpressionTree expression =
((ExpressionStatementTree) getOnlyElement(statements)).getExpression();
fix.prefixWith(expression, prefix.toString());
fix.postfixWith(expression, ")");
} else {
prefix.append(" {");
fix.prefixWith(statements.iterator().next(), prefix.toString());
fix.postfixWith(getLast(statements), "});");
}
return fix.build();
}
/**
* Searches the annotation list for {@code @Test(expected=...)}. If found, deletes the exception
* attribute from the annotation, and returns its value.
*/
private static JCExpression deleteExpectedException(
SuggestedFix.Builder fix, List<JCAnnotation> annotations, VisitorState state) {
Type testAnnotation = state.getTypeFromString(JUnitMatchers.JUNIT4_TEST_ANNOTATION);
for (JCAnnotation annotationTree : annotations) {
if (!ASTHelpers.isSameType(testAnnotation, annotationTree.type, state)) {
continue;
}
com.sun.tools.javac.util.List<JCExpression> arguments = annotationTree.getArguments();
for (JCExpression arg : arguments) {
if (!arg.hasTag(Tag.ASSIGN)) {
continue;
}
JCAssign assign = (JCAssign) arg;
if (assign.lhs.hasTag(Tag.IDENT)
&& ((JCIdent) assign.lhs).getName().contentEquals("expected")) {
if (arguments.size() == 1) {
fix.replace(annotationTree, "@Test");
} else {
removeFromList(fix, state, arguments, assign);
}
return assign.rhs;
}
}
}
return null;
}
/** Deletes an entry and its delimiter from a list. */
private static void removeFromList(
SuggestedFix.Builder fix, VisitorState state, List<? extends Tree> arguments, Tree tree) {
int idx = arguments.indexOf(tree);
if (idx == arguments.size() - 1) {
fix.replace(
state.getEndPosition(arguments.get(arguments.size() - 1)),
state.getEndPosition(tree),
"");
} else {
fix.replace(
((JCTree) tree).getStartPosition(),
((JCTree) arguments.get(idx + 1)).getStartPosition(),
"");
}
}
}