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