Make bootclasspath_fragment hidden API package checks exhaustive

Previously, the bootclasspath_fragment's hidden_api.split_packages and
hidden_api.package_prefixes properties did not specify an exhaustive
set of packages that were provided by the fragment. They excluded
packages which were either not split or which could not be used as a
package prefix because it would match sub-packages provided by other
bootclasspath modules.

This change adds the hidden_api.single_packages list to specify those
additional packages and then uses that information to verify that any
bootclasspath_fragment that specifies at least one of split_packages,
single_packages or package_prefixes properties only contains classes
from a package that matches one of those properties. That will
prevent a module from accidentally including unexpected classes, such
as might happen when statically including a common utility library.

It also adds coverage specific versions of the properties as additional
packages are added to the art-bootclasspath-fragment when building
coverage builds.

Bug: 194063708
Test: atest signature_patterns_test
      m out/soong/hiddenapi/hiddenapi-flags.csv
      m EMMA_INSTRUMENT=true EMMA_INSTRUMENT_FRAMEWORK=true out/soong/hiddenapi/hiddenapi-flags.csv
      # Breaks without corresponding change to add android.system to
      # the art-bootclasspath-fragment.
      /usr/bin/pylint --rcfile $ANDROID_BUILD_TOP/tools/repohooks/tools/pylintrc scripts/hiddenapi/signature_patterns*.py
      pyformat -s 4 --force_quote_type single -i scripts/hiddenapi/signature_patterns*.py
Change-Id: Iddf6c59cd4dc8c36dde7943a9840ccef5794b320
diff --git a/scripts/hiddenapi/signature_patterns.py b/scripts/hiddenapi/signature_patterns.py
index 1abdef1..5a82be7 100755
--- a/scripts/hiddenapi/signature_patterns.py
+++ b/scripts/hiddenapi/signature_patterns.py
@@ -60,7 +60,22 @@
     return False
 
 
-def validate_package_prefixes(split_packages, package_prefixes):
+def validate_package_is_not_matched_by_package_prefix(package_type, pkg,
+                                                      package_prefixes):
+    package_prefix = matched_by_package_prefix_pattern(package_prefixes, pkg)
+    if package_prefix:
+        # A package prefix matches the package.
+        package_for_output = slash_package_to_dot_package(pkg)
+        package_prefix_for_output = slash_package_to_dot_package(package_prefix)
+        return [
+            f'{package_type} {package_for_output} is matched by '
+            f'package prefix {package_prefix_for_output}'
+        ]
+    return []
+
+
+def validate_package_prefixes(split_packages, single_packages,
+                              package_prefixes):
     # If there are no package prefixes then there is no possible conflict
     # between them and the split packages.
     if len(package_prefixes) == 0:
@@ -79,17 +94,16 @@
                 f'{package_prefixes_for_output}\n'
                 '    add split_packages:[] to fix')
         else:
-            package_prefix = matched_by_package_prefix_pattern(
-                package_prefixes, split_package)
-            if package_prefix:
-                # A package prefix matches a split package.
-                split_package_for_output = slash_package_to_dot_package(
-                    split_package)
-                package_prefix_for_output = slash_package_to_dot_package(
-                    package_prefix)
-                errors.append(
-                    f'split package {split_package_for_output} is matched by '
-                    f'package prefix {package_prefix_for_output}')
+            errs = validate_package_is_not_matched_by_package_prefix(
+                'split package', split_package, package_prefixes)
+            errors.extend(errs)
+
+    # Check to make sure that the single packages and package prefixes do not
+    # overlap.
+    for single_package in single_packages:
+        errs = validate_package_is_not_matched_by_package_prefix(
+            'single package', single_package, package_prefixes)
+        errors.extend(errs)
     return errors
 
 
@@ -102,21 +116,40 @@
     return errors
 
 
+def validate_single_packages(split_packages, single_packages):
+    overlaps = []
+    for single_package in single_packages:
+        if single_package in split_packages:
+            overlaps.append(single_package)
+    if overlaps:
+        indented = ''.join([f'\n    {o}' for o in overlaps])
+        return [
+            f'single_packages and split_packages overlap, please ensure the '
+            f'following packages are only present in one:{indented}'
+        ]
+    return []
+
+
 def produce_patterns_from_file(file,
                                split_packages=None,
+                               single_packages=None,
                                package_prefixes=None):
     with open(file, 'r', encoding='utf8') as f:
-        return produce_patterns_from_stream(f, split_packages, package_prefixes)
+        return produce_patterns_from_stream(f, split_packages, single_packages,
+                                            package_prefixes)
 
 
 def produce_patterns_from_stream(stream,
                                  split_packages=None,
+                                 single_packages=None,
                                  package_prefixes=None):
     split_packages = set(split_packages or [])
+    single_packages = set(single_packages or [])
     package_prefixes = list(package_prefixes or [])
     # Read in all the signatures into a list and remove any unnecessary class
     # and member names.
     patterns = set()
+    unmatched_packages = set()
     for row in dict_reader(stream):
         signature = row['signature']
         text = signature.removeprefix('L')
@@ -138,11 +171,31 @@
             # Remove inner class names.
             pieces = qualified_class_name.split('$', maxsplit=1)
             pattern = pieces[0]
-        else:
+            patterns.add(pattern)
+        elif pkg in single_packages:
             # Add a * to ensure that the pattern matches the classes in that
             # package.
             pattern = pkg + '/*'
-        patterns.add(pattern)
+            patterns.add(pattern)
+        else:
+            unmatched_packages.add(pkg)
+
+    # Remove any unmatched packages that would be matched by a package prefix
+    # pattern.
+    unmatched_packages = [
+        p for p in unmatched_packages
+        if not matched_by_package_prefix_pattern(package_prefixes, p)
+    ]
+    errors = []
+    if unmatched_packages:
+        unmatched_packages.sort()
+        indented = ''.join([
+            f'\n    {slash_package_to_dot_package(p)}'
+            for p in unmatched_packages
+        ])
+        errors.append('The following packages were unexpected, please add them '
+                      'to one of the hidden_api properties, split_packages, '
+                      f'single_packages or package_prefixes:{indented}')
 
     # Remove any patterns that would be matched by a package prefix pattern.
     patterns = [
@@ -155,7 +208,13 @@
     patterns = patterns + [f'{p}/**' for p in package_prefixes]
     # Sort the patterns.
     patterns.sort()
-    return patterns
+    return patterns, errors
+
+
+def print_and_exit(errors):
+    for error in errors:
+        print(error)
+    sys.exit(1)
 
 
 def main(args):
@@ -175,26 +234,41 @@
         '--package-prefix',
         action='append',
         help='A package prefix unique to this set of flags')
+    args_parser.add_argument(
+        '--single-package',
+        action='append',
+        help='A single package unique to this set of flags')
     args_parser.add_argument('--output', help='Generated signature prefixes')
     args = args_parser.parse_args(args)
 
     split_packages = set(
         dot_packages_to_slash_packages(args.split_package or []))
     errors = validate_split_packages(split_packages)
+    if errors:
+        print_and_exit(errors)
+
+    single_packages = list(
+        dot_packages_to_slash_packages(args.single_package or []))
+
+    errors = validate_single_packages(split_packages, single_packages)
+    if errors:
+        print_and_exit(errors)
 
     package_prefixes = dot_packages_to_slash_packages(args.package_prefix or [])
 
-    if not errors:
-        errors = validate_package_prefixes(split_packages, package_prefixes)
+    errors = validate_package_prefixes(split_packages, single_packages,
+                                       package_prefixes)
+    if errors:
+        print_and_exit(errors)
+
+    patterns = []
+    # Read in all the patterns into a list.
+    patterns, errors = produce_patterns_from_file(args.flags, split_packages,
+                                                  single_packages,
+                                                  package_prefixes)
 
     if errors:
-        for error in errors:
-            print(error)
-        sys.exit(1)
-
-    # Read in all the patterns into a list.
-    patterns = produce_patterns_from_file(args.flags, split_packages,
-                                          package_prefixes)
+        print_and_exit(errors)
 
     # Write out all the patterns.
     with open(args.output, 'w', encoding='utf8') as outputFile: