diff --git a/workshop/pom.xml b/workshop/pom.xml index 5e1d8576af..b15cdedb75 100644 --- a/workshop/pom.xml +++ b/workshop/pom.xml @@ -88,6 +88,11 @@ junit-jupiter-params test + + org.mockito + mockito-core + provided + diff --git a/workshop/src/main/java/tech/picnic/errorprone/workshop/bugpatterns/MockitoMockClassReference.java b/workshop/src/main/java/tech/picnic/errorprone/workshop/bugpatterns/MockitoMockClassReference.java new file mode 100644 index 0000000000..7a1b7206cf --- /dev/null +++ b/workshop/src/main/java/tech/picnic/errorprone/workshop/bugpatterns/MockitoMockClassReference.java @@ -0,0 +1,97 @@ +package tech.picnic.errorprone.workshop.bugpatterns; + +import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION; +import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION; +import static com.google.errorprone.matchers.Matchers.allOf; +import static com.google.errorprone.matchers.Matchers.argument; +import static com.google.errorprone.matchers.Matchers.isSameType; +import static com.google.errorprone.matchers.Matchers.isVariable; +import static com.google.errorprone.matchers.Matchers.not; +import static com.google.errorprone.matchers.Matchers.staticMethod; + +import com.google.auto.service.AutoService; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; +import com.google.errorprone.fixes.SuggestedFixes; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.matchers.Matcher; +import com.google.errorprone.util.ASTHelpers; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import java.util.List; +import java.util.Optional; + +/** + * A {@link BugChecker} that flags the use of {@link org.mockito.Mockito#mock(Class)} and {@link + * org.mockito.Mockito#spy(Class)} where instead the type to be mocked or spied can be derived from + * context. + */ +@AutoService(BugChecker.class) +@BugPattern( + summary = "Don't unnecessarily pass a type to Mockito's `mock(Class)` and `spy(Class)` methods", + severity = SUGGESTION, + tags = SIMPLIFICATION) +public final class MockitoMockClassReference extends BugChecker + implements MethodInvocationTreeMatcher { + private static final long serialVersionUID = 1L; + private static final Matcher MOCKITO_MOCK_OR_SPY_WITH_HARDCODED_TYPE = + allOf( + argument(0, allOf(isSameType(Class.class.getName()), not(isVariable()))), + staticMethod().onClass("org.mockito.Mockito").namedAnyOf("mock", "spy")); + + /** Instantiates a new {@link MockitoMockClassReference} instance. */ + public MockitoMockClassReference() {} + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + if (!MOCKITO_MOCK_OR_SPY_WITH_HARDCODED_TYPE.matches(tree, state) + || !isTypeDerivableFromContext(tree, state)) { + return Description.NO_MATCH; + } + + List arguments = tree.getArguments(); + return describeMatch(tree, SuggestedFixes.removeElement(arguments.get(0), arguments, state)); + } + + private static boolean isTypeDerivableFromContext(MethodInvocationTree tree, VisitorState state) { + Tree parent = state.getPath().getParentPath().getLeaf(); + switch (parent.getKind()) { + case VARIABLE: + return !ASTHelpers.hasImplicitType((VariableTree) parent, state) + && areSameType(tree, parent, state); + case ASSIGNMENT: + return areSameType(tree, parent, state); + case RETURN: + return findMethodExitedOnReturn(state) + .filter(m -> areSameType(tree, m.getReturnType(), state)) + .isPresent(); + default: + return false; + } + } + + /** + * Tells whether the given trees are of the same type, after type erasure. + * + * @param treeA The first tree of interest. + * @param treeB The second tree of interest. + * @param state The {@link VisitorState} describing the context in which the given trees were + * found. + * @return Whether the specified trees have the same erased types. + */ + private static boolean areSameType(Tree treeA, Tree treeB, VisitorState state) { + return ASTHelpers.isSameType(ASTHelpers.getType(treeA), ASTHelpers.getType(treeB), state); + } + + private static Optional findMethodExitedOnReturn(VisitorState state) { + return Optional.ofNullable(state.findEnclosing(LambdaExpressionTree.class, MethodTree.class)) + .filter(MethodTree.class::isInstance) + .map(MethodTree.class::cast); + } +}