From 3ba62c1762dda386df18c7c3bb7d413e48871883 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 25 Oct 2026 10:15:17 -0700 Subject: [PATCH 1/3] Fail the Lint Run on a Malformed Compile Database --- tests/compile_lint/test_lint.py | 7 +++++++ tools/compile-lint/src/compile_lint/lint.py | 15 +++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/compile_lint/test_lint.py b/tests/compile_lint/test_lint.py index bf54f06..0f995f5 100644 --- a/tests/compile_lint/test_lint.py +++ b/tests/compile_lint/test_lint.py @@ -537,3 +537,10 @@ def test_a_build_tree_under_a_dot_directory_is_not_repository_code(tmp_path: Pat ) assert lint_build.owned_path(generated) is None assert lint_build.owned_path(root / "test" / "device.yaml") == "test/device.yaml" + + +@pytest.mark.parametrize("content", ["{not json", '{"directory": "/x"}', '[{"file": "a.cpp"}]']) +def test_a_malformed_compile_database_is_a_failure_to_run(tmp_path: Path, content: str) -> None: + root, build = write_build(tmp_path, "") + (build / "build" / "compile_commands.json").write_text(content) + assert main(["--config-root", str(root), str(build)]) == 2 diff --git a/tools/compile-lint/src/compile_lint/lint.py b/tools/compile-lint/src/compile_lint/lint.py index 8f35730..fe65827 100644 --- a/tools/compile-lint/src/compile_lint/lint.py +++ b/tools/compile-lint/src/compile_lint/lint.py @@ -209,8 +209,19 @@ def replace_source(arguments: list[str], entry: dict[str, Any], replacement: Pat def load_compile_database(path: Path) -> list[dict[str, Any]]: - """Read a compile database.""" - return cast(list[dict[str, Any]], json.loads(path.read_text(encoding="utf-8"))) + """Read a compile database, failing the run where it is unreadable or not one.""" + try: + entries: Any = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError) as error: + raise LintError(f"cannot read the compile database {path}: {error}") from error + if not isinstance(entries, list) or not all( + isinstance(entry, dict) + and {"directory", "file"} <= entry.keys() + and ("command" in entry or "arguments" in entry) + for entry in cast(list[Any], entries) + ): + raise LintError(f"{path}: not a compile database of directory, file and command entries") + return cast(list[dict[str, Any]], entries) def entry_path(entry: dict[str, Any]) -> Path: From bf5795748aba3dd451999240dd9262abfbf580ab Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 25 Oct 2026 10:17:19 -0700 Subject: [PATCH 2/3] Map Any Unexpected Lint Failure to Exit 2 at the Top Level --- tests/compile_lint/test_lint.py | 1 + tools/compile-lint/src/compile_lint/lint.py | 48 +++++++++++---------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/tests/compile_lint/test_lint.py b/tests/compile_lint/test_lint.py index 0f995f5..dfe9be9 100644 --- a/tests/compile_lint/test_lint.py +++ b/tests/compile_lint/test_lint.py @@ -539,6 +539,7 @@ def test_a_build_tree_under_a_dot_directory_is_not_repository_code(tmp_path: Pat assert lint_build.owned_path(root / "test" / "device.yaml") == "test/device.yaml" +# Invalid JSON is caught where it is read, and the other shapes by main's catch-all. @pytest.mark.parametrize("content", ["{not json", '{"directory": "/x"}', '[{"file": "a.cpp"}]']) def test_a_malformed_compile_database_is_a_failure_to_run(tmp_path: Path, content: str) -> None: root, build = write_build(tmp_path, "") diff --git a/tools/compile-lint/src/compile_lint/lint.py b/tools/compile-lint/src/compile_lint/lint.py index fe65827..968c446 100644 --- a/tools/compile-lint/src/compile_lint/lint.py +++ b/tools/compile-lint/src/compile_lint/lint.py @@ -209,19 +209,12 @@ def replace_source(arguments: list[str], entry: dict[str, Any], replacement: Pat def load_compile_database(path: Path) -> list[dict[str, Any]]: - """Read a compile database, failing the run where it is unreadable or not one.""" + """Read a compile database, failing the run where it cannot be read.""" try: - entries: Any = json.loads(path.read_text(encoding="utf-8")) + return cast(list[dict[str, Any]], json.loads(path.read_text(encoding="utf-8"))) except (OSError, ValueError) as error: + # An interrupted build can leave the file truncated. raise LintError(f"cannot read the compile database {path}: {error}") from error - if not isinstance(entries, list) or not all( - isinstance(entry, dict) - and {"directory", "file"} <= entry.keys() - and ("command" in entry or "arguments" in entry) - for entry in cast(list[Any], entries) - ): - raise LintError(f"{path}: not a compile database of directory, file and command entries") - return cast(list[dict[str, Any]], entries) def entry_path(entry: dict[str, Any]) -> Path: @@ -822,24 +815,33 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: def main(argv: list[str] | None = None) -> int: args = parse_args(argv) + try: + return run(args) + except LintError as error: + print(f"compile-lint: {error}", file=sys.stderr) + return 2 + except Exception as error: # noqa: BLE001 + # Any other failure means the lint did not run, so it must not read as exit 1, a finding. + print(f"compile-lint: unexpected {type(error).__name__}: {error}", file=sys.stderr) + return 2 + + +def run(args: argparse.Namespace) -> int: + """Lint every build the arguments name, and return the exit status for what was found.""" root = Path(os.path.normpath(args.config_root.resolve())) flags: list[str] = args.flags or DEFAULT_FLAGS owned = owned_sources(root) index = YamlIndex(root) + if args.compile: + # The compile takes minutes, and an editor save during it would shift YAML lines. + index.preload() + builds: list[tuple[Path, Path | None]] = [(build, None) for build in args.builds] + builds += [build_from_log(log) for log in args.from_log] + builds += [compile_config(c) for c in args.compile] results: list[BuildResult] = [] - try: - if args.compile: - # The compile takes minutes, and an editor save during it would shift YAML lines. - index.preload() - builds: list[tuple[Path, Path | None]] = [(build, None) for build in args.builds] - builds += [build_from_log(log) for log in args.from_log] - builds += [compile_config(c) for c in args.compile] - for build, config in builds: - build = Path(os.path.normpath(build.resolve())) - results.append(BuildLint(build, root, flags, owned, index, config).run()) - except LintError as error: - print(f"compile-lint: {error}", file=sys.stderr) - return 2 + for build, config in builds: + build = Path(os.path.normpath(build.resolve())) + results.append(BuildLint(build, root, flags, owned, index, config).run()) for result in results: for finding in result.findings: print( From eaaac299a9bbaba2a80165ba77621a353b005439 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 25 Oct 2026 10:18:21 -0700 Subject: [PATCH 3/3] Print the Traceback of an Unexpected Lint Failure --- tests/compile_lint/test_lint.py | 9 +++++++++ tools/compile-lint/src/compile_lint/lint.py | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/compile_lint/test_lint.py b/tests/compile_lint/test_lint.py index dfe9be9..36ae62f 100644 --- a/tests/compile_lint/test_lint.py +++ b/tests/compile_lint/test_lint.py @@ -545,3 +545,12 @@ def test_a_malformed_compile_database_is_a_failure_to_run(tmp_path: Path, conten root, build = write_build(tmp_path, "") (build / "build" / "compile_commands.json").write_text(content) assert main(["--config-root", str(root), str(build)]) == 2 + + +def test_an_unexpected_failure_keeps_its_traceback( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + root, build = write_build(tmp_path, "") + (build / "build" / "compile_commands.json").write_text('{"directory": "/x"}') + assert main(["--config-root", str(root), str(build)]) == 2 + assert "Traceback (most recent call last)" in capsys.readouterr().err diff --git a/tools/compile-lint/src/compile_lint/lint.py b/tools/compile-lint/src/compile_lint/lint.py index 968c446..6c2794d 100644 --- a/tools/compile-lint/src/compile_lint/lint.py +++ b/tools/compile-lint/src/compile_lint/lint.py @@ -30,6 +30,7 @@ import subprocess import sys import tempfile +import traceback from collections.abc import Iterator from dataclasses import dataclass, field from pathlib import Path @@ -821,7 +822,8 @@ def main(argv: list[str] | None = None) -> int: print(f"compile-lint: {error}", file=sys.stderr) return 2 except Exception as error: # noqa: BLE001 - # Any other failure means the lint did not run, so it must not read as exit 1, a finding. + # Any other failure is a bug in the lint, so keep its stack, and never exit 1, a finding. + traceback.print_exc() print(f"compile-lint: unexpected {type(error).__name__}: {error}", file=sys.stderr) return 2