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.
5 // Package lostcancel defines an Analyzer that checks for failure to
6 // call a context cancellation function.
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"
21 const Doc = `check cancel func returned by context.WithCancel is called
23 The cancellation function returned by context.WithCancel, WithTimeout,
24 and WithDeadline must be called or the new context will remain live
25 until its parent context is cancelled.
26 (The background context is never cancelled.)`
28 var Analyzer = &analysis.Analyzer{
32 Requires: []*analysis.Analyzer{
40 var contextPackage = "context"
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.
51 // checkLostCancel analyzes a single named or literal function.
52 func run(pass *analysis.Pass) (interface{}, error) {
53 // Fast path: bypass check if file doesn't use context.WithCancel.
54 if !hasImport(pass.Pkg, contextPackage) {
58 // Call runFunc for each Func{Decl,Lit}.
59 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
60 nodeTypes := []ast.Node{
64 inspect.Preorder(nodeTypes, func(n ast.Node) {
70 func runFunc(pass *analysis.Pass, node ast.Node) {
71 // Find scope of function node
72 var funcScope *types.Scope
73 switch v := node.(type) {
75 funcScope = pass.TypesInfo.Scopes[v.Type]
77 funcScope = pass.TypesInfo.Scopes[v.Type]
80 // Maps each cancel variable to its defining ValueSpec/AssignStmt.
81 cancelvars := make(map[*types.Var]ast.Node)
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}.
87 // Find the set of cancel vars to analyze.
88 stack := make([]ast.Node, 0, 32)
89 ast.Inspect(node, func(n ast.Node) bool {
93 return false // don't stray into nested functions
96 stack = stack[:len(stack)-1] // pop
99 stack = append(stack, n) // push
101 // Look for [{AssignStmt,ValueSpec} CallExpr SelectorExpr]:
103 // ctx, cancel := context.WithCancel(...)
104 // ctx, cancel = context.WithCancel(...)
105 // var ctx, cancel = context.WithCancel(...)
107 if !isContextWithCancel(pass.TypesInfo, n) || !isCall(stack[len(stack)-2]) {
110 var id *ast.Ident // id of cancel var
111 stmt := stack[len(stack)-3]
112 switch stmt := stmt.(type) {
114 if len(stmt.Names) > 1 {
117 case *ast.AssignStmt:
118 if len(stmt.Lhs) > 1 {
119 id, _ = stmt.Lhs[1].(*ast.Ident)
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 v, ok := 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()) {
133 } else if v, ok := pass.TypesInfo.Defs[id].(*types.Var); ok {
140 if len(cancelvars) == 0 {
141 return // no need to inspect CFG
145 cfgs := pass.ResultOf[ctrlflow.Analyzer].(*ctrlflow.CFGs)
147 var sig *types.Signature
148 switch node := node.(type) {
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.
156 g = cfgs.FuncDecl(node)
159 sig, _ = pass.TypesInfo.Types[node.Type].Type.(*types.Signature)
160 g = cfgs.FuncLit(node)
163 return // missing type information
168 fmt.Println(g.Format(pass.Fset))
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 v, stmt := range cancelvars {
175 if ret := lostCancelPath(pass, g, v, stmt, sig); 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)
183 func isCall(n ast.Node) bool { _, ok := n.(*ast.CallExpr); return ok }
185 func hasImport(pkg *types.Package, path string) bool {
186 for _, imp := range pkg.Imports() {
187 if imp.Path() == path {
194 // isContextWithCancel reports whether n is one of the qualified identifiers
195 // context.With{Cancel,Timeout,Deadline}.
196 func isContextWithCancel(info *types.Info, n ast.Node) bool {
197 sel, ok := n.(*ast.SelectorExpr)
201 switch sel.Sel.Name {
202 case "WithCancel", "WithTimeout", "WithDeadline":
206 if x, ok := sel.X.(*ast.Ident); ok {
207 if pkgname, ok := info.Uses[x].(*types.PkgName); ok {
208 return pkgname.Imported().Path() == contextPackage
210 // Import failed, so we can't check package path.
211 // Just check the local package name (heuristic).
212 return x.Name == "context"
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.
221 func lostCancelPath(pass *analysis.Pass, g *cfg.CFG, v *types.Var, stmt ast.Node, sig *types.Signature) *ast.ReturnStmt {
222 vIsNamedResult := sig != nil && tupleContains(sig.Results(), v)
224 // uses reports whether stmts contain a "use" of variable v.
225 uses := func(pass *analysis.Pass, v *types.Var, stmts []ast.Node) bool {
227 for _, stmt := range stmts {
228 ast.Inspect(stmt, func(n ast.Node) bool {
229 switch n := n.(type) {
231 if pass.TypesInfo.Uses[n] == v {
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 {
247 // blockUses computes "uses" for each block, caching the result.
248 memo := make(map[*cfg.Block]bool)
249 blockUses := func(pass *analysis.Pass, v *types.Var, b *cfg.Block) bool {
252 res = uses(pass, v, b.Nodes)
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
263 for _, b := range g.Blocks {
264 for i, n := range b.Nodes {
273 panic("internal error: can't find defining block for cancel var")
276 // Is v "used" in the remainder of its defining block?
277 if uses(pass, v, rest) {
281 // Does the defining block return without using v?
282 if ret := defblock.Return(); ret != nil {
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 {
297 // Prune the search if the block uses v.
298 if blockUses(pass, v, b) {
302 // Found path to return statement?
303 if ret := b.Return(); ret != nil {
305 fmt.Printf("found path to return in block %s\n", b)
311 if ret := search(b.Succs); ret != nil {
313 fmt.Printf(" from block %s\n", b)
320 return search(defblock.Succs)
323 func tupleContains(tuple *types.Tuple, v *types.Var) bool {
324 for i := 0; i < tuple.Len(); i++ {
325 if tuple.At(i) == v {