Merge "enforce_permission_counter: Find targets automatically" into main
diff --git a/tools/lint/fix/soong_lint_fix.py b/tools/lint/fix/soong_lint_fix.py
index 4a2e37e..2e82beb 100644
--- a/tools/lint/fix/soong_lint_fix.py
+++ b/tools/lint/fix/soong_lint_fix.py
@@ -14,6 +14,7 @@
 
 import argparse
 import json
+import functools
 import os
 import shutil
 import subprocess
@@ -28,6 +29,7 @@
 PATH_PREFIX = "out/soong/.intermediates"
 PATH_SUFFIX = "android_common/lint"
 FIX_ZIP = "suggested-fixes.zip"
+MODULE_JAVA_DEPS = "out/soong/module_bp_java_deps.json"
 
 
 class SoongModule:
@@ -49,11 +51,26 @@
         print(f"Found module {partial_path}/{self._name}.")
         self._path = f"{PATH_PREFIX}/{partial_path}/{self._name}/{PATH_SUFFIX}"
 
+    def find_java_deps(self, module_java_deps):
+        """Finds the dependencies of a Java module in the loaded module_bp_java_deps.json.
+
+        Returns:
+            A list of module names.
+        """
+        if self._name not in module_java_deps:
+            raise Exception(f"Module {self._name} not found!")
+
+        return module_java_deps[self._name]["dependencies"]
+
     @property
     def name(self):
         return self._name
 
     @property
+    def path(self):
+        return self._path
+
+    @property
     def lint_report(self):
         return f"{self._path}/lint-report.txt"
 
@@ -62,52 +79,25 @@
         return f"{self._path}/{FIX_ZIP}"
 
 
-class SoongLintFix:
+class SoongLintWrapper:
     """
-    This class creates a command line tool that will apply lint fixes to the
-    platform via the necessary combination of soong and shell commands.
+    This class wraps the necessary calls to Soong and/or shell commands to lint
+    platform modules and apply suggested fixes if desired.
 
-    It breaks up these operations into a few "private" methods that are
-    intentionally exposed so experimental code can tweak behavior.
-
-    The entry point, `run`, will apply lint fixes using the intermediate
-    `suggested-fixes` directory that soong creates during its invocation of
-    lint.
-
-    Basic usage:
-    ```
-    from soong_lint_fix import SoongLintFix
-
-    opts = SoongLintFixOptions()
-    opts.parse_args(sys.argv)
-    SoongLintFix(opts).run()
-    ```
+    It breaks up these operations into a few methods that are available to
+    sub-classes (see SoongLintFix for an example).
     """
-    def __init__(self, opts):
-        self._opts = opts
+    def __init__(self, check=None, lint_module=None):
+        self._check = check
+        self._lint_module = lint_module
         self._kwargs = None
-        self._modules = []
-
-    def run(self):
-        """
-        Run the script
-        """
-        self._setup()
-        self._find_modules()
-        self._lint()
-
-        if not self._opts.no_fix:
-            self._fix()
-
-        if self._opts.print:
-            self._print()
 
     def _setup(self):
         env = os.environ.copy()
-        if self._opts.check:
-            env["ANDROID_LINT_CHECK"] = self._opts.check
-        if self._opts.lint_module:
-            env["ANDROID_LINT_CHECK_EXTRA_MODULES"] = self._opts.lint_module
+        if self._check:
+            env["ANDROID_LINT_CHECK"] = self._check
+        if self._lint_module:
+            env["ANDROID_LINT_CHECK_EXTRA_MODULES"] = self._lint_module
 
         self._kwargs = {
             "env": env,
@@ -117,7 +107,10 @@
 
         os.chdir(ANDROID_BUILD_TOP)
 
-        print("Refreshing soong modules...")
+    @functools.cached_property
+    def _module_info(self):
+        """Returns the JSON content of module-info.json."""
+        print("Refreshing Soong modules...")
         try:
             os.mkdir(ANDROID_PRODUCT_OUT)
         except OSError:
@@ -125,19 +118,54 @@
         subprocess.call(f"{SOONG_UI} --make-mode {PRODUCT_OUT}/module-info.json", **self._kwargs)
         print("done.")
 
-
-    def _find_modules(self):
         with open(f"{ANDROID_PRODUCT_OUT}/module-info.json") as f:
-            module_info = json.load(f)
+            return json.load(f)
 
-        for module_name in self._opts.modules:
-            module = SoongModule(module_name)
-            module.find(module_info)
-            self._modules.append(module)
+    def _find_module(self, module_name):
+        """Returns a SoongModule from a module name.
 
-    def _lint(self):
+        Ensures that the module is known to Soong.
+        """
+        module = SoongModule(module_name)
+        module.find(self._module_info)
+        return module
+
+    def _find_modules(self, module_names):
+        modules = []
+        for module_name in module_names:
+            modules.append(self._find_module(module_name))
+        return modules
+
+    @functools.cached_property
+    def _module_java_deps(self):
+        """Returns the JSON content of module_bp_java_deps.json."""
+        print("Refreshing Soong Java deps...")
+        subprocess.call(f"{SOONG_UI} --make-mode {MODULE_JAVA_DEPS}", **self._kwargs)
+        print("done.")
+
+        with open(f"{MODULE_JAVA_DEPS}") as f:
+            return json.load(f)
+
+    def _find_module_java_deps(self, module):
+        """Returns a list a dependencies for a module.
+
+        Args:
+            module: A SoongModule.
+
+        Returns:
+            A list of SoongModule.
+        """
+        deps = []
+        dep_names = module.find_java_deps(self._module_java_deps)
+        for dep_name in dep_names:
+            dep = SoongModule(dep_name)
+            dep.find(self._module_info)
+            deps.append(dep)
+        return deps
+
+    def _lint(self, modules):
         print("Cleaning up any old lint results...")
-        for module in self._modules:
+        for module in modules:
             try:
                 os.remove(f"{module.lint_report}")
                 os.remove(f"{module.suggested_fixes}")
@@ -145,13 +173,13 @@
                 pass
         print("done.")
 
-        target = " ".join([ module.lint_report for module in self._modules ])
+        target = " ".join([ module.lint_report for module in modules ])
         print(f"Generating {target}")
         subprocess.call(f"{SOONG_UI} --make-mode {target}", **self._kwargs)
         print("done.")
 
-    def _fix(self):
-        for module in self._modules:
+    def _fix(self, modules):
+        for module in modules:
             print(f"Copying suggested fixes for {module.name} to the tree...")
             with zipfile.ZipFile(f"{module.suggested_fixes}") as zip:
                 for name in zip.namelist():
@@ -161,13 +189,40 @@
                         shutil.copyfileobj(src, dst)
             print("done.")
 
-    def _print(self):
-        for module in self._modules:
+    def _print(self, modules):
+        for module in modules:
             print(f"### lint-report.txt {module.name} ###", end="\n\n")
             with open(module.lint_report, "r") as f:
                 print(f.read())
 
 
+class SoongLintFix(SoongLintWrapper):
+    """
+    Basic usage:
+    ```
+    from soong_lint_fix import SoongLintFix
+
+    opts = SoongLintFixOptions()
+    opts.parse_args()
+    SoongLintFix(opts).run()
+    ```
+    """
+    def __init__(self, opts):
+        super().__init__(check=opts.check, lint_module=opts.lint_module)
+        self._opts = opts
+
+    def run(self):
+        self._setup()
+        modules = self._find_modules(self._opts.modules)
+        self._lint(modules)
+
+        if not self._opts.no_fix:
+            self._fix(modules)
+
+        if self._opts.print:
+            self._print(modules)
+
+
 class SoongLintFixOptions:
     """Options for SoongLintFix"""
 
diff --git a/tools/lint/utils/enforce_permission_counter.py b/tools/lint/utils/enforce_permission_counter.py
index b5c2ffe..a4c00f7 100644
--- a/tools/lint/utils/enforce_permission_counter.py
+++ b/tools/lint/utils/enforce_permission_counter.py
@@ -16,57 +16,38 @@
 
 import soong_lint_fix
 
-# Libraries that constitute system_server.
-# It is non-trivial to keep in sync with services/Android.bp as some
-# module are post-processed (e.g, services.core).
-TARGETS = [
-        "services.core.unboosted",
-        "services.accessibility",
-        "services.appprediction",
-        "services.appwidget",
-        "services.autofill",
-        "services.backup",
-        "services.companion",
-        "services.contentcapture",
-        "services.contentsuggestions",
-        "services.coverage",
-        "services.devicepolicy",
-        "services.midi",
-        "services.musicsearch",
-        "services.net",
-        "services.people",
-        "services.print",
-        "services.profcollect",
-        "services.restrictions",
-        "services.searchui",
-        "services.smartspace",
-        "services.systemcaptions",
-        "services.translation",
-        "services.texttospeech",
-        "services.usage",
-        "services.usb",
-        "services.voiceinteraction",
-        "services.wallpapereffectsgeneration",
-        "services.wifi",
-]
+CHECK = "AnnotatedAidlCounter"
+LINT_MODULE = "AndroidUtilsLintChecker"
 
-
-class EnforcePermissionMigratedCounter:
+class EnforcePermissionMigratedCounter(soong_lint_fix.SoongLintWrapper):
     """Wrapper around lint_fix to count the number of AIDL methods annotated."""
+
+    def __init__(self):
+        super().__init__(check=CHECK, lint_module=LINT_MODULE)
+
     def run(self):
-        opts = soong_lint_fix.SoongLintFixOptions()
-        opts.check = "AnnotatedAidlCounter"
-        opts.lint_module = "AndroidUtilsLintChecker"
-        opts.no_fix = True
-        opts.modules = TARGETS
+        self._setup()
 
-        self.linter = soong_lint_fix.SoongLintFix(opts)
-        self.linter.run()
-        self.parse_lint_reports()
+        # Analyze the dependencies of the "services" module and the module
+        # "services.core.unboosted".
+        service_module = self._find_module("services")
+        dep_modules = self._find_module_java_deps(service_module) + \
+                      [self._find_module("services.core.unboosted")]
 
-    def parse_lint_reports(self):
+        # Skip dependencies that are not services. Skip the "services.core"
+        # module which is analyzed via "services.core.unboosted".
+        modules = []
+        for module in dep_modules:
+            if "frameworks/base/services" not in module.path:
+                continue
+            if module.name == "services.core":
+                continue
+            modules.append(module)
+
+        self._lint(modules)
+
         counts = { "unannotated": 0, "enforced": 0, "notRequired": 0 }
-        for module in self.linter._modules:
+        for module in modules:
             with open(module.lint_report, "r") as f:
                 content = f.read()
                 keys = dict(re.findall(r'(\w+)=(\d+)', content))