Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 55e525f

Browse files
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.
1 parent 3452ab3 commit 55e525f

10 files changed

Lines changed: 785 additions & 54 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,7 @@ lint/go:
721721
linter_ver=$$(grep -oE 'GOLANGCI_LINT_VERSION=\S+' dogfood/coder/ubuntu-26.04/Dockerfile | cut -d '=' -f 2)
722722
go run github.com/golangci/golangci-lint/cmd/golangci-lint@v$$linter_ver run
723723
go tool github.com/coder/paralleltestctx/cmd/paralleltestctx -custom-funcs="testutil.Context" ./...
724+
go run ./scripts/intxcheck ./...
724725
.PHONY: lint/go
725726

726727
lint/examples: | _gen/bin/examplegen

coderd/database/db_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func TestNestedInTx(t *testing.T) {
6060
err = db.InTx(func(outer database.Store) error {
6161
return outer.InTx(func(inner database.Store) error {
6262
//nolint:gocritic
63-
require.Equal(t, outer, inner, "should be same transaction")
63+
require.Equal(t, outer, inner, "should be same transaction") // intxcheck:ignore // intentional: test asserts nested InTx returns same store
6464

6565
_, err := inner.InsertUser(context.Background(), database.InsertUserParams{
6666
ID: uid,

coderd/database/dbfake/dbfake.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ func (b WorkspaceBuildBuilder) Do() WorkspaceResponse {
274274
err := b.db.InTx(func(tx database.Store) error {
275275
//nolint:revive // calls do on modified struct
276276
b.db = tx
277-
resp = b.doInTX()
277+
resp = b.doInTX() // intxcheck:ignore // b.db is reassigned to tx on the line above
278278
return nil
279279
}, nil)
280280
require.NoError(b.t, err)

coderd/wsbuilder/wsbuilder.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
490490
}
491491

492492
if b.templateVersionPresetID == uuid.Nil {
493-
presetID, err := prebuilds.FindMatchingPresetID(b.ctx, b.store, templateVersionID, names, values)
493+
presetID, err := prebuilds.FindMatchingPresetID(b.ctx, store, templateVersionID, names, values)
494494
if err != nil {
495495
return BuildError{http.StatusInternalServerError, "find matching preset", err}
496496
}
@@ -528,7 +528,7 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
528528
return BuildError{code, "insert workspace build", err}
529529
}
530530

531-
task, err := b.getWorkspaceTask()
531+
task, err := b.getWorkspaceTask(store)
532532
if err != nil {
533533
return BuildError{http.StatusInternalServerError, "get task by workspace id", err}
534534
}
@@ -677,11 +677,11 @@ func (b *Builder) getTemplateVersionID() (uuid.UUID, error) {
677677

678678
// getWorkspaceTask returns the task associated with the workspace, if any.
679679
// If no task exists, it returns (nil, nil).
680-
func (b *Builder) getWorkspaceTask() (*database.Task, error) {
680+
func (b *Builder) getWorkspaceTask(store database.Store) (*database.Task, error) {
681681
if b.hasTask != nil {
682682
return b.task, nil
683683
}
684-
t, err := b.store.GetTaskByWorkspaceID(b.ctx, b.workspace.ID)
684+
t, err := store.GetTaskByWorkspaceID(b.ctx, b.workspace.ID)
685685
if err != nil {
686686
if xerrors.Is(err, sql.ErrNoRows) {
687687
b.hasTask = ptr.Ref(false)
@@ -1382,7 +1382,7 @@ func (b *Builder) checkUsage() error {
13821382
return BuildError{http.StatusInternalServerError, "Failed to fetch template version", err}
13831383
}
13841384

1385-
task, err := b.getWorkspaceTask()
1385+
task, err := b.getWorkspaceTask(b.store)
13861386
if err != nil {
13871387
return BuildError{http.StatusInternalServerError, "Failed to fetch workspace task", err}
13881388
}

enterprise/dbcrypt/dbcrypt.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,7 @@ func (db *dbCrypt) ensureEncrypted(ctx context.Context) error {
875875
}
876876

877877
// If we get here, then we have a new key that we need to insert.
878-
return db.InsertDBCryptKey(ctx, database.InsertDBCryptKeyParams{
878+
return s.InsertDBCryptKey(ctx, database.InsertDBCryptKeyParams{
879879
Number: highestNumber + 1,
880880
ActiveKeyDigest: db.primaryCipherDigest,
881881
Test: testValue,

0 commit comments

Comments
 (0)