From 7aeacae908e1846501a559223453b99aacfb37a7 Mon Sep 17 00:00:00 2001 From: Your GitHub Username Date: Sat, 3 Oct 2026 08:03:47 +0530 Subject: [PATCH 1/3] fix(ci): parse clang-tidy analyzer warning suffix Signed-off-by: Your GitHub Username --- scripts/lint-mem-gate.py | 10 +++++++++- tests/test_lint_mem_gate.sh | 39 +++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100755 tests/test_lint_mem_gate.sh diff --git a/scripts/lint-mem-gate.py b/scripts/lint-mem-gate.py index 140062f37..d5ccd76d2 100755 --- a/scripts/lint-mem-gate.py +++ b/scripts/lint-mem-gate.py @@ -47,7 +47,15 @@ WHITELIST = Path(os.environ.get( MIN_WHY = 120 MIN_TRIED = 40 -FINDING_RE = re.compile(r"^(?P[^:]+):(?P\d+):\d+:\s+(?:error|warning):\s+(?P.*?)\s*\[(?P[\w.-]+)\]\s*$") +# With WarningsAsErrors enabled, clang-tidy appends the escalation reason to +# analyzer checks, for example: +# [clang-analyzer-core.NullDereference,-warnings-as-errors] +# Keep the check name separate from that suffix so the gate still matches the +# same whitelist key instead of silently dropping the finding. +FINDING_RE = re.compile( + r"^(?P[^:]+):(?P\d+):\d+:\s+(?:error|warning):\s+" + r"(?P.*?)\s*\[(?P[\w.-]+)(?:,-warnings-as-errors)?\]\s*$" +) def strip_noise(text): diff --git a/tests/test_lint_mem_gate.sh b/tests/test_lint_mem_gate.sh new file mode 100755 index 000000000..05c2bd4a3 --- /dev/null +++ b/tests/test_lint_mem_gate.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Regression contract for clang-tidy's `,-warnings-as-errors` diagnostic suffix. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +FIX="$(mktemp -d "${TMPDIR:-/tmp}/lint-mem-gate.XXXXXX")" +trap 'rm -rf "$FIX"' EXIT + +cat > "$FIX/sample.c" <<'EOF' +int sample(int *value) { + return *value; +} +EOF + +cat > "$FIX/whitelist.txt" < "$FIX/stderr" + +grep -q 'memory gate clean (1 argued false positive' "$FIX/stderr" +echo 'lint-mem-gate warning suffix contract passed' From 91070cb2f0309002cca23c69fc3edf4e87feb2f6 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Mon, 2 Nov 2026 01:11:32 +0100 Subject: [PATCH 2/3] test(lint-mem): run the warning-suffix gate test on every leg scripts/test.sh lists every shell contract test explicitly and does not glob tests/, so tests/test_lint_mem_gate.sh never ran on any leg. Register it as Step 0e4, next to the other gate contract (Step 0e3), in the same two-line shape as its neighbours. Step 0 runs in every default scripts/test.sh leg (macOS, Linux, Windows) and in the PR contracts-only job. The wiring, Windows included, is a maintainer decision (R1). Make the test portable to the Windows leg. There python3 is a native MSYS2 build: MSYS converts path arguments for it, but never the diagnostic text on stdin, so the absolute fixture path inside the finding does not resolve there, and a C:/ path would not parse (the file field of FINDING_RE ends at the first colon). The gate now runs from inside the fixture directory, in a subshell, with relative paths. CRLF from a native python3 is stripped from the --hash output, the whitelist is written once the hash is known (no placeholder rewrite), the gate exit status is asserted, and every failed assertion is reported together with the gate output. Refs #2442 Signed-off-by: Martin Vogel --- scripts/test.sh | 3 ++ tests/test_lint_mem_gate.sh | 71 +++++++++++++++++++++++++++++-------- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/scripts/test.sh b/scripts/test.sh index cdc6d3e1d..1b4873c9f 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -267,6 +267,9 @@ bash "$ROOT/tests/test_activation_diagnostic_contract.sh" echo "=== Step 0e3: security gate fail-closed contract ===" bash "$ROOT/tests/test_security_gate_fail_closed.sh" +echo "=== Step 0e4: memory-analyzer gate warning-suffix contract (#2442) ===" +bash "$ROOT/tests/test_lint_mem_gate.sh" + echo "=== Step 0f: tree-sitter runtime Makefile dependencies ===" bash "$ROOT/tests/test_makefile_ts_runtime_dependencies.sh" diff --git a/tests/test_lint_mem_gate.sh b/tests/test_lint_mem_gate.sh index 05c2bd4a3..c6a655222 100755 --- a/tests/test_lint_mem_gate.sh +++ b/tests/test_lint_mem_gate.sh @@ -1,20 +1,56 @@ #!/usr/bin/env bash -# Regression contract for clang-tidy's `,-warnings-as-errors` diagnostic suffix. +# Regression contract for clang-tidy's `,-warnings-as-errors` diagnostic suffix +# (#2442). With WarningsAsErrors enabled, clang-tidy prints an analyzer check as +# [clang-analyzer-,-warnings-as-errors]; a gate that only accepts +# [] drops every such line and reports a clean gate. A suffixed finding +# WITH an argued whitelist entry must be counted under its plain check name and +# suppressed (exit 0, "1 argued false positive"). Every assertion is reported, +# so a regression names what broke. +# +# The gate runs from inside the fixture directory with RELATIVE paths. On the +# Windows leg python3 is a native MSYS2 build: MSYS converts path ARGUMENTS for +# it, but never the diagnostic text on stdin, so an absolute /c/... path in a +# finding would not resolve there, and a C:/... one would not parse (the file +# field ends at the first colon). set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +GATE="$ROOT/scripts/lint-mem-gate.py" FIX="$(mktemp -d "${TMPDIR:-/tmp}/lint-mem-gate.XXXXXX")" trap 'rm -rf "$FIX"' EXIT +failures=0 +fail() { + echo "FAIL: $*" >&2 + failures=$((failures + 1)) +} + +show() { + sed 's/^/ | /' "$1" >&2 +} + +# A subshell, so the test's own shell never sits inside $FIX (Windows cannot +# remove a directory that is a process's working directory). +run_gate() { + (cd "$FIX" && LINT_MEM_WHITELIST=whitelist.txt python3 "$GATE" "$@") +} + cat > "$FIX/sample.c" <<'EOF' int sample(int *value) { return *value; } EOF +# A native Windows python3 writes CRLF; strip it before the hash is reused. +HASH="$(run_gate --hash sample.c sample | tr -d '\r' | awk '/^segment-sha256:/ {print $2}')" +if [[ ! "$HASH" =~ ^[0-9a-f]{64}$ ]]; then + echo "FAIL: lint-mem-gate.py --hash sample.c sample printed no segment hash (got '$HASH')" >&2 + exit 1 +fi + cat > "$FIX/whitelist.txt" < /dev/null 2> "$FIX/whitelisted.err" || rc=$? +if [ "$rc" -ne 0 ]; then + fail "a whitelisted suffixed finding left the gate at exit $rc, expected 0" + show "$FIX/whitelisted.err" +fi +if ! grep -q 'memory gate clean (1 argued false positive' "$FIX/whitelisted.err"; then + fail "a whitelisted suffixed finding was not counted as 1 argued false positive" + show "$FIX/whitelisted.err" +fi -printf '%s\n' "$FIX/sample.c:2:12: warning: possible null dereference [clang-analyzer-core.NullDereference,-warnings-as-errors]" \ - | LINT_MEM_WHITELIST="$FIX/whitelist.txt" python3 "$ROOT/scripts/lint-mem-gate.py" 2> "$FIX/stderr" - -grep -q 'memory gate clean (1 argued false positive' "$FIX/stderr" +if [ "$failures" -ne 0 ]; then + echo "lint-mem-gate warning suffix contract: $failures assertion(s) failed" >&2 + exit 1 +fi echo 'lint-mem-gate warning suffix contract passed' From 24871eed7b62ea94e1629570160b083d4f445255 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Mon, 2 Nov 2026 01:11:51 +0100 Subject: [PATCH 3/3] test(lint-mem): an unwhitelisted suffixed finding must fail the gate The test only covered a whitelisted finding. The defect in #2442 is the other direction: on main clang-tidy reported findings that carry the ,-warnings-as-errors suffix, the old FINDING_RE matched none of them, and the gate reported clean. Pin that direction too: a second fixture function with no whitelist entry, a suffixed finding inside it, and the gate must exit 1 and report the finding under its plain check name ([clang-analyzer-core.NullDereference], the whitelist key form) as an entry missing for unlisted(). With the suffix group of FINDING_RE reverted, this case exits 0 with "memory gate clean" and the test fails, naming all three assertions. A parser that accepts the line but keeps the suffix inside the check name fails it as well. The negative assertion is a maintainer decision (R1). Refs #2442 Signed-off-by: Martin Vogel --- tests/test_lint_mem_gate.sh | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/tests/test_lint_mem_gate.sh b/tests/test_lint_mem_gate.sh index c6a655222..9178bca56 100755 --- a/tests/test_lint_mem_gate.sh +++ b/tests/test_lint_mem_gate.sh @@ -2,10 +2,15 @@ # Regression contract for clang-tidy's `,-warnings-as-errors` diagnostic suffix # (#2442). With WarningsAsErrors enabled, clang-tidy prints an analyzer check as # [clang-analyzer-,-warnings-as-errors]; a gate that only accepts -# [] drops every such line and reports a clean gate. A suffixed finding -# WITH an argued whitelist entry must be counted under its plain check name and -# suppressed (exit 0, "1 argued false positive"). Every assertion is reported, -# so a regression names what broke. +# [] drops every such line and reports a clean gate. Both directions are +# pinned, and every assertion is reported, so a regression names what broke: +# 1. a suffixed finding WITH an argued whitelist entry is counted under its +# plain check name and suppressed (exit 0, "1 argued false positive"); +# 2. a suffixed finding WITHOUT an entry fails the gate (exit 1) and is +# reported under its plain check name. This is the negative assertion: a +# parser that drops the suffixed line reports clean (exit 0) here. +# Reverting the suffix group in FINDING_RE (scripts/lint-mem-gate.py) turns +# both red. # # The gate runs from inside the fixture directory with RELATIVE paths. On the # Windows leg python3 is a native MSYS2 build: MSYS converts path ARGUMENTS for @@ -39,6 +44,10 @@ cat > "$FIX/sample.c" <<'EOF' int sample(int *value) { return *value; } + +int unlisted(int *value) { + return *value; +} EOF # A native Windows python3 writes CRLF; strip it before the hash is reused. @@ -60,7 +69,7 @@ EOF SUFFIX='[clang-analyzer-core.NullDereference,-warnings-as-errors]' -# Whitelisted: counted under the plain check name, then suppressed. +# 1. Whitelisted: counted under the plain check name, then suppressed. rc=0 printf '%s\n' "sample.c:2:12: warning: possible null dereference $SUFFIX" \ | run_gate > /dev/null 2> "$FIX/whitelisted.err" || rc=$? @@ -73,8 +82,24 @@ if ! grep -q 'memory gate clean (1 argued false positive' "$FIX/whitelisted.err" show "$FIX/whitelisted.err" fi +# 2. Not whitelisted: the gate must fail and name the finding. +rc=0 +printf '%s\n' "sample.c:6:12: warning: possible null dereference $SUFFIX" \ + | run_gate > /dev/null 2> "$FIX/unlisted.err" || rc=$? +if [ "$rc" -ne 1 ]; then + fail "an unwhitelisted suffixed finding left the gate at exit $rc, expected 1" \ + "(exit 0 = the suffixed line was parsed as no finding, #2442)" + show "$FIX/unlisted.err" +fi +if ! grep -qF 'sample.c:6: possible null dereference [clang-analyzer-core.NullDereference]' "$FIX/unlisted.err" || + ! grep -qF 'no whitelist entry for unlisted()' "$FIX/unlisted.err" || + ! grep -qF '1 finding(s) unaccounted for' "$FIX/unlisted.err"; then + fail "the gate did not report the unwhitelisted suffixed finding under its plain check name" + show "$FIX/unlisted.err" +fi + if [ "$failures" -ne 0 ]; then echo "lint-mem-gate warning suffix contract: $failures assertion(s) failed" >&2 exit 1 fi -echo 'lint-mem-gate warning suffix contract passed' +echo 'lint-mem-gate warning suffix contract passed (whitelisted + unwhitelisted suffixed findings)'