Created a custom AndroidLint check that statically analyzes a module's codebase for APIs. am: 276df84e5b am: a12fdb7f28

Original change: https://googleplex-android-review.googlesource.com/c/platform/tools/apifinder/+/12275519

Change-Id: Iebf9df7f26028e066f18bb6889ee5862d840b075
diff --git a/Android.bp b/Android.bp
index 7df7356..4db5e7f 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,4 +1,4 @@
-// Copyright (C) 2019 The Android Open Source Project
+// Copyright (C) 2020 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -60,3 +60,29 @@
     path: "src/test/res",
     srcs: ["src/test/res/**/*.java"],
 }
+
+// ------------------------- AndroidLint Checkers ----------------------------------
+
+java_library_host {
+    name: "JavaKotlinApiFinder",
+    srcs: ["checks/src/main/java/**/*.kt"],
+    plugins: ["auto_service_plugin"],
+    libs: [
+        "auto_service_annotations",
+        "lint_api",
+    ],
+}
+
+// TODO: (b/162368644) Implement these (working in gradle) Kotlin Tests to run on Soong
+//java_test_host {
+//    name: "JavaKotlinApiFinderTest",
+//    srcs: [
+//    	"checks/src/test/java/**/*.kt",
+//    	"checks/src/main/java/**/*.kt",
+//    ],
+//    plugins: ["auto_service_plugin"],
+//    static_libs: [
+//        "auto_service_annotations",
+//        "lint_api",
+//    ],
+//}
diff --git a/checks/src/main/java/com/android/apifinder/ApiFinderDetector.kt b/checks/src/main/java/com/android/apifinder/ApiFinderDetector.kt
new file mode 100644
index 0000000..1c72a83
--- /dev/null
+++ b/checks/src/main/java/com/android/apifinder/ApiFinderDetector.kt
@@ -0,0 +1,98 @@
+package com.android.apifinder
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.*
+import com.intellij.lang.jvm.JvmModifier
+import com.intellij.psi.*
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.USimpleNameReferenceExpression
+
+@Suppress("UnstableApiUsage")
+class ApiFinderDetector : Detector(), Detector.UastScanner {
+
+    override fun getApplicableUastTypes(): List<Class<out UElement>>? =
+      listOf(UCallExpression::class.java, USimpleNameReferenceExpression::class.java,
+             UMethod::class.java)
+
+    override fun createUastHandler(context: JavaContext): UElementHandler? =
+      object : UElementHandler() {
+          // Visits all methods that the module itself has defined in the source code.
+          override fun visitMethod(node: UMethod) {
+              val method = node.sourcePsi as? PsiMethod ?: return
+              visitGenericMethod(method, node, isModuleMethod = true)
+          }
+
+          // Visits all method call expressions in the source code.
+          override fun visitCallExpression(node: UCallExpression) {
+              val method = node.resolve() ?: return
+              visitGenericMethod(method, node)
+          }
+
+          // When Kotlin code refers to a Java `getFoo()` or `setFoo()` method with property syntax
+          // * (`obj.foo`), "foo" is represented by a [USimpleNameReferenceExpression].
+          // This ensures that we visit these method calls as well.
+          override fun visitSimpleNameReferenceExpression(node: USimpleNameReferenceExpression) {
+              val method = node.resolve() as? PsiMethod ?: return
+              visitGenericMethod(method, node)
+          }
+
+          private fun visitGenericMethod(
+              method: PsiMethod, node: UElement, isModuleMethod: Boolean = false
+          ) {
+              // Exclude non-public calls.
+              if (!method.hasModifier(JvmModifier.PUBLIC)) {
+                  return
+              }
+              var containingClass = method.containingClass
+              while (containingClass != null) {
+                  if (!containingClass.hasModifier(JvmModifier.PUBLIC)) {
+                      return
+                  }
+                  containingClass = containingClass.containingClass
+              }
+
+              // Construct message as enclosingClassName.className.methodName(parameterNames).
+              // e.g. <com.android.server.wifi>.<WifiNetworkFactory>.<hasConnectionRequests>()
+              val className = method.containingClass!!.qualifiedName!!
+              val methodName = if (method.isConstructor) {
+                  val containingClasses = mutableListOf<PsiClass>()
+                  containingClass = method.containingClass
+                  while (containingClass != null) {
+                      containingClasses += containingClass
+                      containingClass = containingClass.containingClass
+                  }
+                  containingClasses.asReversed().joinToString(".") { it.name!! }
+              } else {
+                  method.name
+              }
+              val parameterNames = method.parameterList.parameters.map {
+                  it.type.canonicalText.replace(Regex("<.*>"), "")
+              }.joinToString(", ")
+
+              val methodCall = "$className.$methodName($parameterNames)"
+              if (isModuleMethod) {
+                  val message = "ModuleMethod:" + methodCall
+                  context.report(ISSUE, node, context.getLocation(node), message)
+              } else {
+                  val message = "Method:" + methodCall
+                  context.report(ISSUE, context.getLocation(node), message)
+              }
+          }
+      }
+
+    companion object {
+        @JvmField
+        val ISSUE = Issue.create(
+          id = "JavaKotlinApiUsedByModule",
+          briefDescription = "API used by Android module",
+          explanation = "Any public/protected items used by an Android module.",
+          category = Category.TESTING,
+          priority = 6,
+          severity = Severity.INFORMATIONAL,
+          implementation = Implementation(ApiFinderDetector::class.java, Scope.JAVA_FILE_SCOPE)
+        )
+    }
+}
+
diff --git a/checks/src/main/java/com/android/apifinder/ApiFinderIssueRegistry.kt b/checks/src/main/java/com/android/apifinder/ApiFinderIssueRegistry.kt
new file mode 100644
index 0000000..f2f91e2
--- /dev/null
+++ b/checks/src/main/java/com/android/apifinder/ApiFinderIssueRegistry.kt
@@ -0,0 +1,12 @@
+package com.android.apifinder
+
+import com.android.tools.lint.client.api.IssueRegistry
+import com.google.auto.service.AutoService
+
+@AutoService(IssueRegistry::class)
+@Suppress("UnstableApiUsage")
+class ApiFinderIssueRegistry : IssueRegistry() {
+    override val api: Int
+        get() = 7
+    override val issues = listOf(ApiFinderDetector.ISSUE)
+}
diff --git a/checks/src/test/java/com/android/apifinder/ApiFinderDetectorTest.kt b/checks/src/test/java/com/android/apifinder/ApiFinderDetectorTest.kt
new file mode 100644
index 0000000..397fe07
--- /dev/null
+++ b/checks/src/test/java/com/android/apifinder/ApiFinderDetectorTest.kt
@@ -0,0 +1,104 @@
+package com.android.apifinder
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+@Suppress("UnstableApiUsage")
+class ApiFinderDetectorTest : LintDetectorTest() {
+    fun testJava() {
+        lint()
+            .files(
+                java(
+// TODO: Remove the explicit constructors once UCallExpression.resolve() can resolve generated
+//  default constructors in Java.
+                    """
+package com.android.apifinder;
+
+public class TestClass {
+    public class PublicSubclass {
+        public PublicSubclass() {}
+        public void publicMethod() {}
+        private void privateMethod() {}
+    }
+
+    private class PrivateSubclass {
+        public PrivateSubclass() {}
+        public void publicMethod() {}
+    }
+
+    public void testMethod() {
+        PublicSubclass publicSubclass = new PublicSubclass();
+        publicSubclass.publicMethod();
+        publicSubclass.privateMethod();
+        PrivateSubclass privateSubclass = new PrivateSubclass();
+        privateSubclass.publicMethod();
+    }
+}
+                    """
+                ).indented()
+            )
+            .run()
+            .expect(
+                """
+src/com/android/apifinder/TestClass.java:16: Information: ModuleMethod:com.android.apifinder.TestClass.PublicSubclass.TestClass.PublicSubclass() [JavaKotlinApiUsedByModule]
+        PublicSubclass publicSubclass = new PublicSubclass();
+                                        ~~~~~~~~~~~~~~~~~~~~
+src/com/android/apifinder/TestClass.java:17: Information: ModuleMethod:com.android.apifinder.TestClass.PublicSubclass.publicMethod() [JavaKotlinApiUsedByModule]
+        publicSubclass.publicMethod();
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+0 errors, 2 warnings
+                """
+            )
+    }
+
+    fun testKotlin() {
+        lint()
+            .files(
+                kotlin(
+                    """
+package com.android.apifinder
+
+class TestClass {
+    class PublicSubclass {
+        fun publicMethod() {}
+        private fun privateMethod() {}
+    }
+
+    private class PrivateSubclass {
+        fun publicMethod() {}
+    }
+
+    fun testMethod() {
+        val publicSubclass = PublicSubclass()
+        publicSubclass.publicMethod()
+        publicSubclass.privateMethod()
+        val privateSubclass = PrivateSubclass()
+        privateSubclass.publicMethod()
+    }
+}
+                    """
+                ).indented()
+            )
+            .run()
+            .expect(
+                """
+src/com/android/apifinder/TestClass.kt:14: Information: ModuleMethod:com.android.apifinder.TestClass.PublicSubclass.TestClass.PublicSubclass() [JavaKotlinApiUsedByModule]
+        val publicSubclass = PublicSubclass()
+                             ~~~~~~~~~~~~~~~~
+src/com/android/apifinder/TestClass.kt:15: Warning: ModuleMethod:com.android.apifinder.TestClass.PublicSubclass.publicMethod() [JavaKotlinApiUsedByModule]
+        publicSubclass.publicMethod()
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+0 errors, 2 warnings
+                """
+            )
+    }
+
+    override fun getDetector(): Detector {
+        return ApiFinderDetector()
+    }
+
+    override fun getIssues(): List<Issue> {
+        return listOf(ApiFinderDetector.ISSUE)
+    }
+}