3"""Compiler-control assertions shared by the suppression selftest."""
5from __future__
import annotations
8from pathlib
import Path
10from check_no_dynamic_alloc
import check
as check_dynamic_alloc
11from selftest_assert
import expect
12from suppression_model
import Inventory
13from suppression_scan
import scan_paths
15EXPECTED_COMPILER_PRAGMA_ROWS = 12
16EXPECTED_CLANG_TIDY_CONFIG_ROWS = 1
17EXPECTED_ALLOC_PROBLEMS = 2
18EXPECTED_CPP_ALLOC_PROBLEMS = 1
19EXPECTED_CPP_ALLOC_ROWS = 2
20EXPECTED_GNU_UNUSED_ROWS = 2
21EXPECTED_GNU_SANITIZER_ROWS = 3
24def _run_tool(executable: str, arguments: list[str], output: Path) -> tuple[int, str]:
25 """Run one fixed absolute tool path and capture its complete diagnostics."""
26 flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
27 descriptor = os.open(output, flags, 0o600)
29 (os.POSIX_SPAWN_DUP2, descriptor, 1),
30 (os.POSIX_SPAWN_DUP2, descriptor, 2),
31 (os.POSIX_SPAWN_CLOSE, descriptor),
34 process = os.posix_spawn(
36 [executable, *arguments],
40 _, status = os.waitpid(process, 0)
43 return os.waitstatus_to_exitcode(status), output.read_text(encoding=
"utf-8")
46def _assert_control_rows(inventory: Inventory, failures: list[str]) ->
None:
47 """Assert valid C-family controls fire once and string lookalikes stay inert."""
48 rows = [item
for item
in inventory.suppressions
if item.path ==
"compiler_controls.c"]
49 pragmas = [item
for item
in rows
if item.provenance ==
"compiler-pragma"]
51 len(pragmas) == EXPECTED_COMPILER_PRAGMA_ROWS,
52 "must fire: paired GCC and clang diagnostic state controls",
56 len([item
for item
in rows
if item.directive ==
"[[maybe_unused]]"]) == 1
57 and len([item
for item
in rows
if item.directive ==
"__attribute__((unused))"])
58 == EXPECTED_GNU_UNUSED_ROWS
59 and len([item
for item
in rows
if item.directive ==
"[[gnu::unused]]"]) == 1
60 and len([item
for item
in rows
if item.directive ==
"__attribute__((no_sanitize))"])
61 == EXPECTED_GNU_SANITIZER_ROWS
62 and len([item
for item
in rows
if item.directive ==
"[[clang::no_sanitize]]"]) == 1,
63 "must fire: every clang-18-valid compiler attribute spelling is inventoried",
67 len([item
for item
in rows
if item.directive ==
"RA8_NASA_RULE_3_OK"]) == 1,
68 "must fire: reason-bearing NASA Rule 3 waiver",
72 len([item
for item
in rows
if item.directive ==
"alloc-allow"]) == 1,
73 "must fire: same-line reasoned allocation waiver",
78def _assert_malformed_controls(inventory: Inventory, failures: list[str]) ->
None:
79 """Assert malformed, orphaned, mismatched, and unscoped controls fail closed."""
80 codes = {item.code
for item
in inventory.findings
if item.path ==
"malformed_controls.c"}
82 "malformed-alloc-allow",
83 "malformed-diagnostic-pragma",
84 "malformed-gnu-attribute",
85 "malformed-maybe-unused",
86 "malformed-standard-attribute",
87 "malformed-nasa-rule-3-waiver",
89 "unmatched-diagnostic-pop",
90 "unmatched-region-end",
91 "unmatched-region-start",
92 "unscoped-diagnostic-control",
96 "must fire: malformed, orphaned, and unscoped compiler controls",
101def _assert_clang_tidy_and_alloc(inventory: Inventory, root: Path, failures: list[str]) ->
None:
102 """Assert clang-tidy reasoning and lexical allocation scope."""
105 for item
in inventory.suppressions
106 if item.path ==
".clang-tidy" and item.directive ==
"Checks exclude"
109 len(config) == EXPECTED_CLANG_TIDY_CONFIG_ROWS
and not config[0].concerns,
110 "must fire: source-located reasoned clang-tidy global exclusion",
113 alloc_problems = check_dynamic_alloc(root /
"alloc_consumer.c")
115 len(alloc_problems) == EXPECTED_ALLOC_PROBLEMS
116 and any(
"string_only" in problem
for problem
in alloc_problems)
117 and any(
"without governed allocation" in problem
for problem
in alloc_problems),
118 "quiet: allocation waiver works only in a lexical comment with a governed call",
123 for item
in inventory.suppressions
124 if item.path ==
"alloc_consumer.cpp" and item.directive ==
"alloc-allow"
126 cpp_problems = check_dynamic_alloc(root /
"alloc_consumer.cpp")
129 for item
in inventory.findings
130 if item.path ==
"alloc_consumer.cpp" and item.code ==
"orphan-alloc-allow"
133 len(cpp_rows) == EXPECTED_CPP_ALLOC_ROWS
134 and {item.rule
for item
in cpp_rows} == {
"C++ new",
"C++ delete"}
135 and len(cpp_problems) == EXPECTED_CPP_ALLOC_PROBLEMS
136 and "without governed allocation" in cpp_problems[0]
137 and len(cpp_findings) == 1,
138 "must fire: C++ new/delete controls are active and orphan controls fail closed",
143def assert_compiler_controls(inventory: Inventory, root: Path, failures: list[str]) ->
None:
144 """Assert every assigned C-family control fires without string false positives."""
145 _assert_control_rows(inventory, failures)
146 _assert_malformed_controls(inventory, failures)
147 _assert_clang_tidy_and_alloc(inventory, root, failures)
150def _clang_syntax_status(root: Path, name: str, source: str) -> int:
151 """Compile one C23 fixture with the pinned warning-strict Clang."""
152 fixture = root / f
"{name}.c"
153 fixture.write_text(source, encoding=
"ascii")
154 status, _ = _run_tool(
164 root / f
"{name}.log",
169def _assert_compiler_probe(root: Path, failures: list[str]) ->
None:
170 """Prove pinned Clang accepts every inventoried valid grammar."""
171 compiler = Path(
"/usr/bin/clang-18")
172 expect(compiler.is_file(),
"must fire: pinned clang-18 is available", failures)
173 if not compiler.is_file():
175 source =
"""_Pragma("clang diagnostic push")
176_Pragma("clang diagnostic ignored \\"-Wunused-variable\\"")
177void probe(void) { int unused; }
178_Pragma("clang diagnostic pop")
179[[deprecated, maybe_unused]] static int maybe_value;
180[[gnu::unused]] static int scoped_gnu_value;
181__attribute((unused)) static int short_gnu_value;
182__attribute__((unused)) static int gnu_value;
183__attribute__((no_sanitize(\"undefined\"))) int identity(int value) { return value; }
184__attribute__((__no_sanitize__(\"undefined\"))) int identity_two(int value) { return value; }
185__attribute__((__no_sanitize_address__)) int identity_three(int value) { return value; }
186[[clang::no_sanitize(\"undefined\")]] int identity_four(int value) { return value; }
188 accepted_status = _clang_syntax_status(root,
"compiler_probe", source)
190 accepted_status == 0,
191 "quiet: clang-18 accepts _Pragma and every supported attribute grammar",
196def _assert_compiler_rejections(root: Path, failures: list[str]) ->
None:
197 """Prove pinned Clang rejects the three fail-closed grammar boundaries."""
198 if not Path(
"/usr/bin/clang-18").is_file():
201 (
"compiler_bad_unused",
"__attribute__((unused extra)) static int value;\n"),
203 "compiler_bad_sanitizer",
204 "__attribute__((no_sanitize(foo))) int identity(int value) { return value; }\n",
206 (
"compiler_bad_maybe",
"[[maybe_unused bogus]] static int value;\n"),
208 for name, source
in invalid:
209 rejected_status = _clang_syntax_status(root, name, source)
211 rejected_status != 0,
212 f
"must fire: clang-18 rejects {name.removeprefix('compiler_bad_')}",
217def _assert_clang_tidy_probe(root: Path, failures: list[str]) ->
None:
218 """Prove pinned clang-tidy accepts the inventoried negative check glob."""
219 tidy = Path(
"/usr/bin/clang-tidy-18")
220 expect(tidy.is_file(),
"must fire: pinned clang-tidy-18 is available", failures)
222 dumped_status, dumped_output = _run_tool(
223 "/usr/bin/clang-tidy-18",
225 f
"--config-file={root / '.clang-tidy'}",
228 root /
"clang_tidy_probe.log",
231 dumped_status == 0
and "-readability-fixture" in dumped_output,
232 "quiet: clang-tidy-18 activates the inventoried negative Checks glob",
237def assert_compiler_tool_probes(root: Path, failures: list[str]) ->
None:
238 """Prove pinned compiler tools accept the inventoried grammars."""
239 _assert_compiler_probe(root, failures)
240 _assert_compiler_rejections(root, failures)
241 _assert_clang_tidy_probe(root, failures)
244def assert_clang_tidy_config_fail_closed(root: Path, failures: list[str]) ->
None:
245 """Assert valid YAML shapes outside the source locator cannot disappear."""
246 config = root /
".clang-tidy"
247 original = config.read_text(encoding=
"ascii")
248 config.write_text(
"Checks: [readability-*]\n", encoding=
"ascii")
249 inventory = scan_paths(root, [
".clang-tidy"])
251 any(item.code ==
"malformed-clang-tidy-config" for item
in inventory.findings),
252 "must fire: non-string clang-tidy Checks config fails closed",
255 config.write_text(original, encoding=
"ascii")