Skip to content

Commit cc8870e

Browse files
committed
fix: context.Background and context.TODO properly mark the loop as non-fat
1 parent 0e5885d commit cc8870e

File tree

7 files changed

+111
-254
lines changed

7 files changed

+111
-254
lines changed

.golangci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ linters:
66
disable:
77
- depguard
88
- exhaustruct
9+
- wsl
10+
- noinlineerr
11+
settings:
12+
cyclop:
13+
max-complexity: 20
914
formatters:
1015
enable:
1116
- goimports

pkg/analyzer/analyzer.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ func getBody(node ast.Node) (*ast.BlockStmt, error) {
172172
}
173173

174174
func findNestedContext(pass *analysis.Pass, node ast.Node, stmts []ast.Stmt) *ast.AssignStmt {
175+
// Track which context variables have been reset to a known empty context
176+
resetContexts := make(map[string]bool)
177+
175178
for _, stmt := range stmts {
176179
// Recurse if necessary
177180
stmtList := getStmtList(stmt)
@@ -198,8 +201,21 @@ func findNestedContext(pass *analysis.Pass, node ast.Node, stmts []ast.Stmt) *as
198201
continue
199202
}
200203

201-
// Ignore [context.Background] & [context.TODO].
204+
// Get the variable name being assigned to
205+
varName := getVarName(pass, assignStmt)
206+
207+
// If the assignment is to a known empty context, mark this variable as reset
202208
if isContextFunction(assignStmt.Rhs[0], "Background", "TODO") {
209+
if varName != "" {
210+
resetContexts[varName] = true
211+
}
212+
213+
continue
214+
}
215+
216+
// If this variable was previously reset to a known empty context in this block,
217+
// it's safe to modify it
218+
if varName != "" && resetContexts[varName] {
203219
continue
204220
}
205221

@@ -218,6 +234,21 @@ func findNestedContext(pass *analysis.Pass, node ast.Node, stmts []ast.Stmt) *as
218234
return nil
219235
}
220236

237+
func getVarName(pass *analysis.Pass, assignStmt *ast.AssignStmt) string {
238+
varName := ""
239+
240+
if ident, ok := assignStmt.Lhs[0].(*ast.Ident); ok {
241+
varName = ident.Name
242+
} else if sel, ok := assignStmt.Lhs[0].(*ast.SelectorExpr); ok {
243+
// For struct fields like tc.ctx
244+
if rendered, err := render(pass.Fset, sel); err == nil {
245+
varName = string(rendered)
246+
}
247+
}
248+
249+
return varName
250+
}
251+
221252
func getStmtList(stmt ast.Stmt) []ast.Stmt {
222253
switch typedStmt := stmt.(type) {
223254
case *ast.BlockStmt:
@@ -240,7 +271,9 @@ func getStmtList(stmt ast.Stmt) []ast.Stmt {
240271
// render returns the pretty-print of the given node.
241272
func render(fset *token.FileSet, x interface{}) ([]byte, error) {
242273
var buf bytes.Buffer
243-
if err := printer.Fprint(&buf, fset, x); err != nil {
274+
275+
err := printer.Fprint(&buf, fset, x)
276+
if err != nil {
244277
return nil, fmt.Errorf("printing node: %w", err)
245278
}
246279

pkg/analyzer/analyzer_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func TestAnalyzer(t *testing.T) {
5353
}
5454
}
5555

56-
analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), anlzr, test.dir)
56+
analysistest.Run(t, analysistest.TestData(), anlzr, test.dir)
5757
})
5858
}
5959
}
@@ -65,3 +65,11 @@ func TestAnalyzer_cgo(t *testing.T) {
6565

6666
analysistest.Run(t, analysistest.TestData(), a, "cgo")
6767
}
68+
69+
func TestSuggestedFixes(t *testing.T) {
70+
t.Parallel()
71+
72+
a := analyzer.NewAnalyzer()
73+
74+
analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), a, "suggestedfix")
75+
}

pkg/analyzer/testdata/src/common/example.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
func example() {
99
ctx := context.Background()
10+
ctx2 := context.Background()
1011

1112
for i := 0; i < 10; i++ {
1213
ctx := context.WithValue(ctx, "key", i)
@@ -18,6 +19,15 @@ func example() {
1819
ctx = context.WithValue(ctx, "other", "val")
1920
}
2021

22+
for i := 0; i < 10; i++ {
23+
ctx = context.Background()
24+
// This is fine because the first action was to re-assign to a known empty context.
25+
ctx = context.WithValue(ctx, "key", i)
26+
ctx = context.WithValue(ctx, "other", "val")
27+
// Not OK because this var has NOT been re-assigned to a known empty context.
28+
ctx2 = context.WithValue(ctx2, "key", i) // want "nested context in loop"
29+
}
30+
2131
for item := range []string{"one", "two", "three"} {
2232
ctx = wrapContext(ctx) // want "nested context in loop"
2333
ctx := context.WithValue(ctx, "key", item)
@@ -241,11 +251,29 @@ func testCasesInit(t *testing.T) {
241251
ctx: context.WithValue(context.Background(), "key", "value"),
242252
},
243253
}
254+
255+
// This is fine because the first action is to re-assign to a known empty context.
244256
for _, tc := range cases {
245257
t.Run("some test", func(t *testing.T) {
246258
if tc.ctx == nil {
247259
tc.ctx = context.Background()
260+
tc.ctx = context.WithValue(tc.ctx, "key", "value")
248261
}
249262
})
250263
}
251264
}
265+
266+
var (
267+
globalCtx context.Context
268+
globalCancel context.CancelFunc
269+
)
270+
271+
func BeforeSuite(_ func()) interface{} {
272+
return nil
273+
}
274+
275+
// This is fine because the first action is to re-assign to a known empty context.
276+
var _ = BeforeSuite(func() {
277+
globalCtx = context.TODO()
278+
globalCtx, globalCancel = context.WithCancel(globalCtx)
279+
})

0 commit comments

Comments
 (0)