1 | // Copyright 2013 The Go Authors. All rights reserved. |
---|---|
2 | // Use of this source code is governed by a BSD-style |
3 | // license that can be found in the LICENSE file. |
4 | |
5 | // Package copylock defines an Analyzer that checks for locks |
6 | // erroneously passed by value. |
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/analysis/passes/internal/analysisutil" |
19 | "golang.org/x/tools/go/ast/inspector" |
20 | "golang.org/x/tools/internal/typeparams" |
21 | ) |
22 | |
23 | const Doc = `check for locks erroneously passed by value |
24 | |
25 | Inadvertently copying a value containing a lock, such as sync.Mutex or |
26 | sync.WaitGroup, may cause both copies to malfunction. Generally such |
27 | values should be referred to through a pointer.` |
28 | |
29 | var Analyzer = &analysis.Analyzer{ |
30 | Name: "copylocks", |
31 | Doc: Doc, |
32 | Requires: []*analysis.Analyzer{inspect.Analyzer}, |
33 | RunDespiteErrors: true, |
34 | Run: run, |
35 | } |
36 | |
37 | func run(pass *analysis.Pass) (interface{}, error) { |
38 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) |
39 | |
40 | nodeFilter := []ast.Node{ |
41 | (*ast.AssignStmt)(nil), |
42 | (*ast.CallExpr)(nil), |
43 | (*ast.CompositeLit)(nil), |
44 | (*ast.FuncDecl)(nil), |
45 | (*ast.FuncLit)(nil), |
46 | (*ast.GenDecl)(nil), |
47 | (*ast.RangeStmt)(nil), |
48 | (*ast.ReturnStmt)(nil), |
49 | } |
50 | inspect.Preorder(nodeFilter, func(node ast.Node) { |
51 | switch node := node.(type) { |
52 | case *ast.RangeStmt: |
53 | checkCopyLocksRange(pass, node) |
54 | case *ast.FuncDecl: |
55 | checkCopyLocksFunc(pass, node.Name.Name, node.Recv, node.Type) |
56 | case *ast.FuncLit: |
57 | checkCopyLocksFunc(pass, "func", nil, node.Type) |
58 | case *ast.CallExpr: |
59 | checkCopyLocksCallExpr(pass, node) |
60 | case *ast.AssignStmt: |
61 | checkCopyLocksAssign(pass, node) |
62 | case *ast.GenDecl: |
63 | checkCopyLocksGenDecl(pass, node) |
64 | case *ast.CompositeLit: |
65 | checkCopyLocksCompositeLit(pass, node) |
66 | case *ast.ReturnStmt: |
67 | checkCopyLocksReturnStmt(pass, node) |
68 | } |
69 | }) |
70 | return nil, nil |
71 | } |
72 | |
73 | // checkCopyLocksAssign checks whether an assignment |
74 | // copies a lock. |
75 | func checkCopyLocksAssign(pass *analysis.Pass, as *ast.AssignStmt) { |
76 | for i, x := range as.Rhs { |
77 | if path := lockPathRhs(pass, x); path != nil { |
78 | pass.ReportRangef(x, "assignment copies lock value to %v: %v", analysisutil.Format(pass.Fset, as.Lhs[i]), path) |
79 | } |
80 | } |
81 | } |
82 | |
83 | // checkCopyLocksGenDecl checks whether lock is copied |
84 | // in variable declaration. |
85 | func checkCopyLocksGenDecl(pass *analysis.Pass, gd *ast.GenDecl) { |
86 | if gd.Tok != token.VAR { |
87 | return |
88 | } |
89 | for _, spec := range gd.Specs { |
90 | valueSpec := spec.(*ast.ValueSpec) |
91 | for i, x := range valueSpec.Values { |
92 | if path := lockPathRhs(pass, x); path != nil { |
93 | pass.ReportRangef(x, "variable declaration copies lock value to %v: %v", valueSpec.Names[i].Name, path) |
94 | } |
95 | } |
96 | } |
97 | } |
98 | |
99 | // checkCopyLocksCompositeLit detects lock copy inside a composite literal |
100 | func checkCopyLocksCompositeLit(pass *analysis.Pass, cl *ast.CompositeLit) { |
101 | for _, x := range cl.Elts { |
102 | if node, ok := x.(*ast.KeyValueExpr); ok { |
103 | x = node.Value |
104 | } |
105 | if path := lockPathRhs(pass, x); path != nil { |
106 | pass.ReportRangef(x, "literal copies lock value from %v: %v", analysisutil.Format(pass.Fset, x), path) |
107 | } |
108 | } |
109 | } |
110 | |
111 | // checkCopyLocksReturnStmt detects lock copy in return statement |
112 | func checkCopyLocksReturnStmt(pass *analysis.Pass, rs *ast.ReturnStmt) { |
113 | for _, x := range rs.Results { |
114 | if path := lockPathRhs(pass, x); path != nil { |
115 | pass.ReportRangef(x, "return copies lock value: %v", path) |
116 | } |
117 | } |
118 | } |
119 | |
120 | // checkCopyLocksCallExpr detects lock copy in the arguments to a function call |
121 | func checkCopyLocksCallExpr(pass *analysis.Pass, ce *ast.CallExpr) { |
122 | var id *ast.Ident |
123 | switch fun := ce.Fun.(type) { |
124 | case *ast.Ident: |
125 | id = fun |
126 | case *ast.SelectorExpr: |
127 | id = fun.Sel |
128 | } |
129 | if fun, ok := pass.TypesInfo.Uses[id].(*types.Builtin); ok { |
130 | switch fun.Name() { |
131 | case "new", "len", "cap", "Sizeof", "Offsetof", "Alignof": |
132 | return |
133 | } |
134 | } |
135 | for _, x := range ce.Args { |
136 | if path := lockPathRhs(pass, x); path != nil { |
137 | pass.ReportRangef(x, "call of %s copies lock value: %v", analysisutil.Format(pass.Fset, ce.Fun), path) |
138 | } |
139 | } |
140 | } |
141 | |
142 | // checkCopyLocksFunc checks whether a function might |
143 | // inadvertently copy a lock, by checking whether |
144 | // its receiver, parameters, or return values |
145 | // are locks. |
146 | func checkCopyLocksFunc(pass *analysis.Pass, name string, recv *ast.FieldList, typ *ast.FuncType) { |
147 | if recv != nil && len(recv.List) > 0 { |
148 | expr := recv.List[0].Type |
149 | if path := lockPath(pass.Pkg, pass.TypesInfo.Types[expr].Type, nil); path != nil { |
150 | pass.ReportRangef(expr, "%s passes lock by value: %v", name, path) |
151 | } |
152 | } |
153 | |
154 | if typ.Params != nil { |
155 | for _, field := range typ.Params.List { |
156 | expr := field.Type |
157 | if path := lockPath(pass.Pkg, pass.TypesInfo.Types[expr].Type, nil); path != nil { |
158 | pass.ReportRangef(expr, "%s passes lock by value: %v", name, path) |
159 | } |
160 | } |
161 | } |
162 | |
163 | // Don't check typ.Results. If T has a Lock field it's OK to write |
164 | // return T{} |
165 | // because that is returning the zero value. Leave result checking |
166 | // to the return statement. |
167 | } |
168 | |
169 | // checkCopyLocksRange checks whether a range statement |
170 | // might inadvertently copy a lock by checking whether |
171 | // any of the range variables are locks. |
172 | func checkCopyLocksRange(pass *analysis.Pass, r *ast.RangeStmt) { |
173 | checkCopyLocksRangeVar(pass, r.Tok, r.Key) |
174 | checkCopyLocksRangeVar(pass, r.Tok, r.Value) |
175 | } |
176 | |
177 | func checkCopyLocksRangeVar(pass *analysis.Pass, rtok token.Token, e ast.Expr) { |
178 | if e == nil { |
179 | return |
180 | } |
181 | id, isId := e.(*ast.Ident) |
182 | if isId && id.Name == "_" { |
183 | return |
184 | } |
185 | |
186 | var typ types.Type |
187 | if rtok == token.DEFINE { |
188 | if !isId { |
189 | return |
190 | } |
191 | obj := pass.TypesInfo.Defs[id] |
192 | if obj == nil { |
193 | return |
194 | } |
195 | typ = obj.Type() |
196 | } else { |
197 | typ = pass.TypesInfo.Types[e].Type |
198 | } |
199 | |
200 | if typ == nil { |
201 | return |
202 | } |
203 | if path := lockPath(pass.Pkg, typ, nil); path != nil { |
204 | pass.Reportf(e.Pos(), "range var %s copies lock: %v", analysisutil.Format(pass.Fset, e), path) |
205 | } |
206 | } |
207 | |
208 | type typePath []string |
209 | |
210 | // String pretty-prints a typePath. |
211 | func (path typePath) String() string { |
212 | n := len(path) |
213 | var buf bytes.Buffer |
214 | for i := range path { |
215 | if i > 0 { |
216 | fmt.Fprint(&buf, " contains ") |
217 | } |
218 | // The human-readable path is in reverse order, outermost to innermost. |
219 | fmt.Fprint(&buf, path[n-i-1]) |
220 | } |
221 | return buf.String() |
222 | } |
223 | |
224 | func lockPathRhs(pass *analysis.Pass, x ast.Expr) typePath { |
225 | if _, ok := x.(*ast.CompositeLit); ok { |
226 | return nil |
227 | } |
228 | if _, ok := x.(*ast.CallExpr); ok { |
229 | // A call may return a zero value. |
230 | return nil |
231 | } |
232 | if star, ok := x.(*ast.StarExpr); ok { |
233 | if _, ok := star.X.(*ast.CallExpr); ok { |
234 | // A call may return a pointer to a zero value. |
235 | return nil |
236 | } |
237 | } |
238 | return lockPath(pass.Pkg, pass.TypesInfo.Types[x].Type, nil) |
239 | } |
240 | |
241 | // lockPath returns a typePath describing the location of a lock value |
242 | // contained in typ. If there is no contained lock, it returns nil. |
243 | // |
244 | // The seenTParams map is used to short-circuit infinite recursion via type |
245 | // parameters. |
246 | func lockPath(tpkg *types.Package, typ types.Type, seenTParams map[*typeparams.TypeParam]bool) typePath { |
247 | if typ == nil { |
248 | return nil |
249 | } |
250 | |
251 | if tpar, ok := typ.(*typeparams.TypeParam); ok { |
252 | if seenTParams == nil { |
253 | // Lazily allocate seenTParams, since the common case will not involve |
254 | // any type parameters. |
255 | seenTParams = make(map[*typeparams.TypeParam]bool) |
256 | } |
257 | if seenTParams[tpar] { |
258 | return nil |
259 | } |
260 | seenTParams[tpar] = true |
261 | terms, err := typeparams.StructuralTerms(tpar) |
262 | if err != nil { |
263 | return nil // invalid type |
264 | } |
265 | for _, term := range terms { |
266 | subpath := lockPath(tpkg, term.Type(), seenTParams) |
267 | if len(subpath) > 0 { |
268 | if term.Tilde() { |
269 | // Prepend a tilde to our lock path entry to clarify the resulting |
270 | // diagnostic message. Consider the following example: |
271 | // |
272 | // func _[Mutex interface{ ~sync.Mutex; M() }](m Mutex) {} |
273 | // |
274 | // Here the naive error message will be something like "passes lock |
275 | // by value: Mutex contains sync.Mutex". This is misleading because |
276 | // the local type parameter doesn't actually contain sync.Mutex, |
277 | // which lacks the M method. |
278 | // |
279 | // With tilde, it is clearer that the containment is via an |
280 | // approximation element. |
281 | subpath[len(subpath)-1] = "~" + subpath[len(subpath)-1] |
282 | } |
283 | return append(subpath, typ.String()) |
284 | } |
285 | } |
286 | return nil |
287 | } |
288 | |
289 | for { |
290 | atyp, ok := typ.Underlying().(*types.Array) |
291 | if !ok { |
292 | break |
293 | } |
294 | typ = atyp.Elem() |
295 | } |
296 | |
297 | ttyp, ok := typ.Underlying().(*types.Tuple) |
298 | if ok { |
299 | for i := 0; i < ttyp.Len(); i++ { |
300 | subpath := lockPath(tpkg, ttyp.At(i).Type(), seenTParams) |
301 | if subpath != nil { |
302 | return append(subpath, typ.String()) |
303 | } |
304 | } |
305 | return nil |
306 | } |
307 | |
308 | // We're only interested in the case in which the underlying |
309 | // type is a struct. (Interfaces and pointers are safe to copy.) |
310 | styp, ok := typ.Underlying().(*types.Struct) |
311 | if !ok { |
312 | return nil |
313 | } |
314 | |
315 | // We're looking for cases in which a pointer to this type |
316 | // is a sync.Locker, but a value is not. This differentiates |
317 | // embedded interfaces from embedded values. |
318 | if types.Implements(types.NewPointer(typ), lockerType) && !types.Implements(typ, lockerType) { |
319 | return []string{typ.String()} |
320 | } |
321 | |
322 | // In go1.10, sync.noCopy did not implement Locker. |
323 | // (The Unlock method was added only in CL 121876.) |
324 | // TODO(adonovan): remove workaround when we drop go1.10. |
325 | if named, ok := typ.(*types.Named); ok && |
326 | named.Obj().Name() == "noCopy" && |
327 | named.Obj().Pkg().Path() == "sync" { |
328 | return []string{typ.String()} |
329 | } |
330 | |
331 | nfields := styp.NumFields() |
332 | for i := 0; i < nfields; i++ { |
333 | ftyp := styp.Field(i).Type() |
334 | subpath := lockPath(tpkg, ftyp, seenTParams) |
335 | if subpath != nil { |
336 | return append(subpath, typ.String()) |
337 | } |
338 | } |
339 | |
340 | return nil |
341 | } |
342 | |
343 | var lockerType *types.Interface |
344 | |
345 | // Construct a sync.Locker interface type. |
346 | func init() { |
347 | nullary := types.NewSignature(nil, nil, nil, false) // func() |
348 | methods := []*types.Func{ |
349 | types.NewFunc(token.NoPos, nil, "Lock", nullary), |
350 | types.NewFunc(token.NoPos, nil, "Unlock", nullary), |
351 | } |
352 | lockerType = types.NewInterface(methods, nil).Complete() |
353 | } |
354 |
Members