diff --git a/README.md b/README.md
index c7f400c..27ad839 100755
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ To use this product, please configure your findbugs-maven-plugin like below.
jp.co.worksap.oss
findbugs-plugin
- 0.0.2
+ 0.0.3-SNAPSHOT
@@ -27,6 +27,10 @@ To use this product, please configure your findbugs-maven-plugin like below.
# history
+## 0.0.3
+
+- added UnexpectedAccessDetector
+
## 0.0.2
- added BrokenImmutableClassDetector
diff --git a/src/main/java/jp/co/worksap/oss/findbugs/guava/UnexpectedAccessDetector.java b/src/main/java/jp/co/worksap/oss/findbugs/guava/UnexpectedAccessDetector.java
new file mode 100644
index 0000000..c78b685
--- /dev/null
+++ b/src/main/java/jp/co/worksap/oss/findbugs/guava/UnexpectedAccessDetector.java
@@ -0,0 +1,109 @@
+package jp.co.worksap.oss.findbugs.guava;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import javax.annotation.Nonnull;
+
+import org.apache.bcel.Repository;
+import org.apache.bcel.classfile.AnnotationEntry;
+import org.apache.bcel.classfile.JavaClass;
+import org.apache.bcel.classfile.Method;
+
+import edu.umd.cs.findbugs.BugInstance;
+import edu.umd.cs.findbugs.BugReporter;
+import edu.umd.cs.findbugs.BytecodeScanningDetector;
+import edu.umd.cs.findbugs.bcel.BCELUtil;
+import edu.umd.cs.findbugs.classfile.ClassDescriptor;
+import edu.umd.cs.findbugs.classfile.MethodDescriptor;
+
+/**
+ *
A detector to ensure that implementation (class in src/main/java) doesn't call
+ * package-private method in other class which is annotated by {@code @VisibleForTesting}.
+ *
+ * @author Kengo TODA (toda_k@worksap.co.jp)
+ * @see com.google.common.annotations.VisibleForTesting
+ */
+public class UnexpectedAccessDetector extends BytecodeScanningDetector {
+ @Nonnull
+ private final BugReporter bugReporter;
+
+ public UnexpectedAccessDetector(BugReporter bugReporter) {
+ this.bugReporter = checkNotNull(bugReporter);
+ }
+
+ @Override
+ public void sawOpcode(int opcode) {
+ if (! isInvoking(opcode)) {
+ return;
+ }
+
+ ClassDescriptor currentClass = getClassDescriptor();
+ ClassDescriptor invokedClass = getClassDescriptorOperand();
+ if (currentClass.equals(invokedClass)) {
+ // no need to check, because method is called by owner
+ } else if (! currentClass.getPackageName().equals(invokedClass.getPackageName())) {
+ // no need to check, because method is called by class in other package
+ } else {
+ MethodDescriptor invokedMethod = getMethodDescriptorOperand();
+
+ try {
+ verifyVisibility(invokedClass, invokedMethod);
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ /**
+ * Report if specified method is package-private and annotated by {@code @VisibleForTesting}.
+ */
+ private void verifyVisibility(ClassDescriptor invokedClass, MethodDescriptor invokedMethod) throws ClassNotFoundException {
+ JavaClass bcelClass = Repository.getRepository().loadClass(invokedClass.getDottedClassName());
+ Method bcelMethod = findMethod(bcelClass, invokedMethod);
+
+ if (checkVisibility(bcelMethod) && checkAnnotated(bcelMethod)) {
+ BugInstance bug = new BugInstance(this, "GUAVA_UNEXPECTED_ACCESS_TO_VISIBLE_FOR_TESTING", HIGH_PRIORITY)
+ .addCalledMethod(this).addClassAndMethod(this).addSourceLine(this);
+ bugReporter.reportBug(bug);
+ }
+ }
+
+ private Method findMethod(JavaClass bcelClass, MethodDescriptor invokedMethod) {
+ for (Method bcelMethod : bcelClass.getMethods()) {
+ MethodDescriptor methodDescriptor = BCELUtil.getMethodDescriptor(bcelClass, bcelMethod);
+ if (methodDescriptor.equals(invokedMethod)) {
+ return bcelMethod;
+ }
+ }
+
+ throw new IllegalArgumentException("Method not found: " + invokedMethod);
+ }
+
+ /**
+ * @return true if visibility of specified method is package-private.
+ */
+ private boolean checkVisibility(Method bcelMethod) {
+ return ! (bcelMethod.isPrivate() || bcelMethod.isProtected() || bcelMethod.isPublic());
+ }
+
+ /**
+ * @return true if specified method is annotated by {@code VisibleForTesting}.
+ */
+ private boolean checkAnnotated(Method bcelMethod) {
+ for (AnnotationEntry annotation : bcelMethod.getAnnotationEntries()) {
+ String type = annotation.getAnnotationType();
+ if ("Lcom/google/common/annotations/VisibleForTesting;".equals(type)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isInvoking(int opcode) {
+ return opcode == INVOKESPECIAL ||
+ opcode == INVOKEINTERFACE ||
+ opcode == INVOKESTATIC ||
+ opcode == INVOKEVIRTUAL;
+ }
+
+}
diff --git a/src/main/meta/bugrank.txt b/src/main/meta/bugrank.txt
index 5bbbeb5..cef2572 100755
--- a/src/main/meta/bugrank.txt
+++ b/src/main/meta/bugrank.txt
@@ -12,3 +12,4 @@
0 BugPattern ILLEGAL_LENGTH
0 BugPattern USE_COLUMN_DEFINITION
0 BugPattern NULLABLE_PRIMITIVE
+0 BugPattern GUAVA_UNEXPECTED_ACCESS_TO_VISIBLE_FOR_TESTING
diff --git a/src/main/meta/findbugs.xml b/src/main/meta/findbugs.xml
index 0db4367..a7885f7 100755
--- a/src/main/meta/findbugs.xml
+++ b/src/main/meta/findbugs.xml
@@ -64,4 +64,9 @@
+
+
+
diff --git a/src/main/meta/messages.xml b/src/main/meta/messages.xml
index ae73cc7..a39bac3 100755
--- a/src/main/meta/messages.xml
+++ b/src/main/meta/messages.xml
@@ -264,8 +264,32 @@
+
+
+
+
+
+
+
+ You cannot access to package-private method which is annotated by @VisibleForTesting.
+
+
+ You cannot access to package-private method which is annotated by @VisibleForTesting.
+ This annotation means that visibility was widened only for test code, so your implementation code
+ shouldn't access to method which is annotated by this annotation.
+
+
+ You cannot access to package-private method which is annotated by @VisibleForTesting.
+ This annotation means that visibility was widened only for test code, so your implementation code
+ shouldn't access to method which is annotated by this annotation.
+ ]]>
+
+
+
SYS
JSR305
JPA
JUnit
+ Google Guava
\ No newline at end of file
diff --git a/src/test/java/jp/co/worksap/oss/findbugs/guava/ClassWhichCallsNormalMethod.java b/src/test/java/jp/co/worksap/oss/findbugs/guava/ClassWhichCallsNormalMethod.java
new file mode 100644
index 0000000..169d8b7
--- /dev/null
+++ b/src/test/java/jp/co/worksap/oss/findbugs/guava/ClassWhichCallsNormalMethod.java
@@ -0,0 +1,12 @@
+package jp.co.worksap.oss.findbugs.guava;
+
+public class ClassWhichCallsNormalMethod {
+ public void method() {
+ new MethodWithoutVisibleForTesting().method();
+ }
+
+ public void anotherMethod() {
+ System.out.println("this method invoking has no problem, because package isn't same.");
+ method();
+ }
+}
diff --git a/src/test/java/jp/co/worksap/oss/findbugs/guava/ClassWhichCallsVisibleMethodForTesting.java b/src/test/java/jp/co/worksap/oss/findbugs/guava/ClassWhichCallsVisibleMethodForTesting.java
new file mode 100644
index 0000000..fbf2751
--- /dev/null
+++ b/src/test/java/jp/co/worksap/oss/findbugs/guava/ClassWhichCallsVisibleMethodForTesting.java
@@ -0,0 +1,7 @@
+package jp.co.worksap.oss.findbugs.guava;
+
+public class ClassWhichCallsVisibleMethodForTesting {
+ public void method() {
+ new MethodWithVisibleForTesting().method();
+ }
+}
diff --git a/src/test/java/jp/co/worksap/oss/findbugs/guava/MethodWithVisibleForTesting.java b/src/test/java/jp/co/worksap/oss/findbugs/guava/MethodWithVisibleForTesting.java
new file mode 100644
index 0000000..2ec1504
--- /dev/null
+++ b/src/test/java/jp/co/worksap/oss/findbugs/guava/MethodWithVisibleForTesting.java
@@ -0,0 +1,8 @@
+package jp.co.worksap.oss.findbugs.guava;
+
+import com.google.common.annotations.VisibleForTesting;
+
+public class MethodWithVisibleForTesting {
+ @VisibleForTesting
+ void method() {}
+}
diff --git a/src/test/java/jp/co/worksap/oss/findbugs/guava/MethodWithoutVisibleForTesting.java b/src/test/java/jp/co/worksap/oss/findbugs/guava/MethodWithoutVisibleForTesting.java
new file mode 100644
index 0000000..0cce7a0
--- /dev/null
+++ b/src/test/java/jp/co/worksap/oss/findbugs/guava/MethodWithoutVisibleForTesting.java
@@ -0,0 +1,5 @@
+package jp.co.worksap.oss.findbugs.guava;
+
+public class MethodWithoutVisibleForTesting {
+ void method() {}
+}
diff --git a/src/test/java/jp/co/worksap/oss/findbugs/guava/UnexpectedAccessDetectorTest.java b/src/test/java/jp/co/worksap/oss/findbugs/guava/UnexpectedAccessDetectorTest.java
new file mode 100644
index 0000000..2fee4cd
--- /dev/null
+++ b/src/test/java/jp/co/worksap/oss/findbugs/guava/UnexpectedAccessDetectorTest.java
@@ -0,0 +1,34 @@
+package jp.co.worksap.oss.findbugs.guava;
+
+import static com.youdevise.fbplugins.tdd4fb.DetectorAssert.assertBugReported;
+import static com.youdevise.fbplugins.tdd4fb.DetectorAssert.assertNoBugsReported;
+import static com.youdevise.fbplugins.tdd4fb.DetectorAssert.bugReporterForTesting;
+import static com.youdevise.fbplugins.tdd4fb.DetectorAssert.ofType;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import edu.umd.cs.findbugs.BugReporter;
+
+public class UnexpectedAccessDetectorTest {
+
+ private UnexpectedAccessDetector detector;
+ private BugReporter bugReporter;
+
+ @Before
+ public void setup() {
+ bugReporter = bugReporterForTesting();
+ detector = new UnexpectedAccessDetector(bugReporter);
+ }
+
+ @Test
+ public void testNormalMethod() throws Exception {
+ assertNoBugsReported(ClassWhichCallsNormalMethod.class, detector, bugReporter);
+ }
+
+ @Test
+ public void testCallingAnnotatedMethod() throws Exception {
+ assertBugReported(ClassWhichCallsVisibleMethodForTesting.class, detector, bugReporter, ofType("GUAVA_UNEXPECTED_ACCESS_TO_VISIBLE_FOR_TESTING"));
+ }
+
+}