Skip to main content

go-linters

Add and validate custom Go analysis linters in gh-aw.

Source facts

Repository
github/gh-aw
Last source activity
October 3, 2026 at 12:46
Detected SKILL.md language
English
Stars
5,404
Forks
588

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
go-linters
description
Add and validate custom Go analysis linters in gh-aw.
# Go Linters Use this guide when adding a new custom Go analysis linter in this repository. For PR-driven linter generation (derive a rule from a specific pull request pattern), use `.github/skills/pr-to-go-linter/SKILL.md`. ## Where to add a new linter 1. Create a new package under `pkg/linters/<linter-name>/`. 2. Define an analyzer in that package (exported as `Analyzer`). 3. Add tests in the same package using `analysistest` with fixtures under `testdata/src/...`. 4. Register the analyzer in `cmd/linters/main.go` so it runs via the multichecker binary. ## Build and test linters - Test only your linter package: - `go test ./pkg/linters/<linter-name>/...` - Build the custom linter runner: - `go build ./cmd/linters` - Run all custom linters across the repo: - `make golint-custom` `make golint-custom` builds `cmd/linters` and runs it against `./cmd/...` and `./pkg/...`. ## Common AST pitfalls ### Unwrap parenthesized expressions Expressions wrapped in parentheses appear as `*ast.ParenExpr`. Unwrap expressions before asserting their AST type or comparing identifiers, so forms such as `(nil)` and `(err)` are handled like `nil` and `err`. Use the shared helper: ```go if ident, ok := astutil.UnwrapParenExpr(expr).(*ast.Ident); ok && ident.Name == "nil" { // Handle nil. } ``` ### Match all relevant statement shapes Semantically similar calls can appear in different statement nodes. A call that discards a result may be a bare `*ast.ExprStmt` or part of an `*ast.AssignStmt`; filtering only for one shape misses the other. Include and handle each relevant node shape: ```go nodeFilter := []ast.Node{(*ast.AssignStmt)(nil), (*ast.ExprStmt)(nil)} return analyzerutil.Preorder(pass, nodeFilter, func(n ast.Node) { switch stmt := n.(type) { case *ast.AssignStmt: analyzeAssign(stmt) case *ast.ExprStmt: analyzeExpr(stmt) } }) ``` Before submitting a linter, check expression-shape assertions for parenthesis unwrapping and verify its node filter covers relevant equivalent syntax forms. ## Coverage-aware perf gating For linters that flag micro-optimizations (allocation/perf rules), only apply them on lines that tests actually exercise — "hot paths" — rather than on dead or rarely-executed code where the optimization brings no measurable benefit. Use the shared `pkg/linters/internal/coverage` package: 1. In your analyzer file, register a `-hot-threshold` flag in `init()` (not as a var initializer, to avoid an `Analyzer`/`run`/flag initialization cycle): ```go var hotThreshold *int func init() { hotThreshold = coverage.RegisterHotThresholdFlag(Analyzer) } ``` 2. Immediately before reporting a diagnostic, gate it with `coverage.ShouldApply`: ```go if !coverage.ShouldApply(pass, node.Pos(), *hotThreshold) { return } ``` `coverage.ShouldApply` is permissive by default: when no coverage profile is loaded via the `GH_AW_LINT_COVERAGE_PROFILE` environment variable, or when `hot-threshold` is `0`, it always returns `true`, preserving pre-coverage-aware behavior. Only wire this into linters whose fix has a genuine performance rationale (extra allocations, O(n²) behavior, etc.) — purely readability/style linters should not be coverage-gated. ### Generating the coverage profile ```bash go test -covermode=count -coverprofile=/tmp/coverage.out ./... export GH_AW_LINT_COVERAGE_PROFILE=/tmp/coverage.out make golint-custom ``` This profile is read once per linter-runner process. To lint only a specific subtree, scope the `go test` and `golint-custom` commands to the same package path.
View on GitHub