Skip to content

Commit 3d456ae

Browse files
committed
Vendor custom linters into Wormhole build process on both CI and local
1 parent 2155d40 commit 3d456ae

41 files changed

Lines changed: 1281 additions & 29 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
/wormchain/devnet/ @evan-gray
4040
/wormchain/devnet/txverifier @djb15 @johnsaigle @mdulin2 @pleasew8t
4141
/wormchain/ts-sdk/ @evan-gray @kev1n-peters @panoel
42+
/linters/ @djb15 @johnsaigle @mdulin2 @pleasew8t
4243

4344
# Protobuf for node
4445

.github/workflows/build.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,13 @@ jobs:
302302
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
303303
with:
304304
go-version: "1.25.10"
305+
- name: Cache the custom linter binary
306+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
307+
with:
308+
# lint.sh content-addresses the built binary in here and reuses it on a hit.
309+
path: .wormhole-lint-cache
310+
# SECURITY: key on the full linter source; exact-match only, NO restore-keys.
311+
key: lint-bin-${{ runner.os }}-${{ hashFiles('linters/**') }}
305312
- name: Install formatter
306313
run: go install golang.org/x/tools/cmd/goimports@v0.8.0
307314
- name: Formatting checks

linters/.custom-gcl.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
version: v2.8.0
2+
name: wormhole-golangci-lint
3+
destination: ./bin
4+
plugins:
5+
- module: github.com/certusone/wormhole/linters/rules/channelcheck
6+
path: ./rules/channelcheck

linters/.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Built binaries.
2+
/bin/
3+
4+
# Stray root-level builds, e.g. from a bare `go build ./...` (the Makefile
5+
# targets put these under bin/).
6+
/wormhole-lint
7+
/wormhole-golangci-lint
8+
9+
# Cloned + patched upstream sources (revive, golangci-lint) used to build
10+
# bin/wormhole-golangci-lint. Recreated by `make build-golangci-lint`.
11+
/build/

linters/Makefile

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
GOLANGCI_LINT_VERSION ?= v2.8.0
2+
3+
BIN_DIR := bin
4+
5+
.PHONY: build build-golangci-lint install-golangci-lint test vet clean
6+
7+
# Standalone multichecker over every rules/<linter>.Analyzer.
8+
build:
9+
go build -o $(BIN_DIR)/wormhole-lint ./cmd/wormhole-lint
10+
11+
# Pinned upstream golangci-lint, dropped into $(BIN_DIR). Used as the driver
12+
# for `golangci-lint custom`; keep its version aligned with the `version:`
13+
# field in .custom-gcl.yml.
14+
install-golangci-lint:
15+
GOOS= GOARCH= GOBIN=$(abspath $(BIN_DIR)) go install \
16+
github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
17+
18+
# Custom golangci-lint with our module plugins baked in. Driven by
19+
# .custom-gcl.yml at the repo root; output goes to ./bin per `destination:`.
20+
build-golangci-lint: install-golangci-lint
21+
$(BIN_DIR)/golangci-lint custom
22+
23+
test:
24+
go test -v ./...
25+
@for mod in $$(find rules -name go.mod); do \
26+
dir=$$(dirname $$mod); \
27+
echo "==> testing $$dir"; \
28+
(cd $$dir && go test -v ./...) || exit $$?; \
29+
done
30+
31+
vet:
32+
go vet ./...
33+
34+
clean:
35+
rm -rf $(BIN_DIR)

linters/README.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# AR Golang CI Lints
2+
3+
Custom Go linters used on Wormhole CI. Each linter is a [golangci-lint
4+
module plugin](https://golangci-lint.run/plugins/module-plugins/) and lives
5+
as its own Go module under `rules/<linter>/`.
6+
7+
Prefer to use the `release` builds.
8+
9+
Currently supported linters:
10+
11+
| Name | Purpose | Features |
12+
| -------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
13+
| `channelcheck` | Flag channel usage patterns that can deadlock or block. | • Blocking channel sends outside a `select`<br>• Unbuffered channel creation (`make(chan T)`)<br>• Ignore specific channels |
14+
15+
16+
## Build & test
17+
```
18+
make build # bin/wormhole-lint
19+
make build-golangci-lint # bin/wormhole-golangci-lint
20+
make test # root + each rules/<linter> module
21+
```
22+
23+
`make build-golangci-lint` first installs the pinned upstream
24+
`golangci-lint` into `bin/` (per `GOLANGCI_LINT_VERSION`), then runs
25+
`golangci-lint custom`.
26+
27+
## Use
28+
29+
Standalone:
30+
31+
```
32+
bin/wormhole-lint ./...
33+
```
34+
35+
Via the custom golangci-lint:
36+
37+
```
38+
bin/wormhole-golangci-lint run --timeout=10m ./...
39+
```
40+
41+
## Enable a plugin in `.golangci.yml`
42+
43+
Module plugins are addressed under `linters.settings.custom.<name>` and
44+
enabled in `linters.enable` by the plugin's registered name. Example for
45+
`channelcheck`:
46+
47+
```yaml
48+
version: "2"
49+
linters:
50+
enable:
51+
- channelcheck
52+
settings:
53+
custom:
54+
channelcheck:
55+
type: module
56+
description: reports channel blocking issues
57+
settings:
58+
blocking: true
59+
unbuffered: false
60+
bufferMax: 0
61+
ignoreChannelsByName: [errC]
62+
```
63+
64+
## Development
65+
66+
### Adding a new linter
67+
68+
1. Scaffold the module:
69+
```
70+
mkdir -p rules/<linter> && cd rules/<linter>
71+
go mod init github.com/certusone/wormhole/linters/rules/<linter>
72+
```
73+
2. Implement `<linter>.go` following the channelcheck reference
74+
(`rules/channelcheck/channelcheck.go`):
75+
- Export an `Analyzer` of type `*analysis.Analyzer`.
76+
- Define a `Settings` struct and a `New(any) (register.LinterPlugin, error)`
77+
constructor that decodes settings via
78+
`register.DecodeSettings[Settings]`.
79+
- Implement `BuildAnalyzers()` and `GetLoadMode()` on your plugin type.
80+
- In `init()`, call `register.Plugin("<name>", New)` so
81+
`golangci-lint custom` picks it up.
82+
3. Add tests + fixtures under `rules/<linter>/testdata/` following
83+
`rules/channelcheck/channelcheck_test.go`.
84+
4. Wire it into the root module so the aggregator can import it:
85+
- In root `go.mod`, add
86+
`require github.com/certusone/wormhole/linters/rules/<linter> v0.0.0` and
87+
`replace github.com/certusone/wormhole/linters/rules/<linter> => ./rules/<linter>`.
88+
- In `cmd/wormhole-lint/main.go`, add the import and append
89+
`<linter>.Analyzer` to the `multichecker.Main` call.
90+
5. Wire it into `.custom-gcl.yml` so the custom golangci-lint picks it up:
91+
```yaml
92+
plugins:
93+
- module: github.com/certusone/wormhole/linters/rules/<linter>
94+
path: ./rules/<linter>
95+
```
96+
6. `make test && make build && make build-golangci-lint` to verify.
97+
98+
### Layout
99+
100+
```
101+
.custom-gcl.yml # plugin manifest for `golangci-lint custom`
102+
Makefile
103+
go.mod # root module: cmd/* + replaces for each rules/<linter>
104+
cmd/
105+
wormhole-lint/ # multichecker aggregator binary
106+
rules/
107+
channelcheck/ # standalone Go module per linter
108+
channelcheck.go
109+
channelcheck_test.go
110+
go.mod
111+
testdata/
112+
```

linters/cmd/wormhole-lint/main.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Command wormhole-lint runs every custom linter under rules/ as a single
2+
// standalone binary. Each rules/<linter>/ exports an Analyzer; add new
3+
// linters by appending one import + one Analyzer below.
4+
package main
5+
6+
import (
7+
"golang.org/x/tools/go/analysis/multichecker"
8+
9+
"github.com/certusone/wormhole/linters/rules/channelcheck"
10+
)
11+
12+
func main() {
13+
multichecker.Main(
14+
channelcheck.Analyzer,
15+
)
16+
}

linters/go.mod

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module github.com/certusone/wormhole/linters
2+
3+
go 1.25.0
4+
5+
require (
6+
github.com/certusone/wormhole/linters/rules/channelcheck v0.0.0
7+
golang.org/x/tools v0.30.0
8+
)
9+
10+
require (
11+
github.com/golangci/plugin-module-register v0.1.1 // indirect
12+
golang.org/x/mod v0.23.0 // indirect
13+
golang.org/x/sync v0.11.0 // indirect
14+
)
15+
16+
replace github.com/certusone/wormhole/linters/rules/channelcheck => ./rules/channelcheck

linters/go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c=
2+
github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc=
3+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
4+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
5+
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
6+
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
7+
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
8+
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
9+
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
10+
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
## Channel Check
2+
3+
Channels are a great feature of Golang but have several footguns that can lead to deadlocks. In particular, if the receiving channel stops processing the messages, a *non-blocking* channel send would fail to continue. In certain mission-critical sections of code, this could lead to a complete deadlock.
4+
5+
This linter currently has three features:
6+
- Identify blocking sends
7+
- Identify non-buffered channel creation
8+
- Identify buffered channel size exceeds maximum size checks
9+
10+
Many of these will lead to false positives or situations where we *want* a blocking channel send. In these cases, `nolint:channelcheck` is easy to add (assuming this is integrated directly with golangci-lint). Regardless, having this issue pointed out automatically is a good way to fix bugs.
11+
12+
## Configuration
13+
14+
Each option can be set two ways: as a golangci-lint module setting (under
15+
`settings.custom.channelcheck.settings:`, using the **Setting** name) or as a
16+
standalone analyzer flag (when running the `wormhole-lint` binary, using the
17+
**Flag** name).
18+
19+
| Setting | Flag | Type | Default | Description |
20+
| ------------------------- | ------------ | ---------- | ------- | ----------------------------------------------------------------------------------------------- |
21+
| `CheckBlockingSends` | `blocking` | bool | `true` | Flag blocking sends that lack a `default`/timeout/ticker escape in their enclosing `select`. |
22+
| `CheckUnbufferedChannels` | `unbuffered` | bool | `false` | Flag creation of unbuffered channels (`make(chan T)`). |
23+
| `CheckBufferAmount` | `bufferMax` | uint64 | `0` | Flag buffered channels whose size exceeds this max. `0` disables the check. |
24+
| `IgnoreChannelsByName` | *(none)* | []string | `[]` | Channel/field names whose direct sends are exempt from the blocking-send check (e.g. `errC`). Settings-only; no standalone flag. |
25+
26+
## Configuration Steps Standalone
27+
The linter can be used by itself. Simply run the following to install the binary:
28+
29+
```bash
30+
go install ./cmd/channelcheck/main.go
31+
```
32+
33+
Usage:
34+
35+
```bash
36+
channellint ./examples
37+
```
38+
39+
## Resources
40+
- https://clavinjune.dev/en/blogs/buffered-vs-unbuffered-channel-in-golang/
41+
- https://medium.com/@chethan13/unbuffered-vs-buffered-channels-in-go-83b1a0956e46
42+
- https://chrisguitarguy.com/2024/04/17/beware-blocking-channel-sends-in-go/
43+
- https://abubakardev0.medium.com/understanding-channels-in-go-a-comprehensive-guide-a5a9f823c709

0 commit comments

Comments
 (0)