mirror of
https://github.com/coder/coder.git
synced 2026-06-04 13:38:21 +00:00
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.
This commit is contained in:
+155
@@ -0,0 +1,155 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user