1
2
3
4
5
6
7 package copylock
8
9 import (
10 "bytes"
11 "fmt"
12 "go/ast"
13 "go/token"
14 "go/types"
15
16 "golang.org/x/tools/go/analysis"
17 "golang.org/x/tools/go/analysis/passes/inspect"
18 "golang.org/x/tools/go/ast/inspector"
19 "golang.org/x/tools/internal/astutil"
20 "golang.org/x/tools/internal/typeparams"
21 "golang.org/x/tools/internal/typesinternal"
22 "golang.org/x/tools/internal/versions"
23 )
24
25 const Doc = `check for locks erroneously passed by value
26
27 Inadvertently copying a value containing a lock, such as sync.Mutex or
28 sync.WaitGroup, may cause both copies to malfunction. Generally such
29 values should be referred to through a pointer.`
30
31 var Analyzer = &analysis.Analyzer{
32 Name: "copylocks",
33 Doc: Doc,
34 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/copylock",
35 Requires: []*analysis.Analyzer{inspect.Analyzer},
36 RunDespiteErrors: true,
37 Run: run,
38 }
39
40 func run(pass *analysis.Pass) (any, error) {
41 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
42
43 var goversion string
44 nodeFilter := []ast.Node{
45 (*ast.AssignStmt)(nil),
46 (*ast.CallExpr)(nil),
47 (*ast.CompositeLit)(nil),
48 (*ast.File)(nil),
49 (*ast.FuncDecl)(nil),
50 (*ast.FuncLit)(nil),
51 (*ast.GenDecl)(nil),
52 (*ast.RangeStmt)(nil),
53 (*ast.ReturnStmt)(nil),
54 }
55 inspect.WithStack(nodeFilter, func(node ast.Node, push bool, stack []ast.Node) bool {
56 if !push {
57 return false
58 }
59 switch node := node.(type) {
60 case *ast.File:
61 goversion = versions.FileVersion(pass.TypesInfo, node)
62 case *ast.RangeStmt:
63 checkCopyLocksRange(pass, node)
64 case *ast.FuncDecl:
65 checkCopyLocksFunc(pass, node.Name.Name, node.Recv, node.Type)
66 case *ast.FuncLit:
67 checkCopyLocksFunc(pass, "func", nil, node.Type)
68 case *ast.CallExpr:
69 checkCopyLocksCallExpr(pass, node)
70 case *ast.AssignStmt:
71 checkCopyLocksAssign(pass, node, goversion, parent(stack))
72 case *ast.GenDecl:
73 checkCopyLocksGenDecl(pass, node)
74 case *ast.CompositeLit:
75 checkCopyLocksCompositeLit(pass, node)
76 case *ast.ReturnStmt:
77 checkCopyLocksReturnStmt(pass, node)
78 }
79 return true
80 })
81 return nil, nil
82 }
83
84
85
86 func checkCopyLocksAssign(pass *analysis.Pass, assign *ast.AssignStmt, goversion string, parent ast.Node) {
87 lhs := assign.Lhs
88 for i, x := range assign.Rhs {
89 if path := lockPathRhs(pass, x); path != nil {
90 pass.ReportRangef(x, "assignment copies lock value to %v: %v", astutil.Format(pass.Fset, assign.Lhs[i]), path)
91 lhs = nil
92 }
93 }
94
95
96
97
98 if assign.Tok == token.DEFINE && versions.AtLeast(goversion, versions.Go1_22) {
99 if parent, _ := parent.(*ast.ForStmt); parent != nil && parent.Init == assign {
100 for _, l := range lhs {
101 if id, ok := l.(*ast.Ident); ok && id.Name != "_" {
102 if obj := pass.TypesInfo.Defs[id]; obj != nil && obj.Type() != nil {
103 if path := lockPath(pass.Pkg, obj.Type(), nil); path != nil {
104 pass.ReportRangef(l, "for loop iteration copies lock value to %v: %v", astutil.Format(pass.Fset, l), path)
105 }
106 }
107 }
108 }
109 }
110 }
111 }
112
113
114
115 func checkCopyLocksGenDecl(pass *analysis.Pass, gd *ast.GenDecl) {
116 if gd.Tok != token.VAR {
117 return
118 }
119 for _, spec := range gd.Specs {
120 valueSpec := spec.(*ast.ValueSpec)
121 for i, x := range valueSpec.Values {
122 if path := lockPathRhs(pass, x); path != nil {
123 pass.ReportRangef(x, "variable declaration copies lock value to %v: %v", valueSpec.Names[i].Name, path)
124 }
125 }
126 }
127 }
128
129
130 func checkCopyLocksCompositeLit(pass *analysis.Pass, cl *ast.CompositeLit) {
131 for _, x := range cl.Elts {
132 if node, ok := x.(*ast.KeyValueExpr); ok {
133 x = node.Value
134 }
135 if path := lockPathRhs(pass, x); path != nil {
136 pass.ReportRangef(x, "literal copies lock value from %v: %v", astutil.Format(pass.Fset, x), path)
137 }
138 }
139 }
140
141
142 func checkCopyLocksReturnStmt(pass *analysis.Pass, rs *ast.ReturnStmt) {
143 for _, x := range rs.Results {
144 if path := lockPathRhs(pass, x); path != nil {
145 pass.ReportRangef(x, "return copies lock value: %v", path)
146 }
147 }
148 }
149
150
151 func checkCopyLocksCallExpr(pass *analysis.Pass, ce *ast.CallExpr) {
152 var id *ast.Ident
153 switch fun := ce.Fun.(type) {
154 case *ast.Ident:
155 id = fun
156 case *ast.SelectorExpr:
157 id = fun.Sel
158 }
159 if fun, ok := pass.TypesInfo.Uses[id].(*types.Builtin); ok {
160 switch fun.Name() {
161 case "len", "cap", "Sizeof", "Offsetof", "Alignof":
162
163
164
165 return
166 }
167 }
168 for _, x := range ce.Args {
169 if path := lockPathRhs(pass, x); path != nil {
170 pass.ReportRangef(x, "call of %s copies lock value: %v", astutil.Format(pass.Fset, ce.Fun), path)
171 }
172 }
173 }
174
175
176
177
178
179 func checkCopyLocksFunc(pass *analysis.Pass, name string, recv *ast.FieldList, typ *ast.FuncType) {
180 if recv != nil && len(recv.List) > 0 {
181 expr := recv.List[0].Type
182 if path := lockPath(pass.Pkg, pass.TypesInfo.Types[expr].Type, nil); path != nil {
183 pass.ReportRangef(expr, "%s passes lock by value: %v", name, path)
184 }
185 }
186
187 if typ.Params != nil {
188 for _, field := range typ.Params.List {
189 expr := field.Type
190 if path := lockPath(pass.Pkg, pass.TypesInfo.Types[expr].Type, nil); path != nil {
191 pass.ReportRangef(expr, "%s passes lock by value: %v", name, path)
192 }
193 }
194 }
195
196
197
198
199
200 }
201
202
203
204
205 func checkCopyLocksRange(pass *analysis.Pass, r *ast.RangeStmt) {
206 checkCopyLocksRangeVar(pass, r.Tok, r.Key)
207 checkCopyLocksRangeVar(pass, r.Tok, r.Value)
208 }
209
210 func checkCopyLocksRangeVar(pass *analysis.Pass, rtok token.Token, e ast.Expr) {
211 if e == nil {
212 return
213 }
214 id, isId := e.(*ast.Ident)
215 if isId && id.Name == "_" {
216 return
217 }
218
219 var typ types.Type
220 if rtok == token.DEFINE {
221 if !isId {
222 return
223 }
224 obj := pass.TypesInfo.Defs[id]
225 if obj == nil {
226 return
227 }
228 typ = obj.Type()
229 } else {
230 typ = pass.TypesInfo.Types[e].Type
231 }
232
233 if typ == nil {
234 return
235 }
236 if path := lockPath(pass.Pkg, typ, nil); path != nil {
237 pass.Reportf(e.Pos(), "range var %s copies lock: %v", astutil.Format(pass.Fset, e), path)
238 }
239 }
240
241 type typePath []string
242
243
244 func (path typePath) String() string {
245 n := len(path)
246 var buf bytes.Buffer
247 for i := range path {
248 if i > 0 {
249 fmt.Fprint(&buf, " contains ")
250 }
251
252 fmt.Fprint(&buf, path[n-i-1])
253 }
254 return buf.String()
255 }
256
257 func lockPathRhs(pass *analysis.Pass, x ast.Expr) typePath {
258 x = ast.Unparen(x)
259
260 if _, ok := x.(*ast.CompositeLit); ok {
261 return nil
262 }
263 if _, ok := x.(*ast.CallExpr); ok {
264
265 return nil
266 }
267 if star, ok := x.(*ast.StarExpr); ok {
268 if _, ok := ast.Unparen(star.X).(*ast.CallExpr); ok {
269
270 return nil
271 }
272 }
273 if tv, ok := pass.TypesInfo.Types[x]; ok && tv.IsValue() {
274 return lockPath(pass.Pkg, tv.Type, nil)
275 }
276 return nil
277 }
278
279
280
281
282
283 func lockPath(tpkg *types.Package, typ types.Type, seen map[types.Type]bool) typePath {
284 if typ == nil || seen[typ] {
285 return nil
286 }
287 if seen == nil {
288 seen = make(map[types.Type]bool)
289 }
290 seen[typ] = true
291
292 if tpar, ok := types.Unalias(typ).(*types.TypeParam); ok {
293 terms, err := typeparams.StructuralTerms(tpar)
294 if err != nil {
295 return nil
296 }
297 for _, term := range terms {
298 subpath := lockPath(tpkg, term.Type(), seen)
299 if len(subpath) > 0 {
300 if term.Tilde() {
301
302
303
304
305
306
307
308
309
310
311
312
313 subpath[len(subpath)-1] = "~" + subpath[len(subpath)-1]
314 }
315 return append(subpath, typ.String())
316 }
317 }
318 return nil
319 }
320
321 for {
322 atyp, ok := typ.Underlying().(*types.Array)
323 if !ok {
324 break
325 }
326 typ = atyp.Elem()
327 }
328
329 ttyp, ok := typ.Underlying().(*types.Tuple)
330 if ok {
331 for v := range ttyp.Variables() {
332 subpath := lockPath(tpkg, v.Type(), seen)
333 if subpath != nil {
334 return append(subpath, typ.String())
335 }
336 }
337 return nil
338 }
339
340
341
342 styp, ok := typ.Underlying().(*types.Struct)
343 if !ok {
344 return nil
345 }
346
347
348
349
350 if types.Implements(types.NewPointer(typ), lockerType) && !types.Implements(typ, lockerType) {
351 return []string{typ.String()}
352 }
353
354
355
356
357 if typesinternal.IsTypeNamed(typ, "sync", "noCopy") {
358 return []string{typ.String()}
359 }
360
361 nfields := styp.NumFields()
362 for i := range nfields {
363 ftyp := styp.Field(i).Type()
364 subpath := lockPath(tpkg, ftyp, seen)
365 if subpath != nil {
366 return append(subpath, typ.String())
367 }
368 }
369
370 return nil
371 }
372
373
374 func parent(stack []ast.Node) ast.Node {
375 if len(stack) >= 2 {
376 return stack[len(stack)-2]
377 }
378 return nil
379 }
380
381 var lockerType *types.Interface
382
383
384 func init() {
385 nullary := types.NewSignatureType(nil, nil, nil, nil, nil, false)
386 methods := []*types.Func{
387 types.NewFunc(token.NoPos, nil, "Lock", nullary),
388 types.NewFunc(token.NoPos, nil, "Unlock", nullary),
389 }
390 lockerType = types.NewInterface(methods, nil).Complete()
391 }
392
View as plain text