Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 71 additions & 40 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,9 +35,9 @@ linters:
- unused

# Additional
- bidichk
- bodyclose
- canonicalheader
- depguard
- durationcheck
- errchkjson
- errname
Expand All @@ -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
Expand All @@ -68,6 +69,7 @@ linters:
- perfsprint
- prealloc
- promlinter
- reassign
- recvcheck
- revive
- rowserrcheck
Expand All @@ -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)
Expand All @@ -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
13 changes: 9 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down
19 changes: 12 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading