Skip to content

Commit 6415a51

Browse files
committed
chore: go static analysis helper
1 parent 346058a commit 6415a51

1 file changed

Lines changed: 255 additions & 37 deletions

File tree

Lines changed: 255 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,298 @@
1-
#!/usr/bin/env bash
2-
# Requires bash (arrays, pipefail). Do not run with dash/sh.
3-
if [ -z "${BASH_VERSION:-}" ]; then
4-
echo "This script must be run with bash (e.g. bash \"$0\" …)." >&2
5-
exit 1
6-
fi
1+
#!/bin/bash
72
set -euo pipefail
83
IFS=$'\n\t'
94

10-
# Refactoring backlog metrics via golangci-lint.
5+
# Refactoring backlog metrics for first-party Go, via golangci-lint.
116
# Uses cognitive + cyclomatic complexity, nesting depth, function length, and
127
# maintainability index — see scripts/golangci.refactor-metrics.yml.
138
#
149
# Requires golangci-lint v2+ (https://golangci-lint.run/).
1510
#
11+
# Focus one or more linters (same settings as in the YAML), e.g. duplicate strings:
12+
# ./scripts/analyze.go.refactor-metrics.sh --only goconst
13+
# REFACTOR_METRICS_ONLY=goconst ./scripts/analyze.go.refactor-metrics.sh
14+
# ./scripts/analyze.go.refactor-metrics.sh --only goconst,misspell
15+
#
1616
# Environment:
17+
# REFACTOR_METRICS_ONLY comma-separated linters; CLI --only overrides this
18+
# run --list-linters to print enabled linters, or see scripts/golangci.refactor-metrics.yml
19+
# full list: https://golangci-lint.run/usage/linters/
1720
# REFACTOR_METRICS_NO_FAIL=1 always exit 0 (useful while triaging a large report)
1821
# REFACTOR_METRICS_FORMAT=json machine-readable report on stdout (JSON object with Issues, etc.)
1922
# REFACTOR_METRICS_JSON=file write JSON issues to file (still prints text to stdout unless FORMAT=json)
23+
# REFACTOR_METRICS_BOOTSTRAP=1 try to auto-install golangci-lint if missing (default: 1)
2024
#
21-
# Positional args: optional package paths (same as `go test`). Default is ./... (this module only;
22-
# nested go.mod trees under e.g. test/ are not included). If you pass paths, only those are analyzed
23-
# — they replace the default, they are not combined with ./...
25+
# Pass a directory that contains go.mod (e.g. test/jquery_example) to lint that
26+
# module from its root; otherwise the repo root module and ./... are used.
2427

2528
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
2629
REPO_ROOT=$(cd -- "${SCRIPT_DIR}/.." &>/dev/null && pwd)
2730
CONFIG="${SCRIPT_DIR}/golangci.refactor-metrics.yml"
2831

32+
usage() {
33+
cat <<'EOF'
34+
Refactoring / maintainability lint for first-party Go via golangci-lint
35+
(see scripts/golangci.refactor-metrics.yml).
36+
37+
Usage:
38+
./scripts/analyze.go.refactor-metrics.sh [options] [golangci-lint run flags...]
39+
40+
Options:
41+
--only, -o NAMES run only these linters (comma-separated); e.g. goconst
42+
--list-linters print linters enabled in golangci.refactor-metrics.yml and exit
43+
-h, --help show this help
44+
45+
Environment:
46+
REFACTOR_METRICS_ONLY same as --only if no --only on the command line
47+
REFACTOR_METRICS_NO_FAIL=1 always exit 0 while triaging
48+
REFACTOR_METRICS_FORMAT=json JSON report on stdout
49+
REFACTOR_METRICS_JSON=path write JSON issues to path
50+
REFACTOR_METRICS_BOOTSTRAP=1 try to auto-install golangci-lint if missing (default: 1)
51+
52+
Examples:
53+
./scripts/analyze.go.refactor-metrics.sh --only goconst
54+
./scripts/analyze.go.refactor-metrics.sh -o errcheck,errorlint
55+
./scripts/analyze.go.refactor-metrics.sh test/jquery_example
56+
EOF
57+
}
58+
59+
list_linters_from_config() {
60+
awk '
61+
/^ enable:/ { in_enable = 1; next }
62+
in_enable && /^ - / { sub(/^ - /, ""); print }
63+
in_enable && /^ (settings|exclusions):/ { exit }
64+
' "${CONFIG}"
65+
}
66+
67+
if [[ "${1:-}" == "--list-linters" ]]; then
68+
list_linters_from_config
69+
exit 0
70+
fi
71+
72+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
73+
usage
74+
exit 0
75+
fi
76+
2977
cd "${REPO_ROOT}"
3078

79+
only_linters="${REFACTOR_METRICS_ONLY:-}"
80+
passthrough=()
81+
while [[ $# -gt 0 ]]; do
82+
case "$1" in
83+
--only=*)
84+
only_linters="${1#*=}"
85+
shift
86+
;;
87+
--only | -o)
88+
if [[ -z "${2:-}" ]]; then
89+
echo "${0##*/}: --only requires a linter name (comma-separated for several)" >&2
90+
exit 2
91+
fi
92+
only_linters="$2"
93+
shift 2
94+
;;
95+
*)
96+
passthrough+=("$1")
97+
shift
98+
;;
99+
esac
100+
done
101+
102+
# Passthrough: golangci-lint flags plus optional package/module paths. Directories that
103+
# contain go.mod are separate modules; lint must run from that root so typecheck uses the
104+
# correct module, not the repo root.
105+
flags_only=()
106+
module_roots=()
107+
go_packages=()
108+
# Bash 3.2 + set -u: "${passthrough[@]}" is unbound when the array is empty.
109+
if [[ ${#passthrough[@]} -gt 0 ]]; then
110+
for a in "${passthrough[@]}"; do
111+
if [[ "${a}" == -* ]]; then
112+
flags_only+=("${a}")
113+
continue
114+
fi
115+
resolved="${a}"
116+
if [[ "${resolved}" != /* ]]; then
117+
resolved="${REPO_ROOT}/${resolved}"
118+
fi
119+
resolved="${resolved%/}"
120+
if [[ -f "${resolved}/go.mod" ]]; then
121+
module_roots+=("$(cd "${resolved}" && pwd)")
122+
else
123+
go_packages+=("${a}")
124+
fi
125+
done
126+
fi
127+
128+
if [[ ${#module_roots[@]} -gt 0 && ${#go_packages[@]} -gt 0 ]]; then
129+
echo "${0##*/}: cannot mix separate-module directories (${module_roots[*]}) with repo-relative package paths (${go_packages[*]}); run those as two invocations." >&2
130+
exit 2
131+
fi
132+
133+
if [[ ${#module_roots[@]} -gt 1 ]]; then
134+
if [[ "${REFACTOR_METRICS_FORMAT:-}" == "json" || -n "${REFACTOR_METRICS_JSON:-}" ]]; then
135+
echo "${0##*/}: JSON output is only supported for a single module directory at a time." >&2
136+
exit 2
137+
fi
138+
fi
139+
31140
golangci_bin=""
32-
if command -v golangci-lint >/dev/null 2>&1; then
33-
golangci_bin=$(command -v golangci-lint)
34-
else
35-
_gopath=""
141+
refresh_golangci_bin() {
142+
golangci_bin=""
143+
if command -v golangci-lint >/dev/null 2>&1; then
144+
golangci_bin=$(command -v golangci-lint)
145+
return 0
146+
fi
147+
148+
local gobin=""
149+
local gopath=""
36150
if command -v go >/dev/null 2>&1; then
37-
_gopath="$(go env GOPATH 2>/dev/null || true)"
151+
gobin="$(go env GOBIN 2>/dev/null || true)"
152+
gopath="$(go env GOPATH 2>/dev/null || true)"
153+
fi
154+
155+
if [[ -n "${gobin}" && -x "${gobin}/golangci-lint" ]]; then
156+
golangci_bin="${gobin}/golangci-lint"
157+
return 0
38158
fi
39-
if [[ -n "${_gopath}" && -x "${_gopath}/bin/golangci-lint" ]]; then
40-
golangci_bin="${_gopath}/bin/golangci-lint"
159+
if [[ -n "${gopath}" && -x "${gopath}/bin/golangci-lint" ]]; then
160+
golangci_bin="${gopath}/bin/golangci-lint"
161+
return 0
41162
fi
163+
return 1
164+
}
165+
166+
try_install_golangci() {
167+
[[ "${REFACTOR_METRICS_BOOTSTRAP:-1}" == "1" ]] || return 1
168+
169+
echo "golangci-lint not found. Trying to install automatically..." >&2
170+
171+
if [[ "$(uname -s)" == "Darwin" ]] && command -v brew >/dev/null 2>&1; then
172+
echo "Attempting install via Homebrew..." >&2
173+
if brew install golangci-lint >/dev/null 2>&1; then
174+
refresh_golangci_bin && return 0
175+
fi
176+
echo "Homebrew install did not succeed; trying Go install fallback..." >&2
177+
fi
178+
179+
if command -v go >/dev/null 2>&1; then
180+
echo "Attempting install via go install..." >&2
181+
if go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest >/dev/null 2>&1; then
182+
local gobin
183+
local gopath
184+
gobin="$(go env GOBIN 2>/dev/null || true)"
185+
gopath="$(go env GOPATH 2>/dev/null || true)"
186+
if [[ -n "${gobin}" ]]; then
187+
export PATH="${gobin}:${PATH}"
188+
elif [[ -n "${gopath}" ]]; then
189+
export PATH="${gopath}/bin:${PATH}"
190+
fi
191+
refresh_golangci_bin && return 0
192+
fi
193+
fi
194+
195+
return 1
196+
}
197+
198+
refresh_golangci_bin || true
199+
if [[ -z "${golangci_bin}" ]]; then
200+
try_install_golangci || true
201+
refresh_golangci_bin || true
42202
fi
43203

44204
if [[ -z "${golangci_bin}" ]]; then
45-
echo "golangci-lint not found. Install v2+ e.g.:" >&2
46-
echo " go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest" >&2
47-
echo " brew install golangci-lint # macOS" >&2
48-
echo " apt install golangci-lint # Debian/Ubuntu (if packaged)" >&2
205+
echo "golangci-lint not found after auto-install attempt." >&2
206+
if [[ "$(uname -s)" == "Darwin" ]] && command -v brew >/dev/null 2>&1; then
207+
echo "Try: brew install golangci-lint" >&2
208+
fi
209+
if command -v go >/dev/null 2>&1; then
210+
echo "Try: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest" >&2
211+
fi
212+
echo "Set REFACTOR_METRICS_BOOTSTRAP=0 to disable auto-install attempts." >&2
49213
exit 127
50214
fi
51215

52-
extra_args=()
216+
# Keep a non-empty argv array before "${arr[@]}" so set -u is safe on Bash 3.2
217+
# (empty array expansion is "unbound" there).
218+
# Treat lint findings as non-blocking unless BUILD_STOP_ON_LINT_FINDINGS=1.
219+
if [[ "${BUILD_STOP_ON_LINT_FINDINGS:-0}" != "1" ]]; then
220+
REFACTOR_METRICS_NO_FAIL=1
221+
fi
222+
223+
golangci_cmd=(run -c "${CONFIG}")
224+
if [[ -n "${only_linters}" ]]; then
225+
golangci_cmd+=(--enable-only "${only_linters}")
226+
fi
53227
if [[ "${REFACTOR_METRICS_NO_FAIL:-}" == "1" ]]; then
54-
extra_args+=(--issues-exit-code=0)
228+
golangci_cmd+=(--issues-exit-code=0)
55229
fi
56230

57-
if [[ $# -gt 0 ]]; then
58-
scope_args=("$@")
59-
else
60-
scope_args=(./...)
231+
# Bash 3.2 + set -u: "${empty[@]}" is unbound — merge into one array before expanding.
232+
full_golangci_cmd=("${golangci_cmd[@]}")
233+
[[ ${#flags_only[@]} -gt 0 ]] && full_golangci_cmd+=("${flags_only[@]}")
234+
235+
run_golangci() {
236+
local workdir="$1"
237+
shift
238+
(cd "${workdir}" && exec "${golangci_bin}" "$@")
239+
}
240+
241+
if [[ ${#module_roots[@]} -gt 0 ]]; then
242+
status=0
243+
for mod_root in "${module_roots[@]}"; do
244+
if [[ "${REFACTOR_METRICS_FORMAT:-}" == "json" ]]; then
245+
run_golangci "${mod_root}" "${full_golangci_cmd[@]}" \
246+
--output.json.path=stdout \
247+
--output.text.path=stderr \
248+
--show-stats=false \
249+
./... || status=$?
250+
elif [[ -n "${REFACTOR_METRICS_JSON:-}" ]]; then
251+
mkdir -p "$(dirname "${REFACTOR_METRICS_JSON}")"
252+
run_golangci "${mod_root}" "${full_golangci_cmd[@]}" \
253+
--output.json.path="${REFACTOR_METRICS_JSON}" \
254+
./... || status=$?
255+
else
256+
run_golangci "${mod_root}" "${full_golangci_cmd[@]}" ./... || status=$?
257+
fi
258+
done
259+
exit "${status}"
61260
fi
62261

63-
# Bash 3.2 + set -u: "${extra_args[@]}" errors when the array is empty ("unbound variable").
64262
if [[ "${REFACTOR_METRICS_FORMAT:-}" == "json" ]]; then
65-
exec "${golangci_bin}" run -c "${CONFIG}" "${extra_args[@]+"${extra_args[@]}"}" \
66-
--output.json.path=stdout \
67-
--output.text.path=stderr \
68-
--show-stats=false \
69-
"${scope_args[@]}"
263+
if [[ ${#go_packages[@]} -gt 0 ]]; then
264+
run_golangci "${REPO_ROOT}" "${full_golangci_cmd[@]}" \
265+
--output.json.path=stdout \
266+
--output.text.path=stderr \
267+
--show-stats=false \
268+
./... "${go_packages[@]}"
269+
else
270+
run_golangci "${REPO_ROOT}" "${full_golangci_cmd[@]}" \
271+
--output.json.path=stdout \
272+
--output.text.path=stderr \
273+
--show-stats=false \
274+
./...
275+
fi
276+
exit $?
70277
fi
71278

72279
if [[ -n "${REFACTOR_METRICS_JSON:-}" ]]; then
73280
mkdir -p "$(dirname "${REFACTOR_METRICS_JSON}")"
74-
"${golangci_bin}" run -c "${CONFIG}" "${extra_args[@]+"${extra_args[@]}"}" \
75-
--output.json.path="${REFACTOR_METRICS_JSON}" \
76-
"${scope_args[@]}"
281+
if [[ ${#go_packages[@]} -gt 0 ]]; then
282+
run_golangci "${REPO_ROOT}" "${full_golangci_cmd[@]}" \
283+
--output.json.path="${REFACTOR_METRICS_JSON}" \
284+
./... "${go_packages[@]}"
285+
else
286+
run_golangci "${REPO_ROOT}" "${full_golangci_cmd[@]}" \
287+
--output.json.path="${REFACTOR_METRICS_JSON}" \
288+
./...
289+
fi
77290
exit $?
78291
fi
79292

80-
exec "${golangci_bin}" run -c "${CONFIG}" "${extra_args[@]+"${extra_args[@]}"}" "${scope_args[@]}"
293+
if [[ ${#go_packages[@]} -gt 0 ]]; then
294+
run_golangci "${REPO_ROOT}" "${full_golangci_cmd[@]}" ./... "${go_packages[@]}"
295+
else
296+
run_golangci "${REPO_ROOT}" "${full_golangci_cmd[@]}" ./...
297+
fi
298+
exit $?

0 commit comments

Comments
 (0)