Added test to verify that all LeakCanary and Shark top-level classes have correct visibility modifiers.
diff --git a/leakcanary-android-core/src/test/java/leakcanary/VisibilityTest.kt b/leakcanary-android-core/src/test/java/leakcanary/VisibilityTest.kt
new file mode 100644
index 0000000..5d496e6
--- /dev/null
+++ b/leakcanary-android-core/src/test/java/leakcanary/VisibilityTest.kt
@@ -0,0 +1,153 @@
+package leakcanary
+
+import org.assertj.core.api.Assertions.fail
+import org.junit.Test
+import java.io.File
+import java.io.IOException
+import kotlin.reflect.KClass
+import kotlin.reflect.KVisibility.PUBLIC
+import kotlin.reflect.jvm.jvmName
+
+class VisibilityTest {
+
+ private val publicClasses = listOf(
+ "leakcanary.OnHeapAnalyzedListener",
+ "leakcanary.LeakCanary",
+ "leakcanary.DefaultOnHeapAnalyzedListener",
+ "leakcanary.AppWatcher",
+ "leakcanary.Clock",
+ "leakcanary.ObjectWatcher",
+ "leakcanary.OnObjectRetainedListener",
+ "leakcanary.KeyedWeakReference",
+ "leakcanary.GcTrigger",
+ "leakcanary.AndroidLeakFixes",
+ "shark.SharkLog",
+ "shark.HprofWriterHelper",
+ "shark.HprofWriter",
+ "shark.HprofRecord",
+ "shark.Hprof",
+ "shark.HprofPrimitiveArrayStripper",
+ "shark.PrimitiveType",
+ "shark.ValueHolder",
+ "shark.OnHprofRecordListener",
+ "shark.GcRoot",
+ "shark.HprofReader",
+ "shark.HeapDumpRule",
+ "shark.JvmTestHeapDumper",
+ "shark.AndroidBuildMirror",
+ "shark.AndroidObjectInspectors",
+ "shark.AndroidMetadataExtractor",
+ "shark.AndroidReferenceMatchers",
+ "shark.AndroidResourceIdNames",
+ "shark.Leak",
+ "shark.FilteringLeakingObjectFinder",
+ "shark.HeapAnalysisFailure",
+ "shark.LeakTraceReference",
+ "shark.HeapAnalysisException",
+ "shark.OnAnalysisProgressListener",
+ "shark.ApplicationLeak",
+ "shark.MetadataExtractor",
+ "shark.ObjectInspector",
+ "shark.IgnoredReferenceMatcher",
+ "shark.HeapAnalysisSuccess",
+ "shark.LibraryLeakReferenceMatcher",
+ "shark.ObjectReporter",
+ "shark.LibraryLeak",
+ "shark.LeakTrace",
+ "shark.HeapAnalysis",
+ "shark.LeakTraceObject",
+ "shark.ReferencePattern",
+ "shark.HeapAnalyzer",
+ "shark.ObjectInspectors",
+ "shark.KeyedWeakReferenceFinder",
+ "shark.ReferenceMatcher",
+ "shark.AppSingletonInspector",
+ "shark.LeakingObjectFinder",
+ "shark.HeapField",
+ "shark.HprofHeapGraph",
+ "shark.internal.hppc.LongLongScatterMap",
+ "shark.internal.hppc.LongScatterSet",
+ "shark.GraphContext",
+ "shark.HeapValue",
+ "shark.HeapObject",
+ "shark.ProguardMapping",
+ "shark.ProguardMappingReader",
+ "shark.HeapGraph"
+ )
+
+
+ /**
+ * Validates that each field in [LeakCanary.Config] has a matching builder function
+ * in [LeakCanary.Config.Builder]
+ */
+ @Test fun `Everything is internal by default unless explicitly public`() {
+ (getClasses("leakcanary") + getClasses("shark"))
+ .asSequence()
+ .filter { !it.jvmName.contains("\\$".toRegex()) } // Any nested classes
+ .filter { !it.jvmName.contains("\\\$WhenMappings".toRegex()) } // When mappings
+ .filter { !it.jvmName.endsWith("Kt") } // ClassNameKt classes
+ .filter { !it.jvmName.endsWith("Test") } // Test classes
+ .filter { it.visibility == PUBLIC }
+ .forEach {
+ if (!publicClasses.contains(it.jvmName)) {
+ fail("Class ${it.jvmName} was declared as PUBLIC, but verification rules " +
+ "don't include this class. Update VisibilityTest if you want to keep " +
+ "this new class PUBLIC, or convert it to be INTERNAL.")
+ }
+ }
+ }
+
+
+
+ /**
+ * Scans all classes accessible from the context class loader which belong to the given package and subpackages.
+ *
+ * @param packageName The base package
+ * @return The classes
+ * @throws ClassNotFoundException
+ * @throws IOException
+ */
+ private fun getClasses(packageName: String): List<KClass<*>> {
+ val classLoader = javaClass.classLoader!!
+ val path = packageName.replace('.', '/')
+ val resources = classLoader.getResources(path)
+ val dirs = mutableListOf<File>()
+ while (resources.hasMoreElements()) {
+ dirs.add(File(resources.nextElement().file))
+ }
+ return dirs.map { findClasses(it, packageName) }.flatten()
+ }
+
+ /**
+ * Recursive method used to find all classes in a given directory and subdirs.
+ *
+ * @param directory The base directory
+ * @param packageName The package name for classes found inside the base directory
+ * @return The classes
+ * @throws ClassNotFoundException
+ */
+ private fun findClasses(
+ directory: File,
+ packageName: String
+ ): List<KClass<*>> {
+ val classes = mutableListOf<KClass<*>>()
+ if (!directory.exists()) {
+ return classes
+ }
+ val files = directory.listFiles() ?: return classes
+ files.forEach { file ->
+ if (file.isDirectory) {
+ classes.addAll(findClasses(file, packageName + "." + file.name))
+ } else if (file.name.endsWith(".class")) {
+ // TODO add filtering for $1 in class name here?
+ classes.add(
+ Class.forName(
+ "$packageName.${file.name.substring(0, file.name.length - 6)}",
+ false,
+ javaClass.classLoader!!).kotlin
+ )
+ }
+ }
+ return classes
+ }
+}
\ No newline at end of file