feat(pkg/paralleltestctx): generalise to catch outer-var reassignment in parallel subtests#4
Merged
Merged
Conversation
Add an opt-in flag that flags an adjacent flake pattern: an outer-scope variable reassigned with `=` inside a parallel subtest. Sibling parallel subtests can race on the shared variable, and the fix is almost always to shadow with `:=`. Example of the bug shape this catches (mirrors coder/coder#25980): func TestX(t *testing.T) { t.Parallel() var err error for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() _, err = doThing(tc) // shared across siblings require.NoError(t, err) }) } } The check is gated behind `-check-outer-assignments` (default off) so existing users see no behavior change. Rule: 1. Enclosing scope contains a t.Parallel() call. 2. Assignment uses `=` (not `:=`). 3. LHS is an *ast.Ident (not SelectorExpr, IndexExpr, etc.). 4. Ident resolves to a *types.Var declared outside the scope. 5. Object isn't already covered by the timeout-context rule. Empirical sweep on coder/coder (930 test files): 4 findings, all actionable (the diagnostic's suggested `:=` shadow is the correct fix in each case).
Empirical sweep on coder/coder (930 test files) found 4 diagnostics under this rule, all actionable (the suggested `:=` shadow is the correct fix in each case). Zero false positives at the actionable-bug standard, so the opt-in gate is unnecessary friction. Remove the -check-outer-assignments flag and run the check unconditionally alongside the existing context rule.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Generalises the linter to catch an adjacent flake pattern: an outer-scope variable reassigned with
=inside a parallel subtest. Sibling parallel subtests can race on the shared variable, and the fix is almost always to shadow with:=.Motivation
Mirrors the bug shape from coder/coder#25980:
The existing context-specific rule already emits the exact diagnostic this needs ("did you mean to shadow the variable?") for the
context.Contextcase. This change applies the same idea to any outer-scope*types.Var.Rule
A diagnostic fires when all of these hold:
t.Parallel()call.=, not:=.*ast.Ident(notSelectorExpr,IndexExpr, etc., soouter.Field = xandouter[k] = xdo not fire).*types.Vardeclared outside the current scope.Default behaviour
The check runs unconditionally alongside the existing context-specific rule. There is no flag. Existing users will see new diagnostics on their next run.
I originally gated this behind
-check-outer-assignments(off by default), then dropped the gate after the empirical evaluation below.Empirical evaluation
Ran against the full
coder/coderrepo (930 test files): 4 findings, 0 false positives. The diagnostic's suggested:=shadow is the correct fix in each case:coderd/mcp/mcp_e2e_test.go:533(err = mcpClient.Start(ctx))scripts/workspace-runtime-audit/runtimeaudit_test.go:36,39(runtimeAuditScript = strings.ReplaceAll(...))//go:embedvar mutated inside a parallel testagent/unit/graph_test.go:225(graph = testFunc(t))var graphshould move into the closure asgraph := …I also verified the rule catches the exact line PR #25980 fixed by temporarily reverting that one character locally.
Considered, not shipped
coder/coder(it didn't suppress any of the 4 findings). Could be added later if a real corpus shows write-only fan-in FPs.mu.Lock()/Unlock()pair to suppress synchronized accumulators. Brittle (people usesync.RWMutex,defer mu.Unlock(), helper wrappers); didn't appear in the empirical sweep. Recommend//nolintcomments instead if the case comes up.