Test and fix cases with nested contexts

As long as the context is rooted in a non-pointer value that has a new
copy in the loop, it is as safe to copy that value as it is to copy the
context. So only report such cases when they are indirected and thus
shared.
This commit is contained in:
Michael Urman 2024-05-29 09:47:58 -05:00
parent 6be4ab74b8
commit 3e9e29f41c
2 changed files with 62 additions and 0 deletions

View file

@ -7,6 +7,7 @@ import (
"go/ast" "go/ast"
"go/printer" "go/printer"
"go/token" "go/token"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
@ -54,6 +55,14 @@ func run(pass *analysis.Pass) (interface{}, error) {
break break
} }
if lhs := getRootIdent(pass, assignStmt.Lhs[0]); lhs != nil {
if obj := pass.TypesInfo.ObjectOf(lhs); obj != nil {
if obj.Pos() >= body.Pos() && obj.Pos() < body.End() {
continue
}
}
}
suggestedStmt := ast.AssignStmt{ suggestedStmt := ast.AssignStmt{
Lhs: assignStmt.Lhs, Lhs: assignStmt.Lhs,
TokPos: assignStmt.TokPos, TokPos: assignStmt.TokPos,
@ -103,6 +112,24 @@ func getBody(node ast.Node) (*ast.BlockStmt, error) {
return nil, errUnknown return nil, errUnknown
} }
func getRootIdent(pass *analysis.Pass, node ast.Node) *ast.Ident {
for {
switch n := node.(type) {
case *ast.Ident:
return n
case *ast.IndexExpr:
node = n.X
case *ast.SelectorExpr:
if sel, ok := pass.TypesInfo.Selections[n]; ok && sel.Indirect() {
return nil // indirected (pointer) roots don't imply a (safe) copy
}
node = n.X
default:
return nil
}
}
}
// render returns the pretty-print of the given node // render returns the pretty-print of the given node
func render(fset *token.FileSet, x interface{}) (string, error) { func render(fset *token.FileSet, x interface{}) (string, error) {
var buf bytes.Buffer var buf bytes.Buffer

View file

@ -30,3 +30,38 @@ func example() {
func wrapContext(ctx context.Context) context.Context { func wrapContext(ctx context.Context) context.Context {
return context.WithoutCancel(ctx) return context.WithoutCancel(ctx)
} }
// storing contexts in a struct isn't recommended, but local copies of a non-pointer struct should act like local copies of a context.
func inStructs(ctx context.Context) {
for i := 0; i < 10; i++ {
c := struct{ Ctx context.Context }{ctx}
c.Ctx = context.WithValue(c.Ctx, "key", i)
c.Ctx = context.WithValue(c.Ctx, "other", "val")
}
for i := 0; i < 10; i++ {
c := []struct{ Ctx context.Context }{{ctx}}
c[0].Ctx = context.WithValue(c[0].Ctx, "key", i)
c[0].Ctx = context.WithValue(c[0].Ctx, "other", "val")
}
c := struct{ Ctx context.Context }{ctx}
for i := 0; i < 10; i++ {
c := c
c.Ctx = context.WithValue(c.Ctx, "key", i)
c.Ctx = context.WithValue(c.Ctx, "other", "val")
}
pc := &struct{ Ctx context.Context }{ctx}
for i := 0; i < 10; i++ {
c := pc
c.Ctx = context.WithValue(c.Ctx, "key", i) // want "nested context in loop"
c.Ctx = context.WithValue(c.Ctx, "other", "val")
}
for i := 0; i < 10; i++ {
c := []*struct{ Ctx context.Context }{{ctx}}
c[0].Ctx = context.WithValue(c[0].Ctx, "key", i) // want "nested context in loop"
c[0].Ctx = context.WithValue(c[0].Ctx, "other", "val")
}
}