diff --git a/.golangci.yaml b/.golangci.yaml index 840d68f..7abc306 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -2,31 +2,30 @@ version: "2" issues: - whole-files: true - - max-same-issues: 0 # 0 = unlimited; default 3 max-issues-per-linter: 0 # 0 = unlimited; default 50 - + max-same-issues: 0 # 0 = unlimited; default 3 uniq-by-line: false # default true hides other linters' findings on the same line + whole-files: true # needs one of the --new* modes formatters: enable: - # goimports rides along with gofumpt because it alone drops unused imports. - - gofumpt - - goimports - - golines + - gci # regroups imports + - gofumpt # stricter gofmt + - goimports # adds and removes imports + - golines # wraps long lines settings: gofumpt: extra: - group-params: true - clothe-returns: true - balance-calls: true + group-params: true # func(a, b string), not func(a string, b string) + clothe-returns: true # naked `return` -> `return err` + balance-calls: true # a call broken after `(` also breaks before `)` + golines: - max-len: 120 + max-len: 120 # default 100 linters: - default: none + default: none # upgrades never add a linter silently enable: # Standard - errcheck @@ -36,9 +35,9 @@ linters: - unused # Additional + - bidichk - bodyclose - canonicalheader - - depguard - durationcheck - errchkjson - errname @@ -49,10 +48,12 @@ linters: - gocognit - goconst - gocritic + - gomoddirectives # forbids every replace; one reaching main breaks the tagged release build - gomodguard_v2 - goprintffuncname - gosec - loggercheck + - makezero - mirror - misspell - modernize @@ -68,6 +69,7 @@ linters: - perfsprint - prealloc - promlinter + - reassign - recvcheck - revive - rowserrcheck @@ -85,32 +87,10 @@ linters: - wsl_v5 settings: - govet: - enable: - - deepequalerrors - - nilness - - reflectvaluecompare - - sortslice - - unusedwrite - - nolintlint: - require-specific: true - require-explanation: true - - depguard: - rules: - main: - deny: - - pkg: github.com/pkg/errors - desc: Should be replaced by standard lib errors package + errcheck: + check-type-assertions: true gomodguard_v2: - # A local `replace` that reaches main breaks the tagged release build. - # Only sees directly imported modules; an indirect dep's replace slips through. - local-replace-directives: true - - # Bar for an entry: archived or known-vulnerable, AND still reached for - # directly today because old tutorials point there. blocked: - module: github.com/Azure/azure-sdk-for-go reason: Track 1 (services/, storage/, profiles/) retired 2023-09-30; use the sdk/ modules (track 2) @@ -126,13 +106,64 @@ linters: recommendations: - go.uber.org/mock reason: Archived upstream; go.uber.org/mock is the maintained fork + - module: github.com/mitchellh/mapstructure + recommendations: + - github.com/go-viper/mapstructure/v2 + reason: Archived upstream; go-viper is the maintained fork, already pulled in by viper + - module: github.com/pkg/errors + reason: No substantive commit since 2020; stdlib error wrapping (fmt.Errorf, errors.Is/As) covers it - module: github.com/satori/go.uuid recommendations: - github.com/google/uuid - reason: Unmaintained; on Go 1.27+ prefer the stdlib uuid package + reason: No commit since 2018; migrate to google/uuid (stdlib uuid on Go 1.27+) + - module: gopkg.in/yaml + match-type: prefix # .v1/.v2/.v3 in one entry + recommendations: + - go.yaml.in/yaml/v2 + - go.yaml.in/yaml/v3 + reason: go-yaml/yaml archived 2025-04-01; go.yaml.in is the YAML org's maintained fork -- keep the major version, v2 and v3 are not API-compatible + + gosec: + # Bare call statements only (never defer/go) plus a whitelist, so a strict subset + # of errcheck, which also names the function -- and uniq-by-line: false prints both. + # Breaks if errcheck is narrowed with exclude-functions or std-error-handling. + excludes: + - G104 + + govet: + # Additive, not a replacement. enable-all would drag in fieldalignment and shadow. + enable: + - deepequalerrors + - nilness + - reflectvaluecompare + - sortslice + - unusedwrite + + nolintlint: + require-explanation: true + require-specific: true + + perfsprint: + # On `s += x` this and modernize both fire and golangci-lint takes perfsprint's + # fix, which invents `sSb7` instead of reusing the variable. Off, `s = s + x` + # falls to gocritic's assignOp -- no autofix, so the Builder lands a round later. + concat-loop: false + + prealloc: + # Default is range loops only. modernize converts the plain 3-clause form to range, + # but never `i += 2`, `i--`, or a non-zero start -- this is their only check. + for-loops: true + + reassign: + # Upstream's recommendation; the default covers only EOF and Err*, leaving os.Args + # and http.DefaultClient open. Whole-variable writes only -- `.Timeout = x` slips + # through -- and package-var config APIs (cobra.EnableCommandSorting) need a nolint. + patterns: + - ".*" exclusions: rules: - path: _test\.go linters: - - goconst + - goconst # repeated literals are normal in table-driven tests + - reassign # tests borrow os.Stdin/os.Args and restore them afterwards diff --git a/AGENTS.md b/AGENTS.md index f44cef5..19c5b6e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ go install github.com/min0625/mint/cmd/mint@latest # Go 1.26.4+; binary → $G make build # compile → bin/mint (CGO_ENABLED=0, trimpath, ldflags injected) make test # go test -race -failfast ./... (use this to watch output; prek buffers it) make lint # golangci-lint (only new violations since HEAD) -make fix # golangci-lint --fix + go mod tidy +make fix # go mod tidy, golangci-lint --fix, tidy again, then re-runs lint make check # every prek hook over all tracked files (CI gate); may rewrite # files -- the formatting fixer hooks run here too make check-tidy # verify go.mod/go.sum are tidy @@ -104,14 +104,19 @@ bin/mint # compiled binary (gitignored) - **No unnecessary dependencies** — keep `go.mod` minimal. - Lint is checked only for *new* violations (`--new-from-rev=$(NEW_FROM_REV)`, default `HEAD`; CI passes the PR base branch). `make lint` and `make fix` hard-fail if that revision does not resolve (shared - `check-rev` guard) — `golangci-lint` itself only warns and exits 0, which would let CI pass having - linted nothing. The pre-commit hook already runs + `check-rev` guard). `golangci-lint` itself fails closed there — it warns, reports every issue in the + repo, and exits 1 — so the guard buys a readable error, not safety. The pre-commit hook already runs lint on every commit, so running it by hand is only needed to re-check a different rev. - Lint config changes do not require fixing the whole codebase. `whole-files: true` + `--new-from-rev` is a ratchet: a PR fixes the lint of the files it touches, and untouched files carry their debt until someone edits them. After changing `.golangci.yaml`, run `golangci-lint run ./...` once (no `--new-from-rev`) to see how much debt the change adds - repo-wide — read the number, don't fix it. + repo-wide — read the number, don't fix it. Formatter findings are the exception: apply + `golangci-lint fmt ./...` repo-wide, since a half-formatted tree contradicts its own config. +- `.golangci.yaml` is not self-contained. `whole-files: true` needs one of the `--new*` modes and + is silently inert without one, so copying the file into another repo without the `--new-from-rev` + harness means the whole backlog lands at once — measured at 40–184 issues/KLOC on third-party + Go. Port the Makefile targets with it, or expect to start from a clean tree. - **Release workflow** — push a tag matching `v*.*.*` to automatically trigger GoReleaser CI; creates GitHub Release with multi-platform binaries. - **Local snapshot testing** — run `goreleaser release --snapshot --clean` to validate build configuration before publishing. diff --git a/Makefile b/Makefile index b9fb6f6..2acc2c9 100644 --- a/Makefile +++ b/Makefile @@ -10,18 +10,23 @@ build: mkdir -p ./bin/ CGO_ENABLED=0 go build -trimpath -ldflags="$(LDFLAGS)" -o ./bin/ ./cmd/mint -# golangci-lint only *warns* and exits 0 when --new-from-rev does not resolve, -# so a typo'd or unfetched rev would silently lint nothing and report success. -# Both --new-from-rev callers below depend on this guard. +# An unresolvable rev is not silent: golangci-lint warns, reports every issue, and +# exits 1. This guard is for the message, not the exit code. .PHONY: check-rev check-rev: @git rev-parse --verify --quiet '$(NEW_FROM_REV)^{commit}' >/dev/null || \ { echo "NEW_FROM_REV=$(NEW_FROM_REV) is not a valid revision" >&2; exit 1; } +# Tidy first: a stale go.mod fails typecheck, which is reported past --new-from-rev +# while every other linter falls silent -- so --fix would quietly repair nothing. +# Tidy again after: exptostd rewrites x/exp imports to stdlib and strips a require. +# The closing lint reports whatever --fix could not repair. .PHONY: fix fix: check-rev go mod tidy golangci-lint run --new-from-rev=$(NEW_FROM_REV) --fix ./... + go mod tidy + @$(MAKE) --no-print-directory lint .PHONY: lint lint: check-rev @@ -45,10 +50,10 @@ cover-html: cover check-tidy: go mod tidy -diff -# The full gate: every hook in .pre-commit-config.yaml -- check-tidy, lint and -# test included -- over every tracked file, i.e. the same suite `git commit` -# runs, but repo-wide instead of staged. NEW_FROM_REV is set on the command -# line, so make exports it and prek passes it through to the nested make. +# The full gate: every hook in .pre-commit-config.yaml -- check-tidy, lint and test +# included -- over every tracked file, i.e. what `git commit` runs but repo-wide. +# NEW_FROM_REV on the command line reaches the nested `make lint` via MAKEFLAGS; +# without it lint falls back to HEAD and reports nothing on CI's clean checkout. .PHONY: check check: prek run --all-files --show-diff-on-failure