a926383827
The 2026-08-10 audit asked for three analyzers. V-682 wired the first as `make vuln`. The other two were still absent: neither was installed on the box and no target ran them, so every reachability claim in the audit stood unchecked. `make lint` runs staticcheck v0.7.0 and `make deadcode` runs deadcode v0.48.0. Both are pinned in the Makefile beside GO_VERSION and installed into deps/bin the way govulncheck is, because a tool is not a dependency of the module. Both carry the CGO env `test` carries, or the four CGO daemons fail to load and the analyzer reports a build error instead of a finding. `make analyze` runs all three. None joins `make test`: they install over the network and `test` has to pass on a box with no route out. Neither reports zero, so neither fails on its own output. staticcheck finds 20 and deadcode finds 13, and the audit asked for an allowlist by name, because three of deadcode's eleven production symbols are deliberate and an unannotated list invites deleting them. The accepted set lives in scripts/analyzers/*.baseline, one line per finding with the reason it stays, and scripts/analyzer-gate.sh gives the verdict. A key holds file, check id and message, never a line number: a line number goes stale on the next edit above it, and a gate that reports moved findings as new ones teaches the reader to skip it. An entry whose finding is gone also fails, so a fix that leaves its line behind does not pass. deadcode runs with -test, because a test is a caller. Without the flag the report is 172 lines, most of internal/router/eval, and none of it is a mistake. With it, the 11 symbols the audit listed come back exactly, plus two test helpers it did not count. Three staticcheck findings were checked and are false positives, recorded as such: the iCal determinism test must call RenderICal twice, the morning hedge loop breaks after the first rune on purpose, and the SA9009 line is prose about //go:embed with the real directive below it. One is V-687 already. The remaining 17 are V-701 with the judgement on each. The analyzers caveat is deleted rather than edited. What replaces it is the limit that is now true: the gates are green against a baseline, not against zero.
98 lines
3.9 KiB
Bash
Executable File
98 lines
3.9 KiB
Bash
Executable File
#!/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 <staticcheck|deadcode>}"
|
|
baseline="scripts/analyzers/$tool.baseline"
|
|
[ -f "$baseline" ] || { printf 'analyzer-gate: no baseline at %s\n' "$baseline" >&2; exit 2; }
|
|
|
|
# Normalise to "<file>\t<id>\t<message>". 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"
|