Files
coder/scripts/intxcheck/testdata/src/example/example.go
T
Ethan 55e525fc28 ci: add InTx linter replacing ruleguard rule (#24422)
Replace the old `InTx` ruleguard rule in `scripts/rules.go` with a
custom in-tree `go/analysis` analyzer under `scripts/intxcheck/`. The
new analyzer catches the same direct and pass-through misuse classes as
before, plus two new classes the pattern-matcher couldn't reach:

- **Indirect same-package helper misuse** — flags `p.someHelper(ctx)`
inside `InTx` when the helper body uses the outer store (the PR #24369
bug class).
- **Nested dangerous closures** — descends into `go func() { ... }()`,
`defer func() { ... }()`, and immediately-invoked function literals.

The analyzer uses semantic `types.Object` identity instead of raw
expression string comparison, which avoids false positives from
closure-local shadowing and catches simple aliases like `outer := s.db`
and `alias := s`.

This PR also fixes three real outer-store-inside-transaction bugs the
new analyzer surfaced:

- `coderd/wsbuilder/wsbuilder.go`: `FindMatchingPresetID` and
`getWorkspaceTask` now use the inner transaction store instead of
`b.store`.
- `enterprise/dbcrypt/dbcrypt.go`: `ensureEncrypted` now calls
`s.InsertDBCryptKey` (the tx-wrapped store) instead of
`db.InsertDBCryptKey`. The `dbCrypt.InTx` method wraps the raw tx in a
new `*dbCrypt`, so `s.InsertDBCryptKey` still dispatches through the
encryption layer.

Two call sites need `// intxcheck:ignore` suppressions. Both are one-off
patterns that only look like misuse because the analyzer doesn't track
assignments — proving them safe would require full dataflow analysis,
which is well beyond what a targeted lint like this should attempt:

- `coderd/database/dbfake/dbfake.go` — `b.db` is reassigned to `tx` on
the preceding line, so `b.doInTX()` actually uses the transaction. The
analyzer sees the original `b.db` identity and flags it.
- `coderd/database/db_test.go` — test intentionally passes the outer
store to `require.Equal` to assert that nested `InTx` returns the same
handle.

Suppressions use `// intxcheck:ignore` instead of `//nolint:intxcheck`
because `intxcheck` runs as a standalone `go/analysis` tool outside
golangci-lint. golangci-lint's `nolintlint` checker flags `//nolint`
directives for linters it doesn't control, so we use a custom comment
prefix to avoid that conflict.
2026-04-17 00:07:30 +10:00

156 lines
3.9 KiB
Go

package example
import "context"
type TxOptions struct{}
type Store interface {
InTx(func(Store) error, *TxOptions) error
GetUser(context.Context) (string, error)
GetConfig(context.Context) (string, error)
}
type Server struct {
db Store
}
type wrapper struct {
db Store
}
func helper(context.Context, Store) {}
func helperWithDB(ctx context.Context, db Store) {
_, _ = db.GetUser(ctx)
}
func shadowingOK(ctx context.Context, db Store) error {
return db.InTx(func(db Store) error {
_, _ = db.GetUser(ctx)
return nil
}, nil)
}
func pkgFuncOK(ctx context.Context, db Store) error {
return db.InTx(func(tx Store) error {
helperWithDB(ctx, tx)
return nil
}, nil)
}
func (s *Server) directMisuse(ctx context.Context) error {
return s.db.InTx(func(tx Store) error {
_, _ = s.db.GetUser(ctx) // want "outer store 's[.]db' used inside InTx; use transaction store 'tx' instead"
return nil
}, nil)
}
func (s *Server) passThroughMisuse(ctx context.Context) error {
return s.db.InTx(func(tx Store) error {
helper(ctx, s.db) // want "outer store 's[.]db' passed as argument inside InTx; use transaction store 'tx' instead"
return nil
}, nil)
}
func (s *Server) indirectMisuse(ctx context.Context) error {
return s.db.InTx(func(tx Store) error {
s.getConfig(ctx) // want "call to 's[.]getConfig' inside InTx uses outer store 's[.]db'; pass 'tx' through the helper or hoist the call"
return nil
}, nil)
}
func (s *Server) shadowedLocalOK(ctx context.Context) error {
return s.db.InTx(func(tx Store) error {
s := wrapper{db: tx}
_, _ = s.db.GetUser(ctx)
return nil
}, nil)
}
func (s *Server) aliasedStoreMisuse(ctx context.Context) error {
return s.db.InTx(func(tx Store) error {
outer := s.db
_, _ = outer.GetUser(ctx) // want "outer store 's[.]db' used inside InTx; use transaction store 'tx' instead"
return nil
}, nil)
}
func (s *Server) aliasedHelperMisuse(ctx context.Context) error {
return s.db.InTx(func(tx Store) error {
alias := s
alias.getConfig(ctx) // want "call to 'alias[.]getConfig' inside InTx uses outer store 's[.]db'; pass 'tx' through the helper or hoist the call"
return nil
}, nil)
}
func (s *Server) goFuncLiteralMisuse(ctx context.Context) error {
return s.db.InTx(func(tx Store) error {
go func() {
_, _ = s.db.GetUser(ctx) // want "outer store 's[.]db' used inside InTx; use transaction store 'tx' instead"
}()
return nil
}, nil)
}
func (s *Server) goFuncLiteralArgMisuse(ctx context.Context) error {
return s.db.InTx(func(tx Store) error {
go func(db Store) {
_, _ = db.GetUser(ctx)
}(s.db) // want "outer store 's[.]db' passed as argument inside InTx; use transaction store 'tx' instead"
return nil
}, nil)
}
func (s *Server) deferFuncLiteralMisuse(ctx context.Context) error {
return s.db.InTx(func(tx Store) error {
defer func() {
_, _ = s.db.GetUser(ctx) // want "outer store 's[.]db' used inside InTx; use transaction store 'tx' instead"
}()
return nil
}, nil)
}
func (s *Server) immediateFuncLiteralMisuse(ctx context.Context) error {
return s.db.InTx(func(tx Store) error {
func() {
_, _ = s.db.GetUser(ctx) // want "outer store 's[.]db' used inside InTx; use transaction store 'tx' instead"
}()
return nil
}, nil)
}
func (s *Server) suppressedCase(ctx context.Context) error {
return s.db.InTx(func(tx Store) error {
_, _ = s.db.GetUser(ctx) // intxcheck:ignore
return nil
}, nil)
}
func (srv *Server) getConfig(ctx context.Context) string {
value, _ := srv.db.GetConfig(ctx)
return value
}
func (s *Server) correctUsage(ctx context.Context) error {
return s.db.InTx(func(tx Store) error {
_, _ = tx.GetUser(ctx)
return nil
}, nil)
}
func (s *Server) safeHelper(ctx context.Context) error {
return s.db.InTx(func(tx Store) error {
s.formatName("test")
return nil
}, nil)
}
func (s *Server) formatName(name string) string {
return name
}
func (s *Server) outsideInTx(ctx context.Context) error {
_, _ = s.db.GetUser(ctx)
return nil
}