diff --git a/error-prone-contrib/pom.xml b/error-prone-contrib/pom.xml
index ec85ebfd47..ec9643c9c6 100644
--- a/error-prone-contrib/pom.xml
+++ b/error-prone-contrib/pom.xml
@@ -147,6 +147,11 @@
mockito-core
provided
+
+ org.mockito
+ mockito-junit-jupiter
+ test
+
org.reactivestreams
reactive-streams
diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MockitoAnnotationCheck.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MockitoAnnotationCheck.java
new file mode 100644
index 0000000000..ca53840355
--- /dev/null
+++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MockitoAnnotationCheck.java
@@ -0,0 +1,75 @@
+package tech.picnic.errorprone.bugpatterns;
+
+import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
+import static com.google.errorprone.matchers.Description.NO_MATCH;
+import static com.google.errorprone.matchers.Matchers.allOf;
+import static com.google.errorprone.matchers.Matchers.annotations;
+import static com.google.errorprone.matchers.Matchers.hasArgumentWithValue;
+import static com.google.errorprone.matchers.Matchers.isSameType;
+import static com.google.errorprone.matchers.Matchers.isType;
+
+import com.google.auto.service.AutoService;
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.BugPattern.LinkType;
+import com.google.errorprone.BugPattern.ProvidesFix;
+import com.google.errorprone.BugPattern.SeverityLevel;
+import com.google.errorprone.BugPattern.StandardTags;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
+import com.google.errorprone.fixes.SuggestedFix;
+import com.google.errorprone.matchers.Description;
+import com.google.errorprone.matchers.MultiMatcher;
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.source.tree.AnnotationTree;
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.ImportTree;
+import com.sun.source.tree.Tree;
+
+/** A {@link BugChecker} which flags classes importing Mockito, but not enforcing strict mocks. */
+@AutoService(BugChecker.class)
+@BugPattern(
+ name = "MockitoAnnotation",
+ summary = "Prefer using strict stubs with Mockito",
+ linkType = LinkType.NONE,
+ severity = SeverityLevel.SUGGESTION,
+ tags = StandardTags.STYLE,
+ providesFix = ProvidesFix.REQUIRES_HUMAN_ATTENTION)
+public final class MockitoAnnotationCheck extends BugChecker implements ClassTreeMatcher {
+ private static final long serialVersionUID = 1L;
+ private static final String MOCKITO_SETTINGS = "org.mockito.junit.jupiter.MockitoSettings";
+ private static final String STRICT_STUBS = "org.mockito.quality.Strictness.STRICT_STUBS";
+ private static final String MOCKITO_ANNOTATION = "@MockitoSettings(strictness = STRICT_STUBS)";
+ private static final MultiMatcher HAS_STRICT_STUBS_ANNOTATION =
+ annotations(
+ AT_LEAST_ONE,
+ allOf(
+ isType(MOCKITO_SETTINGS),
+ hasArgumentWithValue("strictness", isSameType(STRICT_STUBS))));
+
+ @Override
+ public Description matchClass(ClassTree clazz, VisitorState state) {
+ if (ASTHelpers.findEnclosingNode(state.getPath(), ClassTree.class) != null
+ || HAS_STRICT_STUBS_ANNOTATION.matches(clazz, state)
+ || !importsMockito(state)) {
+ return NO_MATCH;
+ }
+
+ return describeMatch(clazz, buildFix(clazz));
+ }
+
+ private static boolean importsMockito(VisitorState state) {
+ return state.getPath().getCompilationUnit().getImports().stream()
+ .map(ImportTree::getQualifiedIdentifier)
+ .map(Object::toString)
+ .anyMatch(importLine -> importLine.startsWith("org.mockito"));
+ }
+
+ private static SuggestedFix buildFix(ClassTree clazz) {
+ return SuggestedFix.builder()
+ .addImport(MOCKITO_SETTINGS)
+ .addStaticImport(STRICT_STUBS)
+ .prefixWith(clazz, MOCKITO_ANNOTATION)
+ .build();
+ }
+}
diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MockitoAnnotationCheckTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MockitoAnnotationCheckTest.java
new file mode 100644
index 0000000000..49303e23d3
--- /dev/null
+++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MockitoAnnotationCheckTest.java
@@ -0,0 +1,71 @@
+package tech.picnic.errorprone.bugpatterns;
+
+import com.google.errorprone.BugCheckerRefactoringTestHelper;
+import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
+import com.google.errorprone.CompilationTestHelper;
+import org.junit.jupiter.api.Test;
+
+public final class MockitoAnnotationCheckTest {
+ private final CompilationTestHelper compilationTestHelper =
+ CompilationTestHelper.newInstance(MockitoAnnotationCheck.class, getClass());
+ private final BugCheckerRefactoringTestHelper refactoringTestHelper =
+ BugCheckerRefactoringTestHelper.newInstance(new MockitoAnnotationCheck(), getClass());
+
+ @Test
+ public void testIdentification() {
+ compilationTestHelper
+ .addSourceLines(
+ "A.java",
+ "import static org.mockito.Mockito.mock;",
+ "",
+ "import org.junit.jupiter.api.Tag;",
+ "import org.junit.jupiter.api.Test;",
+ "",
+ "@Tag(\"unit\")",
+ "// BUG: Diagnostic contains:",
+ "class MockitoTest {",
+ " @Test",
+ " void mockitoTest() {",
+ " mock(String.class);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testReplacement() {
+ refactoringTestHelper
+ .addInputLines(
+ "in/A.java",
+ "import static org.mockito.Mockito.mock;",
+ "",
+ "import org.junit.jupiter.api.Tag;",
+ "import org.junit.jupiter.api.Test;",
+ "",
+ "@Tag(\"unit\")",
+ "class MockitoTest {",
+ " @Test",
+ " void mockitoTest() {",
+ " mock(String.class);",
+ " }",
+ "}")
+ .addOutputLines(
+ "out/A.java",
+ "import static org.mockito.Mockito.mock;",
+ "import static org.mockito.quality.Strictness.STRICT_STUBS;",
+ "",
+ "import org.junit.jupiter.api.Tag;",
+ "import org.junit.jupiter.api.Test;",
+ "import org.mockito.junit.jupiter.MockitoSettings;",
+ "",
+ "@MockitoSettings(strictness = STRICT_STUBS)",
+ "@Tag(\"unit\")",
+ "class MockitoTest {",
+ " @Test",
+ " void mockitoTest() {",
+ " mock(String.class);",
+ " }",
+ "}")
+ .doTest(TestMode.TEXT_MATCH);
+ }
+}
diff --git a/pom.xml b/pom.xml
index c3e3833dec..8cc1c2f06a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -296,6 +296,11 @@
mockito-core
${version.mockito}
+
+ org.mockito
+ mockito-junit-jupiter
+ ${version.mockito}
+
org.reactivestreams
reactive-streams