forgedock 1.18.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/outcome-codes.mjs +13 -0
- package/bin/engine/phases.mjs +54 -4
- package/bin/engine/runlog.mjs +10 -2
- package/bin/engine/telemetry-seam.mjs +3 -0
- package/bin/engine/telemetry.test.mjs +3 -2
- package/bin/engine-cli.mjs +32 -2
- package/bin/engine.mjs +139 -7
- package/bin/runner.mjs +69 -9
- package/bin/stats/aggregate.mjs +7 -3
- package/bin/stats/cache.mjs +7 -3
- package/bin/stats/github-source.mjs +136 -28
- package/bin/stats/index.mjs +46 -9
- package/bin/stats/render.mjs +1 -1
- package/bin/stats/stats.test.mjs +201 -40
- package/commands/review-pr-staging.md +4 -4
- package/commands/review-pr.md +25 -15
- package/commands/work-on/build/architect.md +53 -0
- package/commands/work-on/build/implement.md +19 -0
- package/commands/work-on/build/validate.md +320 -28
- package/commands/work-on/build.md +66 -9
- package/commands/work-on/investigate.md +7 -0
- package/commands/work-on/remediate.md +32 -13
- package/commands/work-on/review.md +32 -19
- package/docs/CONFIG.md +89 -2
- package/docs/OUTCOME-CODES.md +5 -1
- package/docs/STATS.md +11 -4
- package/docs/TELEMETRY.md +5 -2
- package/docs/WORK-ON-RUNTIME.md +18 -0
- package/package.json +2 -2
- package/packages/protocol/src/types.js +1 -1
- package/scripts/acceptance-red-green.sh +90 -0
- package/scripts/acceptance-red-green.test.sh +74 -0
- package/scripts/cap-findings.test.sh +2 -0
- package/scripts/check-contract-scope.test.sh +2 -0
- package/scripts/check-edge-cases.sh +143 -0
- package/scripts/check-edge-cases.test.sh +175 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/check-test-tmp-cleanup.sh +56 -0
- package/scripts/check-test-tmp-cleanup.test.sh +69 -0
- package/scripts/ci-local/adapters/azure-pipelines.mjs +113 -0
- package/scripts/ci-local/adapters/bitbucket.mjs +64 -0
- package/scripts/ci-local/adapters/buildkite.mjs +58 -0
- package/scripts/ci-local/adapters/circleci.mjs +101 -0
- package/scripts/ci-local/adapters/common.mjs +115 -0
- package/scripts/ci-local/adapters/explicit.mjs +21 -0
- package/scripts/ci-local/adapters/github-actions.mjs +349 -0
- package/scripts/ci-local/adapters/gitlab-ci.mjs +184 -0
- package/scripts/ci-local/adapters/portability.mjs +104 -0
- package/scripts/ci-local/adapters/task-runners.mjs +67 -0
- package/scripts/ci-local/cli.mjs +50 -0
- package/scripts/ci-local/lib.mjs +371 -0
- package/scripts/ci-local/plan.mjs +186 -0
- package/scripts/ci-local/run.mjs +345 -0
- package/scripts/ci-local/scoping.mjs +508 -0
- package/scripts/ci-local.sh +97 -0
- package/scripts/ci-local.test.sh +919 -0
- package/scripts/classify-finding.sh +32 -6
- package/scripts/classify-finding.test.sh +23 -0
- package/scripts/classify-test-run.sh +69 -0
- package/scripts/classify-test-run.test.sh +90 -0
- package/scripts/close-parent-c3.test.sh +2 -0
- package/scripts/consumer-sweep.sh +569 -0
- package/scripts/consumer-sweep.test.sh +357 -0
- package/scripts/detect-append-registries.sh +106 -0
- package/scripts/detect-append-registries.test.sh +64 -0
- package/scripts/fixtures/ci-local/azure/azure-pipelines.yml +11 -0
- package/scripts/fixtures/ci-local/bitbucket/bitbucket-pipelines.yml +12 -0
- package/scripts/fixtures/ci-local/buildkite/.buildkite/pipeline.yml +10 -0
- package/scripts/fixtures/ci-local/circleci/.circleci/config.yml +29 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/ci.yml +59 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/docs.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/nightly.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/docs/index.md +1 -0
- package/scripts/fixtures/ci-local/gha-basic/src/greet.sh +2 -0
- package/scripts/fixtures/ci-local/gha-basic/tests/greet.test.sh +3 -0
- package/scripts/fixtures/ci-local/gha-dorny/.github/workflows/ci.yml +51 -0
- package/scripts/fixtures/ci-local/gha-dorny/backend/app.py +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/docs/a.md +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/frontend/app.js +1 -0
- package/scripts/fixtures/ci-local/gha-run/.github/workflows/ci.yml +39 -0
- package/scripts/fixtures/ci-local/gha-run/src/x.txt +1 -0
- package/scripts/fixtures/ci-local/gitlab/.gitlab-ci.yml +43 -0
- package/scripts/fixtures/ci-local/gitlab/docs/d.md +1 -0
- package/scripts/fixtures/ci-local/gitlab/src/a.txt +1 -0
- package/scripts/fixtures/ci-local/monorepo/.github/workflows/ci.yml +13 -0
- package/scripts/fixtures/ci-local/monorepo/package-lock.json +1 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/src/a.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/tests/a.test.sh +3 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/src/b.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/tests/b.test.sh +3 -0
- package/scripts/fixtures/ci-local/no-ci/Makefile +6 -0
- package/scripts/fixtures/ci-local/no-ci/package.json +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/a.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/b.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/tests/a.test.js +2 -0
- package/scripts/fixtures/ci-local/no-ci/tests/b.test.js +2 -0
- package/scripts/forge-root.test.sh +1 -1
- package/scripts/lib/telemetry.sh +1 -0
- package/scripts/remediate-approved-head.test.sh +2 -0
- package/scripts/remediate-basesync.test.sh +1 -1
- package/scripts/review-acceptance-gate.sh +2 -2
- package/scripts/review-acceptance-gate.test.sh +20 -0
- package/scripts/review-delta.sh +8 -1
- package/scripts/review-delta.test.sh +19 -0
- package/scripts/review-finalize.sh +193 -41
- package/scripts/review-finalize.test.sh +211 -4
- package/scripts/review-pr-delta-wiring.test.sh +13 -0
- package/scripts/review-prepare.sh +5 -3
- package/scripts/review-prepare.test.sh +18 -2
- package/scripts/run-shell-tests.sh +5 -0
- package/scripts/telemetry.test.sh +2 -0
- package/scripts/trusted-comments.test.sh +2 -1
- package/scripts/verify-phase-trail.test.sh +8 -0
- package/scripts/wait-ci-green.sh +61 -9
- package/scripts/wait-ci-green.test.sh +41 -3
- package/scripts/with-run-tmpdir.sh +20 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# consumer-sweep.sh — deterministic consumer sweep: every reference, outside the change set, to an identifier the
|
|
6
|
+
# diff removed, renamed or re-typed. Language-agnostic (fixed-string git grep); no ctags or tree-sitter needed.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# consumer-sweep.sh --range <base>..<head> [--repo P] [--exclude-glob G]... [--cap N] [--consumers FILE]
|
|
10
|
+
# [--budget-seconds N] [--mode advisory|blocking]
|
|
11
|
+
# consumer-sweep.sh --ident NAME [--ident NAME]... [--repo P] [--exclude-glob G]... [--cap N] [--budget-seconds N]
|
|
12
|
+
# consumer-sweep.sh --eval-history N [--repo P] [--exclude-glob G]... [--cap N] [--budget-seconds N]
|
|
13
|
+
#
|
|
14
|
+
# --range A..B diff mode (validate). A..B is a plain `git diff A B`. When B is the word WORKTREE the diff is
|
|
15
|
+
# merge-base(A, HEAD) against the working tree, so commits, staged, unstaged and untracked work all
|
|
16
|
+
# count as the change set (validate runs before the commit).
|
|
17
|
+
# --ident NAME identifier mode (architect, before any diff exists): sweep the named identifiers, empty change set.
|
|
18
|
+
# --consumers F gate mode (diff mode only): F is a FORGE:BUILDER body (or any text). A referencing site is
|
|
19
|
+
# addressed when its file carries a line `- <path>[:<line>] unaffected: <reason>` in F (file-level
|
|
20
|
+
# granularity; the line number is ignored because lines shift). The path is everything before the
|
|
21
|
+
# literal ` unaffected: ` token minus one trailing `:<digits>`, so any path characters work
|
|
22
|
+
# (`app/[id]/page.tsx`, `app/(auth)/login.tsx`, spaces). Files in the change set are never sites. An
|
|
23
|
+
# `updated` or `unresolved` line is informational: it never justifies a site.
|
|
24
|
+
# --budget-seconds N wall-clock budget (default 60). When it runs out the sweep stops, reports the partial result
|
|
25
|
+
# and prints BUDGET_EXCEEDED. A budget stop is advisory: it never produces exit 1.
|
|
26
|
+
# --mode M gate-mode strictness. `blocking` (default at script level): unaddressed sites exit 1. `advisory`:
|
|
27
|
+
# sites are printed as `ADVISORY:` lines and the exit is never 1. Callers pass the configured value
|
|
28
|
+
# (verification.consumer_sweep.mode).
|
|
29
|
+
# --eval-history N precision measurement: run the diff-mode sweep over the last N first-parent merges of HEAD and
|
|
30
|
+
# print flagged sites per merge plus the mean and max. Read-only; mutually exclusive with
|
|
31
|
+
# --range, --ident and --consumers.
|
|
32
|
+
#
|
|
33
|
+
# Extraction (diff mode) is a per-file survival filter over the diff. A token is swept only when it was REMOVED or
|
|
34
|
+
# RENAMED: it appears on a removed line and NOT on any added line of the same file, or it is the name on a
|
|
35
|
+
# definition head (function/class/const/def/fn/func/export ... or an assignment head) whose line changed. Tokens
|
|
36
|
+
# that survive the edit, prose words, numbers, versions and semver are never swept. Prose files (*.md, *.rst, *.txt,
|
|
37
|
+
# *.adoc) contribute only backticked tokens (fenced blocks are examples and spec-internal helpers). UPPER_CASE names count only where defined as constants. Kinds: function, class, const, env, key, flag, literal
|
|
38
|
+
# (7+ hex), path. Flags are swept without their leading dashes. Identifiers must match ^[A-Za-z0-9_.:/-]{4,80}$,
|
|
39
|
+
# must not start with "-", must contain a letter, must not be a version, are filtered through a stop-list, and are
|
|
40
|
+
# written to a pattern file consumed by ONE `git grep -n -w -F -f` as data only: diff text is never evaluated as
|
|
41
|
+
# shell or as a regex. Hits in generated/vendored paths are skipped: the same default globs as scripts/diff-size.sh
|
|
42
|
+
# (keep in sync) plus --exclude-glob (callers pass build.scope.exclude_globs).
|
|
43
|
+
#
|
|
44
|
+
# Output (sweep mode): TSV `identifier<TAB>kind<TAB>file:line`, at most --cap rows per identifier (default 25),
|
|
45
|
+
# applied before any per-hit work; a further `identifier<TAB>truncated<TAB>+N` row marks the cut (advisory, never a
|
|
46
|
+
# gate failure). A removed function/class whose name is still defined outside the change set prints
|
|
47
|
+
# `identifier<TAB>ambiguous<TAB>still defined at file:line` instead of sites (informational). An over-budget run adds `*<TAB>BUDGET_EXCEEDED<TAB>detail`. Gate mode prints
|
|
48
|
+
# `UNADDRESSED: <identifier> <file:line>` per unaddressed site (`ADVISORY:` in advisory mode), or `OK: ...`.
|
|
49
|
+
#
|
|
50
|
+
# Exit: 0 swept / all addressed (including no sites, advisory mode, and BUDGET_EXCEEDED); 1 unaddressed sites (gate
|
|
51
|
+
# mode, blocking); 2 usage error, unresolvable ref or any git failure — NEVER a pass; callers report DEGRADED.
|
|
52
|
+
#
|
|
53
|
+
# Offline: no network, no gh. Bash 3.2 portable: no mapfile, no associative arrays in the shell part.
|
|
54
|
+
|
|
55
|
+
set -uo pipefail
|
|
56
|
+
|
|
57
|
+
MAX_IDS=150
|
|
58
|
+
RANGE=""; REPO="."; CAP=25; CONSUMERS=""; IDENTS=""; EXTRA=""; BUDGET=60; MODE=blocking; EVAL=""
|
|
59
|
+
NL='
|
|
60
|
+
'
|
|
61
|
+
TAB=$(printf '\t')
|
|
62
|
+
die2() { echo "consumer-sweep: $*" >&2; exit 2; }
|
|
63
|
+
|
|
64
|
+
while [ "$#" -gt 0 ]; do
|
|
65
|
+
case "$1" in
|
|
66
|
+
--range) [ "$#" -ge 2 ] || die2 "--range needs a value"; RANGE="$2"; shift 2 ;;
|
|
67
|
+
--repo) [ "$#" -ge 2 ] || die2 "--repo needs a value"; REPO="$2"; shift 2 ;;
|
|
68
|
+
--cap) [ "$#" -ge 2 ] || die2 "--cap needs a value"; CAP="$2"; shift 2 ;;
|
|
69
|
+
--consumers) [ "$#" -ge 2 ] || die2 "--consumers needs a value"; CONSUMERS="$2"; shift 2 ;;
|
|
70
|
+
--ident) [ "$#" -ge 2 ] || die2 "--ident needs a value"; IDENTS="${IDENTS}${2}${NL}"; shift 2 ;;
|
|
71
|
+
--exclude-glob) [ "$#" -ge 2 ] || die2 "--exclude-glob needs a value"; [ -z "$2" ] || EXTRA="${EXTRA}${2}${NL}"; shift 2 ;;
|
|
72
|
+
--budget-seconds) [ "$#" -ge 2 ] || die2 "--budget-seconds needs a value"; BUDGET="$2"; shift 2 ;;
|
|
73
|
+
--mode) [ "$#" -ge 2 ] || die2 "--mode needs a value"; MODE="$2"; shift 2 ;;
|
|
74
|
+
--eval-history) [ "$#" -ge 2 ] || die2 "--eval-history needs a value"; EVAL="$2"; shift 2 ;;
|
|
75
|
+
*) die2 "usage: consumer-sweep.sh (--range <base>..<head> [--consumers FILE] | --ident NAME... | --eval-history N) [--repo P] [--exclude-glob G]... [--cap N] [--budget-seconds N] [--mode advisory|blocking]" ;;
|
|
76
|
+
esac
|
|
77
|
+
done
|
|
78
|
+
case "$CAP" in ""|0|*[!0-9]*) die2 "--cap must be a positive integer" ;; esac
|
|
79
|
+
case "$BUDGET" in ""|*[!0-9]*) die2 "--budget-seconds must be a non-negative integer" ;; esac
|
|
80
|
+
case "$MODE" in advisory|blocking) ;; *) die2 "--mode must be advisory or blocking" ;; esac
|
|
81
|
+
if [ -n "$EVAL" ]; then
|
|
82
|
+
case "$EVAL" in 0|*[!0-9]*) die2 "--eval-history must be a positive integer" ;; esac
|
|
83
|
+
if [ -n "$RANGE$IDENTS$CONSUMERS" ]; then die2 "--eval-history is mutually exclusive with --range, --ident and --consumers"; fi
|
|
84
|
+
else
|
|
85
|
+
if [ -n "$RANGE" ] && [ -n "$IDENTS" ]; then die2 "--range and --ident are mutually exclusive"; fi
|
|
86
|
+
if [ -z "$RANGE" ] && [ -z "$IDENTS" ]; then die2 "one of --range, --ident or --eval-history is required"; fi
|
|
87
|
+
fi
|
|
88
|
+
if [ -n "$CONSUMERS" ]; then
|
|
89
|
+
[ -n "$RANGE" ] || die2 "--consumers requires --range"
|
|
90
|
+
[ -f "$CONSUMERS" ] || die2 "consumers file not found: $CONSUMERS"
|
|
91
|
+
fi
|
|
92
|
+
git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1 || die2 "not a git work tree: $REPO"
|
|
93
|
+
SECONDS=0
|
|
94
|
+
|
|
95
|
+
# ---- precision evaluation over merge history ------------------------------------------------------------
|
|
96
|
+
if [ -n "$EVAL" ]; then
|
|
97
|
+
SELF="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
|
|
98
|
+
MERGES=$(git -C "$REPO" log --merges --first-parent -n "$EVAL" --format=%H HEAD 2>/dev/null) || die2 "git log failed"
|
|
99
|
+
[ -n "$MERGES" ] || die2 "no merge commits found for --eval-history"
|
|
100
|
+
SUMMARY=""
|
|
101
|
+
for m in $MERGES; do
|
|
102
|
+
set -- --repo "$REPO" --range "${m}^1..${m}" --budget-seconds "$BUDGET" --cap "$CAP"
|
|
103
|
+
while IFS= read -r g; do [ -z "$g" ] || set -- "$@" --exclude-glob "$g"; done <<EOF
|
|
104
|
+
$EXTRA
|
|
105
|
+
EOF
|
|
106
|
+
out=$(bash "$SELF" "$@" 2>/dev/null); rcx=$?
|
|
107
|
+
subj=$(git -C "$REPO" log -n 1 --format=%s "$m" 2>/dev/null | cut -c1-70)
|
|
108
|
+
if [ "$rcx" -ne 0 ]; then
|
|
109
|
+
printf 'eval\t%s\terror(rc=%s)\t%s\n' "$(printf '%s' "$m" | cut -c1-10)" "$rcx" "$subj"
|
|
110
|
+
continue
|
|
111
|
+
fi
|
|
112
|
+
stat=$(printf '%s\n' "$out" | awk -F'\t' '
|
|
113
|
+
$2 == "BUDGET_EXCEEDED" { b = 1; next }
|
|
114
|
+
$2 == "ambiguous" { next }
|
|
115
|
+
$2 == "truncated" { v = $3; sub(/^\+/, "", v); sites += v + 0; next }
|
|
116
|
+
NF >= 3 { sites++; ids[$1] = 1 }
|
|
117
|
+
END { n = 0; for (k in ids) n++; print sites + 0 "\t" n "\t" (b ? "budget" : "") }')
|
|
118
|
+
sites=${stat%%"$TAB"*}
|
|
119
|
+
printf 'eval\t%s\tsites=%s\t%s\t%s\n' "$(printf '%s' "$m" | cut -c1-10)" "$sites" "$(printf '%s' "$stat" | cut -f3)" "$subj"
|
|
120
|
+
SUMMARY="${SUMMARY}${sites}${NL}"
|
|
121
|
+
done
|
|
122
|
+
printf '%s' "$SUMMARY" | awk '
|
|
123
|
+
NF { n++; t += $1; if ($1 + 0 > mx) mx = $1 + 0 }
|
|
124
|
+
END {
|
|
125
|
+
if (n == 0) { print "mean=0.00 max=0 prs=0 guard=UNKNOWN"; exit }
|
|
126
|
+
mean = t / n
|
|
127
|
+
printf "mean=%.2f max=%d prs=%d guard=%s\n", mean, mx, n, (mean <= 2 && mx <= 10) ? "PASS" : "FAIL"
|
|
128
|
+
}'
|
|
129
|
+
exit 0
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
TMP=$(mktemp -d "${TMPDIR:-/tmp}/consumer-sweep.XXXXXX") || die2 "mktemp failed"
|
|
133
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
134
|
+
|
|
135
|
+
# ---- exclude globs (mirror of scripts/diff-size.sh: keep in sync) ---------------------------------------
|
|
136
|
+
DEFAULTS="package-lock.json yarn.lock pnpm-lock.yaml Cargo.lock poetry.lock uv.lock go.sum Gemfile.lock composer.lock *.min.* *.snap __generated__/ __snapshots__/"
|
|
137
|
+
ROOT_DIRS="dist/ build/ generated/ fixtures/ vendor/"
|
|
138
|
+
GLOBS="${DEFAULTS}${NL}${EXTRA}"
|
|
139
|
+
excluded() { # path -> 0 when excluded (globs are data: matched by case, never evaluated)
|
|
140
|
+
local p="$1" b="${1##*/}" g
|
|
141
|
+
for g in $ROOT_DIRS; do case "$p" in "$g"*) return 0 ;; esac; done
|
|
142
|
+
set -f
|
|
143
|
+
for g in $GLOBS; do
|
|
144
|
+
case "$g" in
|
|
145
|
+
*/) case "/$p" in *"/$g"*) set +f; return 0 ;; esac ;;
|
|
146
|
+
*) case "$b" in $g) set +f; return 0 ;; esac; case "$p" in $g) set +f; return 0 ;; esac ;;
|
|
147
|
+
esac
|
|
148
|
+
done
|
|
149
|
+
set +f
|
|
150
|
+
return 1
|
|
151
|
+
}
|
|
152
|
+
# the same rules, one per line, for the awk-side filter: R:<root prefix> D:<dir glob> G:<name/path glob>
|
|
153
|
+
: > "$TMP/globs"
|
|
154
|
+
for g in $ROOT_DIRS; do printf 'R:%s\n' "$g" >> "$TMP/globs"; done
|
|
155
|
+
set -f
|
|
156
|
+
for g in $GLOBS; do
|
|
157
|
+
case "$g" in */) printf 'D:%s\n' "$g" >> "$TMP/globs" ;; *) printf 'G:%s\n' "$g" >> "$TMP/globs" ;; esac
|
|
158
|
+
done
|
|
159
|
+
set +f
|
|
160
|
+
|
|
161
|
+
: > "$TMP/changed"
|
|
162
|
+
: > "$TMP/excl"
|
|
163
|
+
: > "$TMP/raw"
|
|
164
|
+
OVER=0; OVERWHY=""
|
|
165
|
+
|
|
166
|
+
# ---- diff mode: change set and per-file survival extraction ---------------------------------------------
|
|
167
|
+
if [ -n "$RANGE" ]; then
|
|
168
|
+
case "$RANGE" in *..*) ;; *) die2 "--range must look like <base>..<head>" ;; esac
|
|
169
|
+
A="${RANGE%%..*}"; B="${RANGE#*..}"; B="${B#.}"
|
|
170
|
+
case "$A" in ""|-*) die2 "invalid base in --range: '$A'" ;; esac
|
|
171
|
+
case "$B" in ""|-*) die2 "invalid head in --range: '$B'" ;; esac
|
|
172
|
+
git -C "$REPO" rev-parse --verify --quiet "${A}^{commit}" >/dev/null 2>&1 || die2 "cannot resolve base ref '$A'"
|
|
173
|
+
if [ "$B" = WORKTREE ]; then
|
|
174
|
+
MB=$(git -C "$REPO" merge-base "$A" HEAD 2>/dev/null) && [ -n "$MB" ] || die2 "cannot compute merge base of '$A' and HEAD"
|
|
175
|
+
DIFFARGS="$MB"
|
|
176
|
+
git -C "$REPO" diff -z --name-only --no-renames "$MB" -- > "$TMP/c1" 2>/dev/null || die2 "git diff --name-only failed"
|
|
177
|
+
git -C "$REPO" ls-files -z --others --exclude-standard > "$TMP/c2" 2>/dev/null || die2 "git ls-files failed"
|
|
178
|
+
else
|
|
179
|
+
git -C "$REPO" rev-parse --verify --quiet "${B}^{commit}" >/dev/null 2>&1 || die2 "cannot resolve head ref '$B'"
|
|
180
|
+
DIFFARGS="$A $B"
|
|
181
|
+
# shellcheck disable=SC2086
|
|
182
|
+
git -C "$REPO" diff -z --name-only --no-renames $DIFFARGS -- > "$TMP/c1" 2>/dev/null || die2 "git diff --name-only failed"
|
|
183
|
+
: > "$TMP/c2"
|
|
184
|
+
fi
|
|
185
|
+
cat "$TMP/c1" "$TMP/c2" | tr '\0' '\n' | sed '/^$/d' | sort -u > "$TMP/changed"
|
|
186
|
+
# files whose lines are not extracted (generated/vendored)
|
|
187
|
+
while IFS= read -r f; do
|
|
188
|
+
[ -n "$f" ] || continue
|
|
189
|
+
if excluded "$f"; then printf '%s\n' "$f" >> "$TMP/excl"; fi
|
|
190
|
+
done < "$TMP/changed"
|
|
191
|
+
# shellcheck disable=SC2086
|
|
192
|
+
git -C "$REPO" -c core.quotepath=off diff -U0 --no-color --no-ext-diff --no-renames $DIFFARGS -- > "$TMP/diff" 2>/dev/null \
|
|
193
|
+
|| die2 "git diff failed"
|
|
194
|
+
|
|
195
|
+
# One awk pass: removed and added lines are collected per file (a "--- a/<path>" / "+++ b/<path>" header is read
|
|
196
|
+
# only before the first @@ of a file, so removed "-- " lines are never mistaken for headers), then every
|
|
197
|
+
# candidate token of a removed line is kept only when it did not survive in that file's added lines.
|
|
198
|
+
cat > "$TMP/extract.awk" <<'AWK'
|
|
199
|
+
function trim(s) { gsub(/^[ \t]+|[ \t]+$/, "", s); gsub(/[ \t]+/, " ", s); return s }
|
|
200
|
+
function isprose(f) { f = tolower(f); return f ~ /\.(md|markdown|mdx|rst|txt|adoc|org)$/ }
|
|
201
|
+
function addl(F, s, n, i, parts) {
|
|
202
|
+
anl[F]++; al[F, anl[F]] = s
|
|
203
|
+
gl[trim(s)] = 1
|
|
204
|
+
n = split(s, parts, /[^A-Za-z0-9_]+/)
|
|
205
|
+
for (i = 1; i <= n; i++) if (parts[i] != "") aw[F, parts[i]] = 1
|
|
206
|
+
}
|
|
207
|
+
function spans(s, out) {
|
|
208
|
+
out = "" # spans are joined with a separator so two spans never read as one "keyword NAME" declaration
|
|
209
|
+
while (match(s, /`[^`]+`/)) { out = out " \001 " substr(s, RSTART + 1, RLENGTH - 2); s = substr(s, RSTART + RLENGTH) }
|
|
210
|
+
return out
|
|
211
|
+
}
|
|
212
|
+
# every maximal word-bounded run of identifier characters in one file's added lines, built once per file on first use
|
|
213
|
+
# (O(added chars x 80)), so a survival test is one hash lookup instead of a scan of every added line per candidate
|
|
214
|
+
function build(F, k, line, n, i, j, c, pc, nc2) {
|
|
215
|
+
built[F] = 1
|
|
216
|
+
for (k = 1; k <= anl[F]; k++) {
|
|
217
|
+
line = al[F, k]; n = length(line)
|
|
218
|
+
for (i = 1; i <= n; i++) {
|
|
219
|
+
c = substr(line, i, 1)
|
|
220
|
+
if (c !~ /[A-Za-z0-9_.:\/-]/) continue
|
|
221
|
+
pc = (i > 1) ? substr(line, i - 1, 1) : ""
|
|
222
|
+
if (pc ~ /[A-Za-z0-9_]/) continue
|
|
223
|
+
for (j = i; j <= n && j < i + 80; j++) {
|
|
224
|
+
if (substr(line, j, 1) !~ /[A-Za-z0-9_.:\/-]/) break
|
|
225
|
+
nc2 = (j < n) ? substr(line, j + 1, 1) : ""
|
|
226
|
+
if (nc2 !~ /[A-Za-z0-9_]/) sx[F, substr(line, i, j - i + 1)] = 1
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function survives(F, id) {
|
|
232
|
+
if (id ~ /^[A-Za-z0-9_]+$/) return ((F SUBSEP id) in aw)
|
|
233
|
+
if (!(F in built)) build(F)
|
|
234
|
+
return ((F SUBSEP id) in sx)
|
|
235
|
+
}
|
|
236
|
+
# a candidate must be code-like: charset, 4..80 chars, a letter, not a version/number, not a stop word
|
|
237
|
+
function cand(F, id, kind, head, norm) {
|
|
238
|
+
if (id !~ /^[A-Za-z0-9_.:\/-]+$/) return
|
|
239
|
+
if (length(id) < 4 || length(id) > 80 || substr(id, 1, 1) == "-") return
|
|
240
|
+
if (id !~ /[A-Za-z]/) return
|
|
241
|
+
if (id ~ /^[vV]?[0-9][0-9.]*([-+][A-Za-z0-9.+-]*)?$/) return
|
|
242
|
+
if (id ~ /^[vV]?[0-9]+\.[0-9]+\.[0-9]+/) return
|
|
243
|
+
if (tolower(id) in st) return
|
|
244
|
+
nc++; cF[nc] = F; cid[nc] = id; ckind[nc] = kind; chead[nc] = head; cnorm[nc] = norm
|
|
245
|
+
}
|
|
246
|
+
function scan(F, t, norm, prose, pmode, s, m, kw, nm, k, tok, ok, ext, comment, n, sp) {
|
|
247
|
+
comment = (!prose && t ~ /^[ \t]*(#|\/\/|\*|\/\*|--[ \t]|;)/)
|
|
248
|
+
# declarations: keyword [receiver] NAME -> kind by keyword (a definition head: the whole line is compared)
|
|
249
|
+
if (!comment) {
|
|
250
|
+
s = t
|
|
251
|
+
while (match(s, /(^|[^A-Za-z0-9_])(def|function|func|fn|fun|sub|proc|class|struct|interface|enum|trait|type|const|val)[ \t]+(\([^)]*\)[ \t]*)?[A-Za-z_][A-Za-z0-9_]*/)) {
|
|
252
|
+
m = substr(s, RSTART, RLENGTH); s = substr(s, RSTART + RLENGTH)
|
|
253
|
+
kw = m; sub(/^[^A-Za-z]/, "", kw); sub(/[ \t].*$/, "", kw)
|
|
254
|
+
nm = m; sub(/^.*[ \t)]/, "", nm)
|
|
255
|
+
if (substr(s, 1, 1) !~ /[({<:=\[]/ && s !~ /^[ \t]*([({<:=\[]|$)/) continue
|
|
256
|
+
k = "const"
|
|
257
|
+
if (kw == "def" || kw == "function" || kw == "func" || kw == "fn" || kw == "fun" || kw == "sub" || kw == "proc") k = "function"
|
|
258
|
+
else if (kw == "class" || kw == "struct" || kw == "interface" || kw == "enum" || kw == "trait" || kw == "type") k = "class"
|
|
259
|
+
# a const/val is an API symbol only at top level or with an access modifier; an indented one is a local variable
|
|
260
|
+
if (kw == "const" || kw == "val") {
|
|
261
|
+
if (t !~ /^(const|val)[ \t]/ && t !~ /^[ \t]*((export|pub|public|private|protected|static|final|readonly|default|declare)[ \t]+)+(const|val)[ \t]/) continue
|
|
262
|
+
}
|
|
263
|
+
# only a function/class head is a signature: a changed const/val value does not invalidate its consumers
|
|
264
|
+
cand(F, nm, k, (k == "const") ? 0 : 1, norm)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
# shell-style name() {
|
|
268
|
+
if (match(t, /^[ \t]*[A-Za-z_][A-Za-z0-9_]*\(\)[ \t]*\{/)) {
|
|
269
|
+
nm = substr(t, RSTART, RLENGTH); sub(/^[ \t]*/, "", nm); sub(/\(\).*$/, "", nm)
|
|
270
|
+
cand(F, nm, "function", 1, norm)
|
|
271
|
+
}
|
|
272
|
+
# env vars: a quoted UPPER_CASE name, or process.env.NAME, on a line that reads the environment
|
|
273
|
+
if (t ~ /getenv|environ|process\.env|os\.Getenv|ENV\[|env:|\.env/) {
|
|
274
|
+
s = t
|
|
275
|
+
while (match(s, /["'][A-Z][A-Z0-9_]*["']|process\.env\.[A-Z][A-Z0-9_]*/)) {
|
|
276
|
+
tok = substr(s, RSTART, RLENGTH); s = substr(s, RSTART + RLENGTH)
|
|
277
|
+
sub(/^process\.env\./, "", tok); gsub(/["']/, "", tok)
|
|
278
|
+
cand(F, tok, "env", 0, norm)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
# UPPER_CASE constants: only where the token is DEFINED as a constant. A modifier head (export/const/static/final/
|
|
282
|
+
# readonly/#define ...), a column-0 assignment in a non-shell file, or an assignment in a key/value config file. A
|
|
283
|
+
# shell variable (shell file or fenced shell in prose) is a local, never a constant. A backticked UPPER_SNAKE
|
|
284
|
+
# mention in prose is a documented constant.
|
|
285
|
+
s = t
|
|
286
|
+
while (match(s, /(^|[^A-Za-z0-9_])[A-Z][A-Z0-9_]*([^A-Za-z0-9_]|$)/)) {
|
|
287
|
+
tok = substr(s, RSTART, RLENGTH); s = substr(s, RSTART + RLENGTH)
|
|
288
|
+
sub(/^[^A-Z]/, "", tok); sub(/[^A-Z0-9_]$/, "", tok)
|
|
289
|
+
if (length(tok) < 4) continue
|
|
290
|
+
ok = 0
|
|
291
|
+
if (t ~ ("^[ \t]*((export|const|static|final|readonly|public|private|protected|declare|pub|let|var)[ \t]+)+" tok "([^A-Za-z0-9_]|$)") || t ~ ("^[ \t]*#[ \t]*define[ \t]+" tok "([^A-Za-z0-9_]|$)")) ok = 1
|
|
292
|
+
else if (pmode == 1) ok = (index(tok, "_") > 0)
|
|
293
|
+
else if (pmode == 0 && t !~ ("^[ \t]*" tok "[ \t]*==") && t ~ ("^" tok "[ \t]*[:=]") && F !~ /\.(sh|bash|zsh|ksh)$/) ok = 1
|
|
294
|
+
else if (pmode == 0 && t ~ ("^[ \t]*" tok "[ \t]*[:=]") && F ~ /\.(ya?ml|toml|ini|env|properties|cfg|conf)$/) ok = 1
|
|
295
|
+
if (ok) cand(F, tok, "const", 0, norm)
|
|
296
|
+
}
|
|
297
|
+
# CLI flags (swept without the leading dashes)
|
|
298
|
+
s = t
|
|
299
|
+
while (match(s, /(^|[^A-Za-z0-9_-])--[A-Za-z][A-Za-z0-9_-]*/)) {
|
|
300
|
+
nm = substr(s, RSTART, RLENGTH); s = substr(s, RSTART + RLENGTH)
|
|
301
|
+
sub(/^[^-]?--/, "", nm); sub(/-+$/, "", nm)
|
|
302
|
+
# a flag is swept where the changed file DEFINES it (parser registration, case arm, or a documented option), never
|
|
303
|
+
# where it merely passes the flag to another tool
|
|
304
|
+
if (length(nm) >= 4 && (t ~ /add_argument|add_option|\.option\(|\.flag\(|Flag\(|flag\.[A-Z][A-Za-z]*\(|getopt|argparse|click\.option|\.Var\(/ \
|
|
305
|
+
|| t ~ ("^[ \t]*(-[A-Za-z][ \t]*\\|[ \t]*)?--" nm "(=[^ \t|)]*)?[ \t]*(\\||\\))") \
|
|
306
|
+
|| (prose && t ~ ("^[ \t]*--" nm "([^A-Za-z0-9_-]|$)")))) cand(F, nm, "flag", 0, norm)
|
|
307
|
+
}
|
|
308
|
+
# literals: 7+ hex that mixes digits and a-f (a pinned hash); semver and plain numbers are never swept
|
|
309
|
+
s = t
|
|
310
|
+
while (match(s, /(^|[^A-Za-z0-9_])[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]+([^A-Za-z0-9_]|$)/)) {
|
|
311
|
+
tok = substr(s, RSTART, RLENGTH); s = substr(s, RSTART + RLENGTH)
|
|
312
|
+
sub(/^[^0-9a-f]/, "", tok); sub(/[^0-9a-f]$/, "", tok)
|
|
313
|
+
if (length(tok) <= 64 && tok ~ /[0-9]/ && tok ~ /[a-f]/) cand(F, tok, "literal", 0, norm)
|
|
314
|
+
}
|
|
315
|
+
# quoted config/context keys
|
|
316
|
+
s = t
|
|
317
|
+
while (match(s, /["'][A-Za-z_][A-Za-z0-9_.-]*["']/)) {
|
|
318
|
+
tok = substr(s, RSTART + 1, RLENGTH - 2); s = substr(s, RSTART + RLENGTH)
|
|
319
|
+
# a quoted string is a key only in key position (followed by a colon); other strings are messages, values or uses
|
|
320
|
+
if (substr(s, 1, 1) == ":" || s ~ /^[ \t]*:/) cand(F, tok, "key", 0, norm)
|
|
321
|
+
}
|
|
322
|
+
# prose: the specific identifier a list item or table row DOCUMENTS (a leading backticked span such as
|
|
323
|
+
# "- `legacy_option` ..." or "| `legacy_option` |"); a mid-sentence mention is a use, not a definition
|
|
324
|
+
if (pmode == 1 && match(norm, /^([-*+]|\|)[ \t]*`[A-Za-z_][A-Za-z0-9_.-]*`/)) {
|
|
325
|
+
tok = substr(norm, RSTART, RLENGTH); sub(/^[^`]*`/, "", tok); sub(/`$/, "", tok)
|
|
326
|
+
if (tok ~ /[_.-]/ || tok ~ /.[A-Z]/) cand(F, tok, "ident", 0, norm)
|
|
327
|
+
}
|
|
328
|
+
# yaml-style keys (never from prose files)
|
|
329
|
+
if (!prose && match(t, /^[ \t]*(-[ \t]+)?[A-Za-z_][A-Za-z0-9_.-]*:([ \t]|$)/)) {
|
|
330
|
+
tok = substr(t, RSTART, RLENGTH); sub(/^[ \t]*(-[ \t]+)?/, "", tok); sub(/:[ \t]?$/, "", tok)
|
|
331
|
+
cand(F, tok, "key", 0, norm)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
BEGIN {
|
|
335
|
+
while ((getline l < excl) > 0) skip[l] = 1
|
|
336
|
+
n = split(xstop, sw, " ")
|
|
337
|
+
for (i = 1; i <= n; i++) st[tolower(sw[i])] = 1
|
|
338
|
+
}
|
|
339
|
+
/^diff --git / { inhunk = 0; next }
|
|
340
|
+
/^@@ / { inhunk = 1; hunk++; next }
|
|
341
|
+
!inhunk && /^--- / { p = substr($0, 5); sub(/\t.*$/, "", p); sub(/^a\//, "", p); cur = p; next }
|
|
342
|
+
!inhunk && /^\+\+\+ / {
|
|
343
|
+
p = substr($0, 5); sub(/\t.*$/, "", p); sub(/^b\//, "", p); curb = p
|
|
344
|
+
# a deleted or moved-away file: its own path is an identifier whose references go stale
|
|
345
|
+
if (p == "/dev/null" && cur != "/dev/null" && !(cur in skip)) cand(cur, cur, "path", 0, "")
|
|
346
|
+
next
|
|
347
|
+
}
|
|
348
|
+
inhunk && /^-/ { if (!(cur in skip) && cur != "/dev/null") { nr++; rf[nr] = cur; rt[nr] = substr($0, 2); rh[nr] = hunk } next }
|
|
349
|
+
inhunk && /^\+/ { if (curb != "/dev/null" && !(curb in skip)) addl(curb, substr($0, 2)); next }
|
|
350
|
+
END {
|
|
351
|
+
lasth = -1
|
|
352
|
+
for (i = 1; i <= nr; i++) {
|
|
353
|
+
F = rf[i]; line = rt[i]
|
|
354
|
+
if (rh[i] != lasth) { fence = 0; lasth = rh[i] }
|
|
355
|
+
prose = isprose(F); text = line
|
|
356
|
+
if (prose) {
|
|
357
|
+
if (line ~ /^[ \t]*(```|~~~)/) { fence = !fence; continue }
|
|
358
|
+
# fenced blocks in prose are examples and spec-internal helpers (shell locals, sample JSON): not definitions
|
|
359
|
+
if (fence) continue
|
|
360
|
+
text = spans(line)
|
|
361
|
+
if (text == "") continue
|
|
362
|
+
}
|
|
363
|
+
scan(F, text, trim(line), prose, prose ? 1 : 0)
|
|
364
|
+
}
|
|
365
|
+
for (c = 1; c <= nc; c++) {
|
|
366
|
+
# a definition head whose exact line still exists in the added side (same file or moved) did not change
|
|
367
|
+
if (chead[c] && (cnorm[c] in gl)) continue
|
|
368
|
+
# a token that survives in the file's added lines was edited around, not removed; only a changed head counts
|
|
369
|
+
if (survives(cF[c], cid[c]) && !chead[c]) continue
|
|
370
|
+
print cid[c] "\t" ckind[c]
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
AWK
|
|
374
|
+
XSTOP="that this with from have will your into than then them they were been being their there these those which while where when what would could should about after before because between both each other such only also more most some many much very just like make made used uses using need needs needed must may might shall can cannot does done doing not nor yet but and for are the any all one two use via per set get put add see run runs also else elif try catch finally case switch default return returns import export from yield await async static public private protected final override begin end then fi done esac function class struct enum type none null true false undefined void version versions note notes todo fixme hack example examples see below above following here step steps stage phase section sections item items line lines word words text name names title label labels body bodies content contents message messages description summary details detail info information option options param params parameter parameters arg args argument arguments key keys value values field fields row rows column columns table tables list lists array arrays map maps string strings number numbers boolean object objects element elements node nodes child children parent parents root base head tail first last next prev previous current latest main master release releases branch branches commit commits change changes changed file files path paths dir dirs directory folder folders link links url urls href src dest target targets source sources input inputs output outputs result results error errors warn warning warnings debug trace level levels mode modes state states status user users admin id ids uid uuid time date datetime timestamp when once always never true false enabled disabled enable disable active inactive required optional allow deny allowed denied yes no on off"
|
|
375
|
+
# the extraction runs inside the same --budget-seconds deadline as the grep: over budget is advisory, never a failure
|
|
376
|
+
REM=$((BUDGET - SECONDS)); TO=""
|
|
377
|
+
if command -v timeout >/dev/null 2>&1; then TO="timeout"; elif command -v gtimeout >/dev/null 2>&1; then TO="gtimeout"; fi
|
|
378
|
+
if [ "$REM" -le 0 ]; then
|
|
379
|
+
OVER=1; OVERWHY="budget ${BUDGET}s spent before the extraction started"
|
|
380
|
+
else
|
|
381
|
+
${TO:+"$TO" "$REM"} awk -v excl="$TMP/excl" -v xstop="$XSTOP" -f "$TMP/extract.awk" "$TMP/diff" 2>/dev/null > "$TMP/raw"
|
|
382
|
+
xrc=$?
|
|
383
|
+
if [ "$xrc" -eq 124 ]; then
|
|
384
|
+
: > "$TMP/raw"; OVER=1; OVERWHY="budget ${BUDGET}s elapsed during the extraction; results are partial"
|
|
385
|
+
elif [ "$xrc" -ne 0 ]; then die2 "extraction failed"; fi
|
|
386
|
+
fi
|
|
387
|
+
else
|
|
388
|
+
# ---- identifier mode ------------------------------------------------------------------------------------
|
|
389
|
+
printf '%s' "$IDENTS" | sed '/^$/d' | awk '{ print $0 "\tident" }' > "$TMP/raw"
|
|
390
|
+
fi
|
|
391
|
+
|
|
392
|
+
# ---- filter: first-seen per identifier, charset, length, stop-list -------------------------------------
|
|
393
|
+
STOP="true false null none self this main init test tests data name type types value values list dict item items result results error errors string default config options args kwargs size path file files text node nodes index count state status user users TODO FIXME NOTE HACK NULL TRUE FALSE NONE UTF8 JSON YAML HTML HTTP HTTPS GET POST PUT PATCH DELETE ERROR WARN INFO DEBUG EOF LINE MAIN version VERSION"
|
|
394
|
+
awk -F'\t' -v stop="$STOP" '
|
|
395
|
+
BEGIN { n = split(stop, s, " "); for (i = 1; i <= n; i++) st[tolower(s[i])] = 1 }
|
|
396
|
+
$1 ~ /^[A-Za-z0-9_.:\/-]+$/ && length($1) >= 4 && length($1) <= 80 && substr($1, 1, 1) != "-" && $1 ~ /[A-Za-z]/ \
|
|
397
|
+
&& $1 !~ /^[vV]?[0-9][0-9.]*([-+][A-Za-z0-9.+-]*)?$/ \
|
|
398
|
+
&& !(tolower($1) in st) && !seen[$1]++ { print $1 "\t" $2 }
|
|
399
|
+
' "$TMP/raw" > "$TMP/ids"
|
|
400
|
+
if [ -z "$RANGE" ]; then
|
|
401
|
+
# an explicit --ident that fails validation is a usage error, never silently skipped
|
|
402
|
+
want=$(printf '%s' "$IDENTS" | sed '/^$/d' | wc -l | tr -d ' ')
|
|
403
|
+
have=$(wc -l < "$TMP/ids" | tr -d ' ')
|
|
404
|
+
[ "$want" = "$have" ] || die2 "invalid --ident (want ^[A-Za-z0-9_.:/-]{4,80}\$, not a stop-list word, no duplicates)"
|
|
405
|
+
fi
|
|
406
|
+
|
|
407
|
+
# ---- sweep: ONE bounded git grep -----------------------------------------------------------------------
|
|
408
|
+
: > "$TMP/sites" # identifier<TAB>kind<TAB>file:line
|
|
409
|
+
NIDS=$(wc -l < "$TMP/ids" | tr -d ' ')
|
|
410
|
+
if [ "$NIDS" -gt "$MAX_IDS" ]; then
|
|
411
|
+
head -n "$MAX_IDS" "$TMP/ids" > "$TMP/ids.cut"; mv "$TMP/ids.cut" "$TMP/ids"
|
|
412
|
+
fi
|
|
413
|
+
if [ -s "$TMP/ids" ]; then
|
|
414
|
+
cut -f1 "$TMP/ids" > "$TMP/idents"
|
|
415
|
+
REM=$((BUDGET - SECONDS))
|
|
416
|
+
if [ "$REM" -le 0 ]; then
|
|
417
|
+
OVER=1; OVERWHY="budget ${BUDGET}s spent before the grep started"
|
|
418
|
+
else
|
|
419
|
+
TO=""
|
|
420
|
+
if command -v timeout >/dev/null 2>&1; then TO="timeout"; elif command -v gtimeout >/dev/null 2>&1; then TO="gtimeout"; fi
|
|
421
|
+
if [ -n "$TO" ]; then
|
|
422
|
+
"$TO" "$REM" git -C "$REPO" grep -n -z -I -w -F -f "$TMP/idents" -- . > "$TMP/g" 2> "$TMP/gerr"
|
|
423
|
+
else
|
|
424
|
+
git -C "$REPO" grep -n -z -I -w -F -f "$TMP/idents" -- . > "$TMP/g" 2> "$TMP/gerr"
|
|
425
|
+
fi
|
|
426
|
+
grc=$?
|
|
427
|
+
if [ "$grc" -eq 124 ]; then
|
|
428
|
+
OVER=1; OVERWHY="budget ${BUDGET}s elapsed during the grep; results are partial"
|
|
429
|
+
elif [ "$grc" -gt 1 ]; then
|
|
430
|
+
die2 "git grep failed (rc=$grc): $(head -n 1 "$TMP/gerr")"
|
|
431
|
+
fi
|
|
432
|
+
if [ -s "$TMP/g" ]; then
|
|
433
|
+
# group, drop the change set and excluded paths, cap per identifier before any per-hit work: one awk, no forks
|
|
434
|
+
tr '\0' '\001' < "$TMP/g" | awk -v cap="$CAP" -v plainprose="$([ -n "$RANGE" ] && echo 1 || echo 0)" -v ids="$TMP/ids" -v changed="$TMP/changed" -v globs="$TMP/globs" '
|
|
435
|
+
function globre(g, i, c, r) {
|
|
436
|
+
r = "^"
|
|
437
|
+
for (i = 1; i <= length(g); i++) {
|
|
438
|
+
c = substr(g, i, 1)
|
|
439
|
+
if (c == "*") r = r ".*"
|
|
440
|
+
else if (c == "?") r = r "."
|
|
441
|
+
else if (c ~ /[.+(){}|^$\\\[\]]/) r = r "\\" c
|
|
442
|
+
else r = r c
|
|
443
|
+
}
|
|
444
|
+
return r "$"
|
|
445
|
+
}
|
|
446
|
+
function isexcl(p, i, b) {
|
|
447
|
+
for (i = 1; i <= nroot; i++) if (index(p, roots[i]) == 1) return 1
|
|
448
|
+
for (i = 1; i <= ndir; i++) if (index("/" p, "/" dirs[i]) > 0) return 1
|
|
449
|
+
b = p; sub(/^.*\//, "", b)
|
|
450
|
+
for (i = 1; i <= ng; i++) if (b ~ gre[i] || p ~ gre[i]) return 1
|
|
451
|
+
return 0
|
|
452
|
+
}
|
|
453
|
+
function hasword(text, id, start, p, abs, prev, nx) {
|
|
454
|
+
start = 1
|
|
455
|
+
while ((p = index(substr(text, start), id)) > 0) {
|
|
456
|
+
abs = start + p - 1
|
|
457
|
+
prev = (abs > 1) ? substr(text, abs - 1, 1) : ""
|
|
458
|
+
nx = substr(text, abs + length(id), 1)
|
|
459
|
+
if (prev !~ /[A-Za-z0-9_]/ && nx !~ /[A-Za-z0-9_]/) return 1
|
|
460
|
+
start = abs + 1
|
|
461
|
+
}
|
|
462
|
+
return 0
|
|
463
|
+
}
|
|
464
|
+
# the name a hit line DEFINES (keyword NAME, or shell name() {), or ""
|
|
465
|
+
function defname(text, m, nm, r) {
|
|
466
|
+
if (match(text, /^[ \t]*[A-Za-z_][A-Za-z0-9_]*\(\)[ \t]*\{/)) {
|
|
467
|
+
nm = substr(text, RSTART, RLENGTH); sub(/^[ \t]*/, "", nm); sub(/\(\).*$/, "", nm); return nm
|
|
468
|
+
}
|
|
469
|
+
if (match(text, /(^|[^A-Za-z0-9_])(def|function|func|fn|fun|sub|proc|class|struct|interface|enum|trait|type)[ \t]+(\([^)]*\)[ \t]*)?[A-Za-z_][A-Za-z0-9_]*/)) {
|
|
470
|
+
nm = substr(text, RSTART, RLENGTH); sub(/^.*[ \t)]/, "", nm); return nm
|
|
471
|
+
}
|
|
472
|
+
return ""
|
|
473
|
+
}
|
|
474
|
+
BEGIN {
|
|
475
|
+
FS = "\001"
|
|
476
|
+
while ((getline l < ids) > 0) {
|
|
477
|
+
split(l, a, "\t"); N++; iid[N] = a[1]; ikind[N] = a[2]
|
|
478
|
+
if (a[1] ~ /^[A-Za-z0-9_]+$/) wid[a[1]] = N; else sid[++ns] = N
|
|
479
|
+
# a plain lowercase word is generic English in prose: it is swept in code files only (diff mode)
|
|
480
|
+
if (plainprose && a[1] ~ /^[a-z]+$/) plain[N] = 1
|
|
481
|
+
}
|
|
482
|
+
while ((getline l < changed) > 0) chg[l] = 1
|
|
483
|
+
while ((getline l < globs) > 0) {
|
|
484
|
+
v = substr(l, 3)
|
|
485
|
+
if (substr(l, 1, 2) == "R:") roots[++nroot] = v
|
|
486
|
+
else if (substr(l, 1, 2) == "D:") dirs[++ndir] = v
|
|
487
|
+
else gre[++ng] = globre(v)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
NF >= 3 {
|
|
491
|
+
f = $1; ln = $2
|
|
492
|
+
if (f in chg) next
|
|
493
|
+
if (!(f in exc)) exc[f] = isexcl(f)
|
|
494
|
+
if (exc[f]) next
|
|
495
|
+
if (!(f in pro)) pro[f] = (tolower(f) ~ /\.(md|markdown|mdx|rst|txt|adoc|org)$/)
|
|
496
|
+
text = $3
|
|
497
|
+
for (j = 4; j <= NF; j++) text = text "\001" $j
|
|
498
|
+
hit++
|
|
499
|
+
dn = defname(text)
|
|
500
|
+
if (dn != "" && (dn in wid)) { dk = wid[dn]; if ((ikind[dk] == "function" || ikind[dk] == "class") && !(dk in defat)) defat[dk] = f ":" ln }
|
|
501
|
+
nw = split(text, w, /[^A-Za-z0-9_]+/)
|
|
502
|
+
for (j = 1; j <= nw; j++) if (w[j] in wid) { k = wid[w[j]]; if (seen[k] != hit) { seen[k] = hit; take(k, f, ln) } }
|
|
503
|
+
for (j = 1; j <= ns; j++) { k = sid[j]; if (seen[k] != hit && index(text, iid[k]) > 0 && hasword(text, iid[k])) { seen[k] = hit; take(k, f, ln) } }
|
|
504
|
+
}
|
|
505
|
+
function take(k, f, ln) {
|
|
506
|
+
if (plain[k] && pro[f]) return
|
|
507
|
+
cnt[k]++
|
|
508
|
+
if (cnt[k] <= cap) rows[k] = rows[k] iid[k] "\t" ikind[k] "\t" f ":" ln "\n"
|
|
509
|
+
else extra[k]++
|
|
510
|
+
}
|
|
511
|
+
END {
|
|
512
|
+
for (k = 1; k <= N; k++) {
|
|
513
|
+
# a removed function/class whose name is still DEFINED outside the change set is not a unique symbol:
|
|
514
|
+
# its references resolve to the other definition, so they cannot be attributed to the removed one
|
|
515
|
+
if (k in defat) { printf "%s\tambiguous\tstill defined at %s\n", iid[k], defat[k]; continue }
|
|
516
|
+
if (k in rows) printf "%s", rows[k]
|
|
517
|
+
if (extra[k] > 0) printf "%s\ttruncated\t+%d\n", iid[k], extra[k]
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
' > "$TMP/sites" || die2 "sweep grouping failed"
|
|
521
|
+
fi
|
|
522
|
+
fi
|
|
523
|
+
fi
|
|
524
|
+
if [ "$NIDS" -gt "$MAX_IDS" ]; then printf '*\ttruncated\tidentifiers beyond %s not swept\n' "$MAX_IDS" >> "$TMP/sites"; fi
|
|
525
|
+
if [ "$OVER" -eq 1 ]; then printf '*\tBUDGET_EXCEEDED\t%s\n' "$OVERWHY" >> "$TMP/sites"; fi
|
|
526
|
+
|
|
527
|
+
if [ -z "$CONSUMERS" ]; then
|
|
528
|
+
cat "$TMP/sites"
|
|
529
|
+
exit 0
|
|
530
|
+
fi
|
|
531
|
+
|
|
532
|
+
# ---- gate mode -------------------------------------------------------------------------------------------
|
|
533
|
+
# Justified files: lines like "- path[:line] unaffected: reason" (leading bullet and backticks tolerated). The
|
|
534
|
+
# path is everything before the literal " unaffected: " token minus one trailing :<digits>, so any path characters
|
|
535
|
+
# work; a line with an empty reason never justifies.
|
|
536
|
+
sed -E 's/\r$//; s/`//g; s/^[[:space:]]*([-*][[:space:]]+)?//' "$CONSUMERS" \
|
|
537
|
+
| awk '{
|
|
538
|
+
sub(/[ \t]+$/, "")
|
|
539
|
+
i = index($0, " unaffected: ")
|
|
540
|
+
if (i > 1) {
|
|
541
|
+
p = substr($0, 1, i - 1); r = substr($0, i + 13)
|
|
542
|
+
gsub(/^[ \t]+|[ \t]+$/, "", p); gsub(/^[ \t]+|[ \t]+$/, "", r)
|
|
543
|
+
sub(/:[0-9]+$/, "", p)
|
|
544
|
+
if (p != "" && r != "") print p
|
|
545
|
+
}
|
|
546
|
+
}' \
|
|
547
|
+
| sort -u > "$TMP/justified"
|
|
548
|
+
RC=0; NSITES=0; NUNADDR=0
|
|
549
|
+
LABEL="UNADDRESSED"; [ "$MODE" != advisory ] || LABEL="ADVISORY"
|
|
550
|
+
while IFS="$TAB" read -r id kind loc; do
|
|
551
|
+
[ -n "$id" ] || continue
|
|
552
|
+
[ "$kind" != truncated ] || continue
|
|
553
|
+
[ "$kind" != BUDGET_EXCEEDED ] || continue
|
|
554
|
+
[ "$kind" != ambiguous ] || continue
|
|
555
|
+
NSITES=$((NSITES+1))
|
|
556
|
+
f="${loc%:*}"
|
|
557
|
+
grep -Fxq -- "$f" "$TMP/justified" && continue
|
|
558
|
+
echo "$LABEL: $id $loc"; NUNADDR=$((NUNADDR+1)); RC=1
|
|
559
|
+
done < "$TMP/sites"
|
|
560
|
+
if [ "$OVER" -eq 1 ]; then
|
|
561
|
+
echo "BUDGET_EXCEEDED: $OVERWHY (advisory: the sweep is incomplete and never fails the gate)"
|
|
562
|
+
exit 0
|
|
563
|
+
fi
|
|
564
|
+
if [ "$MODE" = advisory ]; then
|
|
565
|
+
echo "OK: consumer sweep advisory ($NSITES site(s), $NUNADDR unaddressed; mode=advisory never fails)"
|
|
566
|
+
exit 0
|
|
567
|
+
fi
|
|
568
|
+
[ "$RC" -ne 0 ] || echo "OK: consumer sweep addressed ($NSITES site(s))"
|
|
569
|
+
exit "$RC"
|