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