pw_presubmit: Search BUILD.gn files directly

- Support directly searching BUILD.gn files for path names. This finds
  files more reliably than gn desc in some circumstances.
- List a few sources in source_sets in BUILD.gn files instead of
  referring directly to these files from elsewhere. This makes it
  possible to find these files using the new BUILD.gn searching method.
- Switch to directly searching BUILD.gn files rather than using gn desc.
  This approach is faster and ensures that sources are listed in their
  BUILD.gn files rather than directly referred to from elsewhere.

Change-Id: Ife525143d3d70f6a19719027e02bda21263f12eb
diff --git a/pw_bloat/BUILD.gn b/pw_bloat/BUILD.gn
index 2ae952c..8011fc2 100644
--- a/pw_bloat/BUILD.gn
+++ b/pw_bloat/BUILD.gn
@@ -30,6 +30,10 @@
   sources = [ "bloat_this_binary.cc" ] + public
 }
 
+source_set("base_main") {
+  sources = [ "base_main.cc" ]
+}
+
 # Standard minimal base binary for bloat reports.
 pw_executable("bloat_base") {
   forward_variables_from(pw_bloat_empty_base, "*")
diff --git a/pw_bloat/bloat.gni b/pw_bloat/bloat.gni
index c2e42a5..bb6a653 100644
--- a/pw_bloat/bloat.gni
+++ b/pw_bloat/bloat.gni
@@ -340,6 +340,8 @@
 # main() function that loads the bloat_this_binary library and does nothing
 # else.
 pw_bloat_empty_base = {
-  deps = [ "$dir_pw_bloat:bloat_this_binary" ]
-  sources = [ "$dir_pw_bloat/base_main.cc" ]
+  deps = [
+    "$dir_pw_bloat:base_main",
+    "$dir_pw_bloat:bloat_this_binary",
+  ]
 }
diff --git a/pw_fuzzer/BUILD.gn b/pw_fuzzer/BUILD.gn
index 6b67150..61271c3 100644
--- a/pw_fuzzer/BUILD.gn
+++ b/pw_fuzzer/BUILD.gn
@@ -77,6 +77,15 @@
   public_deps = [ "$dir_pw_log" ]
 }
 
+source_set("run_as_unit_test") {
+  configs = [ ":default_config" ]
+  sources = [ "pw_fuzzer_disabled.cc" ]
+  deps = [
+    dir_pw_log,
+    dir_pw_unit_test,
+  ]
+}
+
 # See https://llvm.org/docs/LibFuzzer.html#fuzzer-friendly-build-mode
 config("fuzzing_build_mode_unsafe_for_production") {
   defines = [ "FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION" ]
diff --git a/pw_fuzzer/fuzzer.gni b/pw_fuzzer/fuzzer.gni
index 101d4b1..fd19308 100644
--- a/pw_fuzzer/fuzzer.gni
+++ b/pw_fuzzer/fuzzer.gni
@@ -82,14 +82,7 @@
       forward_variables_from(invoker, "*", [ "visibility" ])
       forward_variables_from(invoker, [ "visibility" ])
       sources += [ "$dir_pw_fuzzer/pw_fuzzer_disabled.cc" ]
-      if (!defined(configs)) {
-        configs = []
-      }
-      configs += [ "$dir_pw_fuzzer:default_config" ]
-      deps += [
-        "$dir_pw_log",
-        "$dir_pw_unit_test",
-      ]
+      deps += [ "$dir_pw_fuzzer:run_as_unit_test" ]
     }
   }
 }
diff --git a/pw_preprocessor/BUILD.gn b/pw_preprocessor/BUILD.gn
index f8ca773..dff5a11 100644
--- a/pw_preprocessor/BUILD.gn
+++ b/pw_preprocessor/BUILD.gn
@@ -48,19 +48,22 @@
   ]
 }
 
-template("preprocessor_test") {
-  not_needed([ "invoker" ])
-  pw_test(target_name) {
-    deps = [ ":pw_preprocessor" ]
-    sources = [ "$target_name.cc" ]
-  }
+pw_test("boolean_test") {
+  deps = [ ":pw_preprocessor" ]
+  sources = [ "boolean_test.cc" ]
 }
 
-preprocessor_test("boolean_test") {
+pw_test("concat_test") {
+  deps = [ ":pw_preprocessor" ]
+  sources = [ "concat_test.cc" ]
 }
-preprocessor_test("concat_test") {
+
+pw_test("macro_arg_count_test") {
+  deps = [ ":pw_preprocessor" ]
+  sources = [ "macro_arg_count_test.cc" ]
 }
-preprocessor_test("macro_arg_count_test") {
-}
-preprocessor_test("util_test") {
+
+pw_test("util_test") {
+  deps = [ ":pw_preprocessor" ]
+  sources = [ "util_test.cc" ]
 }
diff --git a/pw_presubmit/py/pw_presubmit/build.py b/pw_presubmit/py/pw_presubmit/build.py
index 5d53b82..618f3a2 100644
--- a/pw_presubmit/py/pw_presubmit/build.py
+++ b/pw_presubmit/py/pw_presubmit/build.py
@@ -17,6 +17,7 @@
 import logging
 import os
 from pathlib import Path
+import re
 from typing import Container, Dict, Iterable, List, Mapping, Set, Tuple
 
 from pw_presubmit import call, log_run, plural, PresubmitFailure, tools
@@ -90,11 +91,26 @@
     return files
 
 
+# Finds string literals with '.' in them.
+_MAYBE_A_PATH = re.compile(r'"([^\n"]+\.[^\n"]+)"')
+
+
+def _search_files_for_paths(build_files: Iterable[Path]) -> Iterable[Path]:
+    for build_file in build_files:
+        directory = build_file.parent
+
+        for string in _MAYBE_A_PATH.finditer(build_file.read_text()):
+            path = directory / string.group(1)
+            if path.is_file():
+                yield path
+
+
 def check_builds_for_files(
         extensions_to_check: Container[str],
         files: Iterable[Path],
         bazel_dirs: Iterable[Path] = (),
         gn_dirs: Iterable[Tuple[Path, Path]] = (),
+        gn_build_files: Iterable[Path] = (),
 ) -> Dict[str, List[Path]]:
     """Checks that source files are in the GN and Bazel builds.
 
@@ -103,6 +119,7 @@
         files: the files that should be checked
         bazel_dirs: directories in which to run bazel query
         gn_dirs: (source_dir, output_dir) tuples with which to run gn desc
+        gn_build_files: paths to BUILD.gn files to directly search for paths
 
     Returns:
         a dictionary mapping build system ('Bazel' or 'GN' to a list of missing
@@ -123,6 +140,8 @@
         gn_builds.update(
             _get_paths_from_command(source_dir, 'gn', 'desc', output_dir, '*'))
 
+    gn_builds.update(_search_files_for_paths(gn_build_files))
+
     missing: Dict[str, List[Path]] = collections.defaultdict(list)
 
     for path in (p for p in files if p.suffix in extensions_to_check):
@@ -130,7 +149,7 @@
             # TODO(pwbug/176) Replace this workaround for fuzzers.
             if 'fuzz' not in str(path):
                 missing['Bazel'].append(path)
-        if gn_dirs and path not in gn_builds:
+        if (gn_dirs or gn_build_files) and path not in gn_builds:
             missing['GN'].append(path)
 
     for builder, paths in missing.items():
diff --git a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
index 8ef7136..b7ee2fc 100755
--- a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
@@ -32,7 +32,8 @@
         os.path.abspath(__file__))))
     import pw_presubmit
 
-from pw_presubmit import build, cli, environment, format_code, python_checks
+from pw_presubmit import build, cli, environment, format_code, git_repo
+from pw_presubmit import python_checks
 from pw_presubmit import call, filter_paths, PresubmitContext, PresubmitFailure
 from pw_presubmit import Programs
 from pw_presubmit.install_hook import install_hook
@@ -251,13 +252,12 @@
 @filter_paths(endswith=(*_SOURCES_IN_BUILD, 'BUILD', '.bzl', '.gn', '.gni'))
 def source_is_in_build_files(ctx: PresubmitContext):
     """Checks that source files are in the GN and Bazel builds."""
-    gn_path = ctx.output_dir.joinpath('default')
-    build.gn_gen(ctx.root, gn_path, pw_build_HOST_TOOLS='true')
-
-    missing = build.check_builds_for_files(_SOURCES_IN_BUILD,
-                                           ctx.paths,
-                                           bazel_dirs=[ctx.root],
-                                           gn_dirs=[(ctx.root, gn_path)])
+    missing = build.check_builds_for_files(
+        _SOURCES_IN_BUILD,
+        ctx.paths,
+        bazel_dirs=[ctx.root],
+        gn_build_files=git_repo.list_files(
+            pathspecs=['BUILD.gn', '*BUILD.gn']))
 
     if missing:
         _LOG.warning(