Add metalava support for --validate-nullability-from-list.

This supplements the existing --validate-nullability-from-merged-stubs
flag, which relies on the existence of stub files to be merged to
determine which classes should be validated, with the possibility of
explicitly naming the classes. This is needed when the classes are
annotated directly in the source, rather than via stub files.

Test: core-current-stubs-nullability-validation-check-nullability-warnings
Test: ./gradlew test
Bug: 73448108
Change-Id: Iac0cb5f74e78bda789b8b4b3e8cec141759225fc
diff --git a/src/main/java/com/android/tools/metalava/Driver.kt b/src/main/java/com/android/tools/metalava/Driver.kt
index 94c997c..6674ce5 100644
--- a/src/main/java/com/android/tools/metalava/Driver.kt
+++ b/src/main/java/com/android/tools/metalava/Driver.kt
@@ -692,6 +692,7 @@
     analyzer.mergeExternalInclusionAnnotations()
     analyzer.computeApi()
     analyzer.mergeExternalQualifierAnnotations()
+    options.nullabilityAnnotationsValidator?.validateAllFrom(codebase, options.validateNullabilityFromList)
     options.nullabilityAnnotationsValidator?.report()
     analyzer.handleStripping()
 
@@ -784,6 +785,7 @@
     analyzer.mergeExternalInclusionAnnotations()
     analyzer.computeApi()
     analyzer.mergeExternalQualifierAnnotations()
+    options.nullabilityAnnotationsValidator?.validateAllFrom(codebase, options.validateNullabilityFromList)
     options.nullabilityAnnotationsValidator?.report()
     analyzer.generateInheritedStubs(apiEmit, apiReference)
     codebase.bindingContext = trace.bindingContext
diff --git a/src/main/java/com/android/tools/metalava/NullabilityAnnotationsValidator.kt b/src/main/java/com/android/tools/metalava/NullabilityAnnotationsValidator.kt
index b3033f2..0618442 100644
--- a/src/main/java/com/android/tools/metalava/NullabilityAnnotationsValidator.kt
+++ b/src/main/java/com/android/tools/metalava/NullabilityAnnotationsValidator.kt
@@ -26,7 +26,9 @@
 import com.android.tools.metalava.model.TypeItem
 import com.android.tools.metalava.model.visitors.ApiVisitor
 import com.google.common.io.Files
+import java.io.File
 import java.io.PrintWriter
+import java.nio.charset.StandardCharsets
 
 private const val RETURN_LABEL = "return value"
 
@@ -79,7 +81,7 @@
     fun validateAll(codebase: Codebase, topLevelClassNames: List<String>) {
         for (topLevelClassName in topLevelClassNames) {
             val topLevelClass = codebase.findClass(topLevelClassName)
-                    ?: throw DriverException("External nullability annotations reference class $topLevelClassName which could not be found in main codebase")
+                    ?: throw DriverException("Trying to validate nullability annotations for class $topLevelClassName which could not be found in main codebase")
             // Visit methods to check their return type, and parameters to check them. Don't visit
             // constructors as we don't want to check their return types. This visits members of
             // inner classes as well.
@@ -96,6 +98,22 @@
         }
     }
 
+    /**
+     * As [validateAll], reading the list of class names from [topLevelClassesList]. The file names
+     * one top-level class per line, and lines starting with # are skipped. Does nothing if
+     * [topLevelClassesList] is null.
+     */
+    fun validateAllFrom(codebase: Codebase, topLevelClassesList: File?) {
+        if (topLevelClassesList != null) {
+            val classes =
+                Files.readLines(topLevelClassesList, StandardCharsets.UTF_8)
+                    .filterNot { it.isBlank() }
+                    .map { it.trim() }
+                    .filterNot { it.startsWith("#") }
+            validateAll(codebase, classes)
+        }
+    }
+
     private fun checkItem(method: MethodItem, label: String, type: TypeItem?, item: Item) {
         if (type == null) {
             throw DriverException("Missing type on $method item $label")
diff --git a/src/main/java/com/android/tools/metalava/Options.kt b/src/main/java/com/android/tools/metalava/Options.kt
index 6635234..7de777e 100644
--- a/src/main/java/com/android/tools/metalava/Options.kt
+++ b/src/main/java/com/android/tools/metalava/Options.kt
@@ -59,6 +59,7 @@
 const val ARG_MERGE_QUALIFIER_ANNOTATIONS = "--merge-qualifier-annotations"
 const val ARG_MERGE_INCLUSION_ANNOTATIONS = "--merge-inclusion-annotations"
 const val ARG_VALIDATE_NULLABILITY_FROM_MERGED_STUBS = "--validate-nullability-from-merged-stubs"
+const val ARG_VALIDATE_NULLABILITY_FROM_LIST = "--validate-nullability-from-list"
 const val ARG_NULLABILITY_WARNINGS_TXT = "--nullability-warnings-txt"
 const val ARG_NULLABILITY_ERRORS_NON_FATAL = "--nullability-errors-non-fatal"
 const val ARG_INPUT_API_JAR = "--input-api-jar"
@@ -202,6 +203,12 @@
     var validateNullabilityFromMergedStubs = false
 
     /**
+     * A file containing a list of classes whose nullability annotations should be validated. If
+     * set, [nullabilityAnnotationsValidator] must also be set.
+     */
+    var validateNullabilityFromList: File? = null
+
+    /**
      * Whether to include element documentation (javadoc and KDoc) is in the generated stubs.
      * (Copyright notices are not affected by this, they are always included. Documentation stubs
      * (--doc-stubs) are not affected.)
@@ -566,6 +573,11 @@
                     nullabilityAnnotationsValidator =
                         nullabilityAnnotationsValidator ?: NullabilityAnnotationsValidator()
                 }
+                ARG_VALIDATE_NULLABILITY_FROM_LIST -> {
+                    validateNullabilityFromList = stringToExistingFile(getValue(args, ++index))
+                    nullabilityAnnotationsValidator =
+                        nullabilityAnnotationsValidator ?: NullabilityAnnotationsValidator()
+                }
                 ARG_NULLABILITY_WARNINGS_TXT ->
                     nullabilityWarningsTxt = stringToNewFile(getValue(args, ++index))
                 ARG_NULLABILITY_ERRORS_NON_FATAL ->
@@ -1509,6 +1521,9 @@
             ARG_VALIDATE_NULLABILITY_FROM_MERGED_STUBS, "Triggers validation of nullability annotations " +
                 "for any class where $ARG_MERGE_QUALIFIER_ANNOTATIONS includes a Java stub file.",
 
+            ARG_VALIDATE_NULLABILITY_FROM_LIST, "Triggers validation of nullability annotations " +
+                "for any class listed in the named file (one top-level class per line, # prefix for comment line).",
+
             "$ARG_NULLABILITY_WARNINGS_TXT <file>", "Specifies where to write warnings encountered during " +
                 "validation of nullability annotations. (Does not trigger validation by itself.)",
 
diff --git a/src/test/java/com/android/tools/metalava/DriverTest.kt b/src/test/java/com/android/tools/metalava/DriverTest.kt
index 83133be..dc6f3e5 100644
--- a/src/test/java/com/android/tools/metalava/DriverTest.kt
+++ b/src/test/java/com/android/tools/metalava/DriverTest.kt
@@ -300,6 +300,8 @@
         extractAnnotations: Map<String, String>? = null,
         /** Creates the nullability annotations validator, and check that the report has the given lines (does not define files to be validated) */
         validateNullability: Set<String>? = null,
+        /** Enable nullability validation for the listed classes */
+        validateNullabilityFromList: String? = null,
         /**
          * Whether to include source retention annotations in the stubs (in that case they do not
          * go into the extracted annotations zip file)
@@ -825,6 +827,16 @@
             validateNullabilityTxt = null
             emptyArray()
         }
+        val validateNullablityFromListFile: File?
+        val validateNullabilityFromListArgs = if (validateNullabilityFromList != null) {
+            validateNullablityFromListFile = temporaryFolder.newFile("validate-nullability-classes.txt")
+            Files.asCharSink(validateNullablityFromListFile, Charsets.UTF_8).write(validateNullabilityFromList)
+            arrayOf(
+                ARG_VALIDATE_NULLABILITY_FROM_LIST, validateNullablityFromListFile.path
+            )
+        } else {
+            emptyArray()
+        }
 
         val signatureFormatArgs = if (format != null) {
             arrayOf("$ARG_FORMAT=v$format")
@@ -899,6 +911,7 @@
             *artifactArgs,
             *extractAnnotationsArgs,
             *validateNullabilityArgs,
+            *validateNullabilityFromListArgs,
             *signatureFormatArgs,
             *sourceList,
             *extraArguments,
diff --git a/src/test/java/com/android/tools/metalava/NullabilityAnnotationsValidatorTest.kt b/src/test/java/com/android/tools/metalava/NullabilityAnnotationsValidatorTest.kt
index e3229b5..2cbfc4d 100644
--- a/src/test/java/com/android/tools/metalava/NullabilityAnnotationsValidatorTest.kt
+++ b/src/test/java/com/android/tools/metalava/NullabilityAnnotationsValidatorTest.kt
@@ -233,4 +233,43 @@
             )
         )
     }
+
+    @Test
+    fun `Using class list`() {
+        check(
+            sourceFiles = *arrayOf(
+                java(
+                    """
+                        package test.pkg;
+
+                        import libcore.util.Nullable;
+
+                        // This will be validated. It is missing an annotation on its return type.
+                        public interface Appendable {
+                            Appendable append(@Nullable CharSequence csq) throws IOException;
+                        }
+
+                        // This is missing an annotation on its return type, but will not be validated.
+                        public interface List<T> {
+                            T get(int index);
+                        }
+                    """
+            ),
+            libcoreNullableSource
+        ),
+        compatibilityMode = false,
+        outputKotlinStyleNulls = false,
+        omitCommonPackages = false,
+        extraArguments = arrayOf(ARG_VALIDATE_NULLABILITY_FROM_MERGED_STUBS),
+        validateNullabilityFromList =
+            """
+                # a comment, then a blank line, then the class to validate
+
+                test.pkg.Appendable
+            """,
+        validateNullability = setOf(
+            "WARNING: method test.pkg.Appendable.append(CharSequence), return value, MISSING"
+        )
+    )
+  }
 }
diff --git a/src/test/java/com/android/tools/metalava/OptionsTest.kt b/src/test/java/com/android/tools/metalava/OptionsTest.kt
index ae723bb..6d34b6b 100644
--- a/src/test/java/com/android/tools/metalava/OptionsTest.kt
+++ b/src/test/java/com/android/tools/metalava/OptionsTest.kt
@@ -76,6 +76,10 @@
                                           for any class where
                                           --merge-qualifier-annotations includes a Java
                                           stub file.
+--validate-nullability-from-list          Triggers validation of nullability annotations
+                                          for any class listed in the named file (one
+                                          top-level class per line, # prefix for comment
+                                          line).
 --nullability-warnings-txt <file>         Specifies where to write warnings encountered
                                           during validation of nullability annotations.
                                           (Does not trigger validation by itself.)