mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
55e525fc28
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.
156 lines
3.9 KiB
Go
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
|
|
}
|