#!/usr/bin/env bash # analyzer-gate.sh — turn an analyzer's output into a pass/fail verdict. # # The 2026-08-10 audit asked for staticcheck, govulncheck and deadcode # (V-694). govulncheck needed no gate of this shape because it already # reported zero after the toolchain bump. The other two do not: staticcheck # reports 20 findings today and deadcode reports 11 unreachable symbols, and # three of those eleven are deliberate. A target that fails on the first run # is not a gate, it is a target nobody runs. So the accepted set is written # down, and only what is NOT in it fails. # # staticcheck ./... | scripts/analyzer-gate.sh staticcheck # deadcode -test ./... | scripts/analyzer-gate.sh deadcode # # The baseline is keyed on file, check id and message, never on line number. # A key carrying a line number goes stale on the next edit above it and then # reports moved findings as new ones, which trains the reader to ignore it. # The cost of dropping the line is that two identical findings in one file # share one key, so the second is accepted with the first. That is the right # way round: the same check firing twice on the same file is one thing to fix. # # A baseline entry with no finding left also fails. Fixing something and # leaving its entry behind is how the accepted set stops describing the repo. # The fix is one line: delete the entry the failure names. # # Reads stdin, writes a report, never writes a file. set -uo pipefail cd "$(dirname "$0")/.." || exit 1 tool="${1:?usage: analyzer-gate.sh }" baseline="scripts/analyzers/$tool.baseline" [ -f "$baseline" ] || { printf 'analyzer-gate: no baseline at %s\n' "$baseline" >&2; exit 2; } # Normalise to "\t\t". Anything that does not parse is an # analyzer error, not a finding, and it fails without consulting the baseline. # staticcheck: path.go:12:34: message (SA1234) # deadcode: path.go:12:34: unreachable func: Symbol found=$(mktemp) || exit 2 malformed=$(mktemp) || exit 2 trap 'rm -f "$found" "$malformed"' EXIT while IFS= read -r line; do [ -n "$line" ] || continue case "$tool" in staticcheck) if [[ "$line" =~ ^([^:]+):[0-9]+:[0-9]+:\ (.*)\ \(([A-Z]+[0-9]+)\)$ ]]; then # SA1019 ends its message with a space. Trim, so no baseline entry # depends on trailing whitespace surviving an editor. msg="${BASH_REMATCH[2]}" printf '%s\t%s\t%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[3]}" "${msg%"${msg##*[![:space:]]}"}" >>"$found" else printf '%s\n' "$line" >>"$malformed" fi ;; deadcode) if [[ "$line" =~ ^([^:]+):[0-9]+:[0-9]+:\ unreachable\ func:\ (.*)$ ]]; then printf '%s\tunreachable\t%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" >>"$found" else printf '%s\n' "$line" >>"$malformed" fi ;; *) printf 'analyzer-gate: unknown tool %s\n' "$tool" >&2; exit 2 ;; esac done if [ -s "$malformed" ]; then printf '%s: the analyzer said something that is not a finding:\n' "$tool" >&2 sed 's/^/ /' "$malformed" >&2 exit 1 fi accepted=$(mktemp) || exit 2 trap 'rm -f "$found" "$malformed" "$accepted"' EXIT grep -v '^[[:space:]]*\(#\|$\)' "$baseline" | sort -u >"$accepted" sort -u "$found" -o "$found" new=$(comm -23 "$found" "$accepted") gone=$(comm -13 "$found" "$accepted") status=0 if [ -n "$new" ]; then printf '%s: %d finding(s) not in %s:\n' "$tool" "$(printf '%s\n' "$new" | wc -l)" "$baseline" printf '%s\n' "$new" | sed 's/^/ /' printf 'Fix it, or add the line to the baseline with the reason it stays.\n' status=1 fi if [ -n "$gone" ]; then printf '%s: %d baseline entry/entries no longer found:\n' "$tool" "$(printf '%s\n' "$gone" | wc -l)" printf '%s\n' "$gone" | sed 's/^/ /' printf 'Delete them from %s.\n' "$baseline" status=1 fi [ "$status" -eq 0 ] && printf '%s: clean against %d accepted finding(s)\n' "$tool" "$(wc -l <"$accepted")" exit "$status"