| #!/usr/bin/env python3 |
| # Copyright 2023 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Pre-upload hook to check every #include has an actual path to the file. |
| |
| Certain files (like `snappy.h`) and certain prefixes (like `base`) are excluded, |
| but all other #include directives must point to a file that exists. |
| """ |
| |
| import argparse |
| from pathlib import Path |
| import re |
| import sys |
| from typing import Iterator, List, Optional, Tuple |
| |
| # pylint: disable=wrong-import-position |
| import chromite_init # pylint: disable=unused-import |
| |
| from chromite.lib import git |
| |
| |
| # Sub-directories that need to be verified for #includes correctness. |
| # Any platform2/project that is ready can be added. |
| PROJECTS_TO_VERIFY = frozenset({"missive"}) |
| |
| # Suffixes of files that need to be verifies for #includes correctness. |
| SUFFIXES_TO_VERIFY = frozenset({".h", ".hpp", ".cc", ".cpp", ".cxx", ".c"}) |
| |
| # Header file names that do not need to be verified: |
| # they refer to the runtime library. |
| ALLOWED_HEADERS = frozenset( |
| { |
| "snappy.h", |
| "unistd.h", |
| } |
| ) |
| |
| # Prefixes of the files that do not need to be verified: |
| # they refer to known components rather than the current |
| # module. |
| ALLOWED_PREFIXES = frozenset( |
| { |
| "absl", |
| "base", |
| "brillo", |
| "crypto", |
| "chromeos", |
| "dbus", |
| "dbus_adaptors", |
| "google", |
| "gmock", |
| "gtest", |
| "openssl", |
| "re2", |
| } |
| ) |
| |
| INCLUDE_RE: re.Pattern = re.compile(r"^#include\s*[\"<](\S*)[\">]") |
| |
| TOP_DIR = Path(__file__).resolve().parent.parent |
| |
| |
| def get_lines_from_commit( |
| file: Path, commit: Optional[str] |
| ) -> Iterator[Tuple[int, str]]: |
| """Reads the file data for |path| either from disk or git |commit|. |
| |
| Args: |
| file: Path of the file being processed. |
| commit: Hash of the commit. If given, only files referred by this commit |
| will be checked; otherwise check all files included in the |
| command. |
| |
| Returns: |
| An iterator of (#, line) pairs added to file by the given commit. |
| """ |
| if commit: |
| contents = git.GetObjectAtRev(None, f"./{file}", commit) |
| else: |
| contents = file.read_text(encoding="utf-8") |
| |
| return enumerate(contents.splitlines(), start=1) |
| |
| |
| def check_include_for_full_path( |
| file: Path, lines: Iterator[Tuple[int, str]] |
| ) -> Iterator[str]: |
| """Checks to make sure every #include .h file has a full path. |
| |
| Args: |
| file: Path of the file being processed. |
| lines: An iterator of [#, line] pairs from the file. |
| |
| Yields: |
| Error messages reporting incorrect #includes. |
| """ |
| |
| for lineno, line in lines: |
| m = INCLUDE_RE.match(line) |
| if not m: |
| continue |
| |
| header = Path(m.group(1)) |
| if header.suffix != ".h": |
| continue |
| if header.name in ALLOWED_HEADERS: |
| continue |
| if header.parts[0] in ALLOWED_PREFIXES: |
| continue |
| if not header.is_absolute(): |
| header = TOP_DIR / header |
| if tuple(header.suffixes) == (".pb", ".h"): |
| # *.pb.h files don't exist in source code, replace the suffix with |
| # *.proto |
| header = header.with_suffix("").with_suffix(".proto") |
| if not header.is_file(): |
| # Attempt to check `synced/pipeline` location instead. |
| header = header.parent / "synced" / "pipeline" / header.name |
| |
| if not header.is_file(): |
| yield f"{file}:{str(lineno)} {m.group(1)}" |
| |
| |
| def check_files(files: List[Path], commit: Optional[str]) -> Iterator[str]: |
| """Check for #include's without full path. |
| |
| Args: |
| files: A list of file paths. |
| commit: Hash of a commit. If given, only files referred by this commit |
| will be checked; otherwise check all files included in the |
| command. |
| |
| Yields: |
| Error messages reporting incorrect #includes. |
| """ |
| for file in files: |
| if not file.parts[0] in PROJECTS_TO_VERIFY: |
| continue |
| if not file.suffix in SUFFIXES_TO_VERIFY: |
| continue |
| lines = get_lines_from_commit(file, commit) |
| yield from check_include_for_full_path(file, lines) |
| |
| |
| def main(argv: Optional[List[str]] = None) -> int: |
| parser = argparse.ArgumentParser(description=__doc__) |
| |
| parser.add_argument( |
| "--commit", |
| help="Hash of commit to check. Only show errors on " |
| "lines added in the commit if set.", |
| ) |
| parser.add_argument("files", type=Path, nargs="*") |
| opts = parser.parse_args(argv) |
| |
| error_count = 0 |
| for error in check_files(opts.files, opts.commit): |
| if error_count == 0: |
| print( |
| "Some #include(s) in source errors do not have a full path\n", |
| file=sys.stderr, |
| ) |
| error_count += 1 |
| print( |
| error, |
| file=sys.stderr, |
| ) |
| |
| return error_count |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main(sys.argv[1:])) |