pw_presubmit: Build file check; fatal pylint

- Check that all C, C++ sources appear in BUILD and BUILD.gn files.
- Check that all .rst files appear in BUILD.gn files.
- Make the pylint check fatal. The full presubmit will fail until some
  issues are fixed. Incremental presubmits (e.g. with the pre-push hook
  or pw presubmit --base master) will pass (unless there are errors).

Change-Id: Id42cc2e06278d0e56017a2008676db8e77ba6c20
diff --git a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
index bf1a224..1dde12c 100755
--- a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
@@ -34,7 +34,7 @@
 
 from pw_presubmit import format_code
 from pw_presubmit.install_hook import install_hook
-from pw_presubmit import call, filter_paths, PresubmitFailure
+from pw_presubmit import call, filter_paths, plural, PresubmitFailure
 
 _LOG: logging.Logger = logging.getLogger(__name__)
 
@@ -171,12 +171,7 @@
 
 @filter_paths(endswith='.py')
 def pylint(paths):
-    try:
-        run_python_module('pylint', '-j', '0', *paths)
-    except PresubmitFailure:
-        # TODO(hepler): Enforce pylint when it passes.
-        _LOG.warning('pylint checks FAILED!')
-        _LOG.warning('Treating this as a warning... for now.')
+    run_python_module('pylint', '-j', '0', *paths)
 
 
 @filter_paths(endswith='.py', exclude=r'(?:.+/)?setup\.py')
@@ -270,6 +265,52 @@
 CODE_FORMAT = (copyright_notice, *format_code.PRESUBMIT_CHECKS)
 
 #
+# General presubmit checks
+#
+
+
+def _read_contents(paths, comment: bytes = b'#') -> bytearray:
+    contents = bytearray()
+
+    for path in paths:
+        with open(path, 'rb') as file:
+            for line in file:
+                line = line.strip()
+                if not line.startswith(comment):
+                    contents += line
+
+    return contents
+
+
+@filter_paths(endswith=('.rst', *format_code.C_FORMAT.extensions))
+def source_is_in_build_files(paths):
+    build = _read_contents(
+        pw_presubmit.git_stdout('ls-files', 'BUILD', '*/BUILD').split())
+    build_gn = _read_contents(
+        pw_presubmit.git_stdout('ls-files', 'BUILD.gn', '*/BUILD.gn').split())
+
+    failed = []
+
+    for path in paths:
+        filename = os.path.basename(path).encode()
+
+        if not filename.endswith(b'.rst') and filename not in build:
+            _LOG.warning('Missing from Bazel BUILD files: %s', path)
+            failed.append(path)
+
+        if filename not in build_gn:
+            _LOG.warning('Missing from GN BUILD.gn files: %s', path)
+            failed.append(path)
+
+    if failed:
+        _LOG.warning('%s are missing from build files!',
+                     plural(failed, 'source'))
+        raise PresubmitFailure
+
+
+GENERAL = (source_is_in_build_files, )
+
+#
 # Presubmit check programs
 #
 QUICK_PRESUBMIT: Sequence = (
@@ -278,10 +319,11 @@
     gn_clang_build,
     pw_presubmit.pragma_once,
     *CODE_FORMAT,
+    *GENERAL,
 )
 
 PROGRAMS: Dict[str, Sequence] = {
-    'full': INIT + GN + CC + PYTHON + BAZEL + CODE_FORMAT,
+    'full': INIT + GN + CC + PYTHON + BAZEL + CODE_FORMAT + GENERAL,
     'quick': QUICK_PRESUBMIT,
 }