Add argument for base API during compat checks

When feeding metalava a partial codebase as source, there is currently
no way to provide it with the "rest" of the codebase for purposes of
compat tracking. Add an argument for that.

Bug: 174847574
Test: m frameworks-base-api-system-current-compat
Change-Id: I32f245921155ce55a1146b56e98dcafae6fc223a
diff --git a/src/main/java/com/android/tools/metalava/Driver.kt b/src/main/java/com/android/tools/metalava/Driver.kt
index cc1dded..a826dc0 100644
--- a/src/main/java/com/android/tools/metalava/Driver.kt
+++ b/src/main/java/com/android/tools/metalava/Driver.kt
@@ -602,6 +602,12 @@
                 kotlinStyleNulls = options.inputKotlinStyleNulls
             )
         } else if (!options.showUnannotated || apiType != ApiType.PUBLIC_API) {
+            if (options.baseApiForCompatCheck != null) {
+                // This option does not make sense with showAnnotation, as the "base" in that case
+                // is the non-annotated APIs.
+                throw DriverException(ARG_CHECK_COMPATIBILITY_BASE_API +
+                    " is not compatible with --showAnnotation.")
+            }
             val apiFile = apiType.getSignatureFile(codebase, "compat-check-signatures-$apiType")
 
             // Fast path: if the signature files are identical, we're already good!
@@ -622,6 +628,14 @@
                 return
             }
 
+            val baseApiFile = options.baseApiForCompatCheck
+            if (baseApiFile != null) {
+                base = SignatureFileLoader.load(
+                    file = baseApiFile,
+                    kotlinStyleNulls = options.inputKotlinStyleNulls
+                )
+            }
+
             codebase
         }
 
diff --git a/src/main/java/com/android/tools/metalava/Options.kt b/src/main/java/com/android/tools/metalava/Options.kt
index 850d8f4..52bd926 100644
--- a/src/main/java/com/android/tools/metalava/Options.kt
+++ b/src/main/java/com/android/tools/metalava/Options.kt
@@ -93,6 +93,7 @@
 const val ARG_CHECK_COMPATIBILITY_API_RELEASED = "--check-compatibility:api:released"
 const val ARG_CHECK_COMPATIBILITY_REMOVED_CURRENT = "--check-compatibility:removed:current"
 const val ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED = "--check-compatibility:removed:released"
+const val ARG_CHECK_COMPATIBILITY_BASE_API = "--check-compatibility:base"
 const val ARG_ALLOW_COMPATIBLE_DIFFERENCES = "--allow-compatible-differences"
 const val ARG_NO_NATIVE_DIFF = "--no-native-diff"
 const val ARG_INPUT_KOTLIN_NULLS = "--input-kotlin-nulls"
@@ -500,6 +501,9 @@
     /** The list of compatibility checks to run */
     val compatibilityChecks: List<CheckRequest> = mutableCompatibilityChecks
 
+    /** The API to use a base for the otherwise checked API during compat checks. */
+    var baseApiForCompatCheck: File? = null
+
     /**
      * When checking signature files, whether compatible differences in signature
      * files are allowed. This is normally not allowed (since it means the next
@@ -1100,6 +1104,11 @@
                     mutableCompatibilityChecks.add(CheckRequest(file, ApiType.REMOVED, ReleaseType.RELEASED))
                 }
 
+                ARG_CHECK_COMPATIBILITY_BASE_API -> {
+                    val file = stringToExistingFile(getValue(args, ++index))
+                    baseApiForCompatCheck = file
+                }
+
                 ARG_ALLOW_COMPATIBLE_DIFFERENCES -> allowCompatibleDifferences = true
                 ARG_NO_NATIVE_DIFF -> noNativeDiff = true
 
@@ -2384,6 +2393,11 @@
                 "released API, respectively. Different compatibility checks apply in the two scenarios. " +
                 "For example, to check the code base against the current public API, use " +
                 "$ARG_CHECK_COMPATIBILITY:api:current.",
+            "$ARG_CHECK_COMPATIBILITY_BASE_API <file>", "When performing a compat check, use the provided signature " +
+                "file as a base api, which is treated as part of the API being checked. This allows us to compute the " +
+                "full API surface from a partial API surface (e.g. the current @SystemApi txt file), which allows us to " +
+                "recognize when an API is moved from the partial API to the base API and avoid incorrectly flagging this " +
+                "as an API removal.",
             "$ARG_API_LINT [api file]", "Check API for Android API best practices. If a signature file is " +
                 "provided, only the APIs that are new since the API will be checked.",
             "$ARG_API_LINT_IGNORE_PREFIX [prefix]", "A list of package prefixes to ignore API issues in " +
diff --git a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt
index cabb328..d61a6a7 100644
--- a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt
+++ b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt
@@ -2258,6 +2258,39 @@
     }
 
     @Test
+    fun `Test check release with base api`() {
+        check(
+            expectedIssues = "",
+            checkCompatibilityApiReleased = """
+                package test.pkg {
+                  public class SomeClass {
+                      method public static void publicMethodA();
+                      method public static void publicMethodB();
+                  }
+                }
+                """,
+            sourceFiles = arrayOf(
+                java(
+                    """
+                    package test.pkg;
+
+                    public class SomeClass {
+                      public static void publicMethodA();
+                    }
+                    """
+                )
+            ),
+            checkCompatibilityBaseApi = """
+                package test.pkg {
+                  public class SomeClass {
+                      method public static void publicMethodB();
+                  }
+                }
+            """
+        )
+    }
+
+    @Test
     fun `Implicit nullness`() {
         check(
             compatibilityMode = false,
diff --git a/src/test/java/com/android/tools/metalava/DriverTest.kt b/src/test/java/com/android/tools/metalava/DriverTest.kt
index b7f99b3..21b9c70 100644
--- a/src/test/java/com/android/tools/metalava/DriverTest.kt
+++ b/src/test/java/com/android/tools/metalava/DriverTest.kt
@@ -311,6 +311,9 @@
         /** An optional API signature to check the last released removed API's compatibility with */
         @Language("TEXT")
         checkCompatibilityRemovedApiReleased: String? = null,
+        /** An optional API signature to use as the base API codebase during compat checks */
+        @Language("TEXT")
+        checkCompatibilityBaseApi: String? = null,
         /** An optional API signature to compute nullness migration status from */
         allowCompatibleDifferences: Boolean = true,
         @Language("TEXT")
@@ -658,6 +661,19 @@
             null
         }
 
+        val checkCompatibilityBaseApiFile = if (checkCompatibilityBaseApi != null) {
+            val maybeFile = File(checkCompatibilityBaseApi)
+            if (maybeFile.isFile) {
+                maybeFile
+            } else {
+                val file = File(project, "compatibility-base-api.txt")
+                file.writeText(checkCompatibilityBaseApi.trimIndent())
+                file
+            }
+        } else {
+            null
+        }
+
         val migrateNullsApiFile = if (migrateNullsApi != null) {
             val jar = File(migrateNullsApi)
             if (jar.isFile) {
@@ -702,6 +718,12 @@
             emptyArray()
         }
 
+        val checkCompatibilityBaseApiArguments = if (checkCompatibilityBaseApiFile != null) {
+            arrayOf(ARG_CHECK_COMPATIBILITY_BASE_API, checkCompatibilityBaseApiFile.path)
+        } else {
+            emptyArray()
+        }
+
         val checkCompatibilityRemovedCurrentArguments = if (checkCompatibilityRemovedApiCurrentFile != null) {
             val extra: Array<String> = if (allowCompatibleDifferences) {
                 arrayOf(ARG_ALLOW_COMPATIBLE_DIFFERENCES)
@@ -1146,6 +1168,7 @@
             *migrateNullsArguments,
             *checkCompatibilityArguments,
             *checkCompatibilityApiReleasedArguments,
+            *checkCompatibilityBaseApiArguments,
             *checkCompatibilityRemovedCurrentArguments,
             *checkCompatibilityRemovedReleasedArguments,
             *proguardKeepArguments,
diff --git a/src/test/java/com/android/tools/metalava/OptionsTest.kt b/src/test/java/com/android/tools/metalava/OptionsTest.kt
index fcb4770..6797b24 100644
--- a/src/test/java/com/android/tools/metalava/OptionsTest.kt
+++ b/src/test/java/com/android/tools/metalava/OptionsTest.kt
@@ -262,6 +262,13 @@
                                              publicly released API, respectively. Different compatibility checks apply
                                              in the two scenarios. For example, to check the code base against the
                                              current public API, use --check-compatibility:api:current.
+--check-compatibility:base <file>
+                                             When performing a compat check, use the provided signature file as a base
+                                             api, which is treated as part of the API being checked. This allows us to
+                                             compute the full API surface from a partial API surface (e.g. the current
+                                             @SystemApi txt file), which allows us to recognize when an API is moved
+                                             from the partial API to the base API and avoid incorrectly flagging this as
+                                             an API removal.
 --api-lint [api file]
                                              Check API for Android API best practices. If a signature file is provided,
                                              only the APIs that are new since the API will be checked.