Repository: mockito/mockito
Issue: Add subproject for errorprone Mockito plugins
Labels: enhancement
new feature

We heavily rely on ErrorProne plugins to prohibit certain usage of Mockito. Examples include disallowing the usages of Mockito internals or extending interfaces annotated by `@NotExtensible`.

These plugins are beneficial to the Java community and we would like to open source them to the project. The extra subproject would contain several checkers (and tests) that implement several safeguards. Users can add the checkers to their project to their benefit.

For example for Gradle, the configuration would be as follows (using https://github.com/tbroyer/gradle-errorprone-plugin#custom-error-prone-checks):

```gradle
dependencies {
  errorprone("org.mockito:mockito-errorprone:$mockitoVersion")
}
```

This would allow us to make issues like #1616 and #1613 compiler errors rather than runtime failures.

Issue comments:

Pull request review comments:
I think the JUnit dependency should be kept at 4.12, we should avoid depending on a beta version.
Shall we keep a lower target, to allow benefit for older platforms ?
ErrorProne must run on Java 8: https://errorprone.info/docs/installation

Edit: Although we could bump the target compatibility, I would rather not deal with that.
The checker API requires 4.13 sadly. However, I will revert it to just run 4.13 for the tests on this project. 4.13 will be released next week.
OK that makes sense ;)
OK waiting for junit 4.13 then
I'm OK shipping with 4.13 beta because this new jar is completely decoupled from other jars. Hope that helps!

Patch that resolved the issue:
diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle
index f164631907..ed5f763cbe 100644
--- a/gradle/dependencies.gradle
+++ b/gradle/dependencies.gradle
@@ -6,6 +6,7 @@ def versions = [:]
 
 versions.bytebuddy = '1.9.7'
 versions.junitJupiter = '5.1.1'
+versions.errorprone = '2.3.2'
 
 libraries.junit4 = 'junit:junit:4.12'
 libraries.junitJupiterApi = "org.junit.jupiter:junit-jupiter-api:${versions.junitJupiter}"
@@ -18,7 +19,8 @@ libraries.bytebuddy = "net.bytebuddy:byte-buddy:${versions.bytebuddy}"
 libraries.bytebuddyagent = "net.bytebuddy:byte-buddy-agent:${versions.bytebuddy}"
 libraries.bytebuddyandroid = "net.bytebuddy:byte-buddy-android:${versions.bytebuddy}"
 
-libraries.errorprone = 'com.google.errorprone:error_prone_core:2.3.2'
+libraries.errorprone = "com.google.errorprone:error_prone_core:${versions.errorprone}"
+libraries.errorproneTestApi = "com.google.errorprone:error_prone_test_helpers:${versions.errorprone}"
 
 // objenesis 3.x with full Java 11 support requires Java 8, bump version when Java 7 support is dropped
 libraries.objenesis = 'org.objenesis:objenesis:2.6'
diff --git a/gradle/root/java-compatibility-check.gradle b/gradle/root/java-compatibility-check.gradle
index c7ad1f602a..fb2a1ca002 100644
--- a/gradle/root/java-compatibility-check.gradle
+++ b/gradle/root/java-compatibility-check.gradle
@@ -2,10 +2,11 @@
 //if we're skipping release, let's also skip checking compatibility (faster builds)
 if (project.hasProperty('checkJavaCompatibility') && !System.getenv("SKIP_RELEASE")) {
     allprojects { p ->
+        def isJava8 = p.name != 'junit-jupiter' && p.name != 'errorprone'
         plugins.withId('java') {
             p.apply plugin: 'ru.vyarus.animalsniffer'
             p.dependencies {
-                signature "org.codehaus.mojo.signature:java1${p.name != 'junit-jupiter' ? '6' : '8'}:1.0@signature"
+                signature "org.codehaus.mojo.signature:java1${isJava8 ? '6' : '8'}:1.0@signature"
             }
         }
     }
diff --git a/settings.gradle b/settings.gradle
index 34ffa162a1..37f0a5d799 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -7,6 +7,7 @@ include 'android'
 include 'junit-jupiter'
 include 'junitJupiterExtensionTest'
 include 'module-test'
+include 'errorprone'
 
 rootProject.name = 'mockito'
 
diff --git a/subprojects/errorprone/errorprone.gradle b/subprojects/errorprone/errorprone.gradle
new file mode 100644
index 0000000000..5f97da1c22
--- /dev/null
+++ b/subprojects/errorprone/errorprone.gradle
@@ -0,0 +1,23 @@
+description = "ErrorProne plugins for Mockito"
+
+apply from: "$rootDir/gradle/java-library.gradle"
+apply from: "$rootDir/gradle/dependencies.gradle"
+
+dependencies {
+    compile project.rootProject
+    compile libraries.errorprone
+
+    testCompile 'junit:junit:4.13-beta-1'
+    testCompile libraries.errorproneTestApi
+}
+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+test {
+    inputs.files(configurations.errorproneJavac).withNormalizer(ClasspathNormalizer)
+    jvmArgs += "-Xbootclasspath/p:${configurations.errorproneJavac.asPath}"
+
+    // ErrorProne can only run on JDK 8
+    it.enabled = !JavaVersion.current().isJava9Compatible()
+}
diff --git a/subprojects/errorprone/src/main/java/org/mockito/errorprone/bugpatterns/MockitoNotExtensible.java b/subprojects/errorprone/src/main/java/org/mockito/errorprone/bugpatterns/MockitoNotExtensible.java
new file mode 100644
index 0000000000..95f18dc296
--- /dev/null
+++ b/subprojects/errorprone/src/main/java/org/mockito/errorprone/bugpatterns/MockitoNotExtensible.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2017 Mockito contributors
+ * This program is made available under the terms of the MIT License.
+ */
+package org.mockito.errorprone.bugpatterns;
+
+import static com.google.errorprone.matchers.Description.NO_MATCH;
+
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.BugPattern.SeverityLevel;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
+import com.google.errorprone.matchers.Description;
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.source.tree.ClassTree;
+
+/** Finds subclasses of @NotExtensible interfaces. */
+@BugPattern(
+    name = "MockitoNotExtensible",
+    summary = "Some types that are a part of Mockito public API are not intended to be extended.",
+    explanation =
+        "Some types that are a part of Mockito public API are not intended to be extended."
+            + " It's because Mockito team needs to be able to add new methods to some types"
+            + " without breaking compatibility contract. Any type that is not intended"
+            + " to be extended is annotated with @NotExtensible.",
+    severity = SeverityLevel.ERROR)
+public class MockitoNotExtensible extends BugChecker implements ClassTreeMatcher {
+
+    @Override
+    public Description matchClass(ClassTree tree, VisitorState state) {
+        if (tree.getImplementsClause().stream()
+            .anyMatch(
+                implementing ->
+                    ASTHelpers.hasAnnotation(
+                        ASTHelpers.getSymbol(implementing), "org.mockito.NotExtensible", state))) {
+            return describeMatch(tree);
+        }
+
+        return NO_MATCH;
+    }
+}


Test patch:
diff --git a/subprojects/errorprone/src/test/java/org/mockito/errorprone/bugpatterns/MockitoNotExtensibleTest.java b/subprojects/errorprone/src/test/java/org/mockito/errorprone/bugpatterns/MockitoNotExtensibleTest.java
new file mode 100644
index 0000000000..4e0f3f7152
--- /dev/null
+++ b/subprojects/errorprone/src/test/java/org/mockito/errorprone/bugpatterns/MockitoNotExtensibleTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2017 Mockito contributors
+ * This program is made available under the terms of the MIT License.
+ */
+package org.mockito.errorprone.bugpatterns;
+
+import com.google.errorprone.CompilationTestHelper;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Verifies that there are no subclasses of {@link org.mockito.NotExtensible} Mockito types. */
+@RunWith(JUnit4.class)
+public class MockitoNotExtensibleTest {
+
+    private CompilationTestHelper compilationTestHelper;
+
+    @Before
+    public void setup() {
+        compilationTestHelper =
+            CompilationTestHelper.newInstance(MockitoNotExtensible.class, getClass());
+    }
+
+    @Test
+    public void notExtensibleSubclass_shouldWarn() {
+        compilationTestHelper
+            .addSourceLines(
+                "input.java",
+                "import org.mockito.invocation.InvocationOnMock;",
+                "// BUG: Diagnostic contains:",
+                "abstract class Invocation implements InvocationOnMock { }")
+            .doTest();
+    }
+
+    @Test
+    public void anonymousClassImplementingNotExtensible_shouldWarn() {
+        compilationTestHelper
+            .addSourceLines(
+                "input.java",
+                "import org.mockito.NotExtensible;",
+                "class Test {",
+                "  public void test() {",
+                "    // BUG: Diagnostic contains:",
+                "    new Anonymous() { };",
+                "  }",
+                "  @NotExtensible",
+                "  interface Anonymous {}",
+                "}")
+            .doTest();
+    }
+
+    @Test
+    public void anonymousClassImplementingOtherAnnotation_shouldNotWarn() {
+        compilationTestHelper
+            .addSourceLines(
+                "input.java",
+                "import org.mockito.NotExtensible;",
+                "class Test {",
+                "  public void test() {",
+                "    new Anonymous() { };",
+                "  }",
+                "  @OtherAnnotation",
+                "  interface Anonymous {}",
+                "  @interface OtherAnnotation {}",
+                "}")
+            .doTest();
+    }
+}