|
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 |
7 | 2 | set -euo pipefail |
8 | 3 | IFS=$'\n\t' |
9 | 4 |
|
10 | | -# Refactoring backlog metrics via golangci-lint. |
| 5 | +# Refactoring backlog metrics for first-party Go, via golangci-lint. |
11 | 6 | # Uses cognitive + cyclomatic complexity, nesting depth, function length, and |
12 | 7 | # maintainability index — see scripts/golangci.refactor-metrics.yml. |
13 | 8 | # |
14 | 9 | # Requires golangci-lint v2+ (https://golangci-lint.run/). |
15 | 10 | # |
| 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 | +# |
16 | 16 | # 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/ |
17 | 20 | # REFACTOR_METRICS_NO_FAIL=1 always exit 0 (useful while triaging a large report) |
18 | 21 | # REFACTOR_METRICS_FORMAT=json machine-readable report on stdout (JSON object with Issues, etc.) |
19 | 22 | # 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) |
20 | 24 | # |
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. |
24 | 27 |
|
25 | 28 | SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) |
26 | 29 | REPO_ROOT=$(cd -- "${SCRIPT_DIR}/.." &>/dev/null && pwd) |
27 | 30 | CONFIG="${SCRIPT_DIR}/golangci.refactor-metrics.yml" |
28 | 31 |
|
| 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 | + |
29 | 77 | cd "${REPO_ROOT}" |
30 | 78 |
|
| 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 | + |
31 | 140 | 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="" |
36 | 150 | 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 |
38 | 158 | 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 |
41 | 162 | 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 |
42 | 202 | fi |
43 | 203 |
|
44 | 204 | 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 |
49 | 213 | exit 127 |
50 | 214 | fi |
51 | 215 |
|
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 |
53 | 227 | if [[ "${REFACTOR_METRICS_NO_FAIL:-}" == "1" ]]; then |
54 | | - extra_args+=(--issues-exit-code=0) |
| 228 | + golangci_cmd+=(--issues-exit-code=0) |
55 | 229 | fi |
56 | 230 |
|
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}" |
61 | 260 | fi |
62 | 261 |
|
63 | | -# Bash 3.2 + set -u: "${extra_args[@]}" errors when the array is empty ("unbound variable"). |
64 | 262 | 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 $? |
70 | 277 | fi |
71 | 278 |
|
72 | 279 | if [[ -n "${REFACTOR_METRICS_JSON:-}" ]]; then |
73 | 280 | 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 |
77 | 290 | exit $? |
78 | 291 | fi |
79 | 292 |
|
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