GoPLS Viewer

Home|gopls/go/analysis/passes/lostcancel/lostcancel.go
1// Copyright 2016 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 lostcancel defines an Analyzer that checks for failure to
6// call a context cancellation function.
7package lostcancel
8
9import (
10    "fmt"
11    "go/ast"
12    "go/types"
13
14    "golang.org/x/tools/go/analysis"
15    "golang.org/x/tools/go/analysis/passes/ctrlflow"
16    "golang.org/x/tools/go/analysis/passes/inspect"
17    "golang.org/x/tools/go/ast/inspector"
18    "golang.org/x/tools/go/cfg"
19)
20
21const Doc = `check cancel func returned by context.WithCancel is called
22
23The cancellation function returned by context.WithCancel, WithTimeout,
24and WithDeadline must be called or the new context will remain live
25until its parent context is cancelled.
26(The background context is never cancelled.)`
27
28var Analyzer = &analysis.Analyzer{
29    Name"lostcancel",
30    Doc:  Doc,
31    Run:  run,
32    Requires: []*analysis.Analyzer{
33        inspect.Analyzer,
34        ctrlflow.Analyzer,
35    },
36}
37
38const debug = false
39
40var contextPackage = "context"
41
42// checkLostCancel reports a failure to the call the cancel function
43// returned by context.WithCancel, either because the variable was
44// assigned to the blank identifier, or because there exists a
45// control-flow path from the call to a return statement and that path
46// does not "use" the cancel function.  Any reference to the variable
47// counts as a use, even within a nested function literal.
48// If the variable's scope is larger than the function
49// containing the assignment, we assume that other uses exist.
50//
51// checkLostCancel analyzes a single named or literal function.
52func run(pass *analysis.Pass) (interface{}, error) {
53    // Fast path: bypass check if file doesn't use context.WithCancel.
54    if !hasImport(pass.PkgcontextPackage) {
55        return nilnil
56    }
57
58    // Call runFunc for each Func{Decl,Lit}.
59    inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
60    nodeTypes := []ast.Node{
61        (*ast.FuncLit)(nil),
62        (*ast.FuncDecl)(nil),
63    }
64    inspect.Preorder(nodeTypes, func(n ast.Node) {
65        runFunc(passn)
66    })
67    return nilnil
68}
69
70func runFunc(pass *analysis.Passnode ast.Node) {
71    // Find scope of function node
72    var funcScope *types.Scope
73    switch v := node.(type) {
74    case *ast.FuncLit:
75        funcScope = pass.TypesInfo.Scopes[v.Type]
76    case *ast.FuncDecl:
77        funcScope = pass.TypesInfo.Scopes[v.Type]
78    }
79
80    // Maps each cancel variable to its defining ValueSpec/AssignStmt.
81    cancelvars := make(map[*types.Var]ast.Node)
82
83    // TODO(adonovan): opt: refactor to make a single pass
84    // over the AST using inspect.WithStack and node types
85    // {FuncDecl,FuncLit,CallExpr,SelectorExpr}.
86
87    // Find the set of cancel vars to analyze.
88    stack := make([]ast.Node032)
89    ast.Inspect(node, func(n ast.Nodebool {
90        switch n.(type) {
91        case *ast.FuncLit:
92            if len(stack) > 0 {
93                return false // don't stray into nested functions
94            }
95        case nil:
96            stack = stack[:len(stack)-1// pop
97            return true
98        }
99        stack = append(stackn// push
100
101        // Look for [{AssignStmt,ValueSpec} CallExpr SelectorExpr]:
102        //
103        //   ctx, cancel    := context.WithCancel(...)
104        //   ctx, cancel     = context.WithCancel(...)
105        //   var ctx, cancel = context.WithCancel(...)
106        //
107        if !isContextWithCancel(pass.TypesInfon) || !isCall(stack[len(stack)-2]) {
108            return true
109        }
110        var id *ast.Ident // id of cancel var
111        stmt := stack[len(stack)-3]
112        switch stmt := stmt.(type) {
113        case *ast.ValueSpec:
114            if len(stmt.Names) > 1 {
115                id = stmt.Names[1]
116            }
117        case *ast.AssignStmt:
118            if len(stmt.Lhs) > 1 {
119                id_ = stmt.Lhs[1].(*ast.Ident)
120            }
121        }
122        if id != nil {
123            if id.Name == "_" {
124                pass.ReportRangef(id,
125                    "the cancel function returned by context.%s should be called, not discarded, to avoid a context leak",
126                    n.(*ast.SelectorExpr).Sel.Name)
127            } else if vok := pass.TypesInfo.Uses[id].(*types.Var); ok {
128                // If the cancel variable is defined outside function scope,
129                // do not analyze it.
130                if funcScope.Contains(v.Pos()) {
131                    cancelvars[v] = stmt
132                }
133            } else if vok := pass.TypesInfo.Defs[id].(*types.Var); ok {
134                cancelvars[v] = stmt
135            }
136        }
137        return true
138    })
139
140    if len(cancelvars) == 0 {
141        return // no need to inspect CFG
142    }
143
144    // Obtain the CFG.
145    cfgs := pass.ResultOf[ctrlflow.Analyzer].(*ctrlflow.CFGs)
146    var g *cfg.CFG
147    var sig *types.Signature
148    switch node := node.(type) {
149    case *ast.FuncDecl:
150        sig_ = pass.TypesInfo.Defs[node.Name].Type().(*types.Signature)
151        if node.Name.Name == "main" && sig.Recv() == nil && pass.Pkg.Name() == "main" {
152            // Returning from main.main terminates the process,
153            // so there's no need to cancel contexts.
154            return
155        }
156        g = cfgs.FuncDecl(node)
157
158    case *ast.FuncLit:
159        sig_ = pass.TypesInfo.Types[node.Type].Type.(*types.Signature)
160        g = cfgs.FuncLit(node)
161    }
162    if sig == nil {
163        return // missing type information
164    }
165
166    // Print CFG.
167    if debug {
168        fmt.Println(g.Format(pass.Fset))
169    }
170
171    // Examine the CFG for each variable in turn.
172    // (It would be more efficient to analyze all cancelvars in a
173    // single pass over the AST, but seldom is there more than one.)
174    for vstmt := range cancelvars {
175        if ret := lostCancelPath(passgvstmtsig); ret != nil {
176            lineno := pass.Fset.Position(stmt.Pos()).Line
177            pass.ReportRangef(stmt"the %s function is not used on all paths (possible context leak)"v.Name())
178            pass.ReportRangef(ret"this return statement may be reached without using the %s var defined on line %d"v.Name(), lineno)
179        }
180    }
181}
182
183func isCall(n ast.Nodebool { _ok := n.(*ast.CallExpr); return ok }
184
185func hasImport(pkg *types.Packagepath stringbool {
186    for _imp := range pkg.Imports() {
187        if imp.Path() == path {
188            return true
189        }
190    }
191    return false
192}
193
194// isContextWithCancel reports whether n is one of the qualified identifiers
195// context.With{Cancel,Timeout,Deadline}.
196func isContextWithCancel(info *types.Infon ast.Nodebool {
197    selok := n.(*ast.SelectorExpr)
198    if !ok {
199        return false
200    }
201    switch sel.Sel.Name {
202    case "WithCancel""WithTimeout""WithDeadline":
203    default:
204        return false
205    }
206    if xok := sel.X.(*ast.Ident); ok {
207        if pkgnameok := info.Uses[x].(*types.PkgName); ok {
208            return pkgname.Imported().Path() == contextPackage
209        }
210        // Import failed, so we can't check package path.
211        // Just check the local package name (heuristic).
212        return x.Name == "context"
213    }
214    return false
215}
216
217// lostCancelPath finds a path through the CFG, from stmt (which defines
218// the 'cancel' variable v) to a return statement, that doesn't "use" v.
219// If it finds one, it returns the return statement (which may be synthetic).
220// sig is the function's type, if known.
221func lostCancelPath(pass *analysis.Passg *cfg.CFGv *types.Varstmt ast.Nodesig *types.Signature) *ast.ReturnStmt {
222    vIsNamedResult := sig != nil && tupleContains(sig.Results(), v)
223
224    // uses reports whether stmts contain a "use" of variable v.
225    uses := func(pass *analysis.Passv *types.Varstmts []ast.Nodebool {
226        found := false
227        for _stmt := range stmts {
228            ast.Inspect(stmt, func(n ast.Nodebool {
229                switch n := n.(type) {
230                case *ast.Ident:
231                    if pass.TypesInfo.Uses[n] == v {
232                        found = true
233                    }
234                case *ast.ReturnStmt:
235                    // A naked return statement counts as a use
236                    // of the named result variables.
237                    if n.Results == nil && vIsNamedResult {
238                        found = true
239                    }
240                }
241                return !found
242            })
243        }
244        return found
245    }
246
247    // blockUses computes "uses" for each block, caching the result.
248    memo := make(map[*cfg.Block]bool)
249    blockUses := func(pass *analysis.Passv *types.Varb *cfg.Blockbool {
250        resok := memo[b]
251        if !ok {
252            res = uses(passvb.Nodes)
253            memo[b] = res
254        }
255        return res
256    }
257
258    // Find the var's defining block in the CFG,
259    // plus the rest of the statements of that block.
260    var defblock *cfg.Block
261    var rest []ast.Node
262outer:
263    for _b := range g.Blocks {
264        for in := range b.Nodes {
265            if n == stmt {
266                defblock = b
267                rest = b.Nodes[i+1:]
268                break outer
269            }
270        }
271    }
272    if defblock == nil {
273        panic("internal error: can't find defining block for cancel var")
274    }
275
276    // Is v "used" in the remainder of its defining block?
277    if uses(passvrest) {
278        return nil
279    }
280
281    // Does the defining block return without using v?
282    if ret := defblock.Return(); ret != nil {
283        return ret
284    }
285
286    // Search the CFG depth-first for a path, from defblock to a
287    // return block, in which v is never "used".
288    seen := make(map[*cfg.Block]bool)
289    var search func(blocks []*cfg.Block) *ast.ReturnStmt
290    search = func(blocks []*cfg.Block) *ast.ReturnStmt {
291        for _b := range blocks {
292            if seen[b] {
293                continue
294            }
295            seen[b] = true
296
297            // Prune the search if the block uses v.
298            if blockUses(passvb) {
299                continue
300            }
301
302            // Found path to return statement?
303            if ret := b.Return(); ret != nil {
304                if debug {
305                    fmt.Printf("found path to return in block %s\n"b)
306                }
307                return ret // found
308            }
309
310            // Recur
311            if ret := search(b.Succs); ret != nil {
312                if debug {
313                    fmt.Printf(" from block %s\n"b)
314                }
315                return ret
316            }
317        }
318        return nil
319    }
320    return search(defblock.Succs)
321}
322
323func tupleContains(tuple *types.Tuplev *types.Varbool {
324    for i := 0i < tuple.Len(); i++ {
325        if tuple.At(i) == v {
326            return true
327        }
328    }
329    return false
330}
331
MembersX
lostCancelPath.RangeStmt_7808.BlockStmt.RangeStmt_7839.i
analysis
runFunc.stack
lostCancelPath
inspector
isContextWithCancel
tupleContains.i
lostCancelPath.g
lostCancelPath.BlockStmt.RangeStmt_8513.b
inspect
runFunc.RangeStmt_5078.v
hasImport.path
run.pass
lostCancelPath.seen
hasImport.RangeStmt_5596.imp
Doc
lostCancelPath.ret
lostCancelPath.BlockStmt.RangeStmt_8513.BlockStmt.ret
isCall.n
hasImport.pkg
run
runFunc.pass
runFunc.g
runFunc.RangeStmt_5078.BlockStmt.ret
lostCancelPath.BlockStmt.RangeStmt_6955.stmt
lostCancelPath.RangeStmt_7808.BlockStmt.RangeStmt_7839.n
ast
debug
runFunc.funcScope
runFunc.node
runFunc.RangeStmt_5078.stmt
run.nodeTypes
tupleContains
contextPackage
lostCancelPath.stmt
tupleContains.tuple
lostCancelPath.sig
lostCancelPath.BlockStmt.found
lostCancelPath.defblock
fmt
cfg
isContextWithCancel.n
runFunc.sig
lostCancelPath.memo
lostCancelPath.rest
tupleContains.v
types
runFunc
runFunc.cancelvars
lostCancelPath.v
lostCancelPath.RangeStmt_7808.b
runFunc.RangeStmt_5078.BlockStmt.BlockStmt.lineno
hasImport
lostCancelPath.pass
isContextWithCancel.info
ctrlflow
runFunc.BlockStmt.id
isCall
Members
X