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