| 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