blob: 85e7ba7a8efee0d32f494424a403e7dc605f78e0 [file] [log] [blame]
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package lostcancel defines an Analyzer that checks for failure to
// call a context cancelation function.
package lostcancel
import (
"fmt"
"go/ast"
"go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/ctrlflow"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/cfg"
)
const Doc = `check cancel func returned by context.WithCancel is called
The cancelation function returned by context.WithCancel, WithTimeout,
and WithDeadline must be called or the new context will remain live
until its parent context is cancelled.
(The background context is never cancelled.)`
var Analyzer = &analysis.Analyzer{
Name: "lostcancel",
Doc: Doc,
Run: run,
Requires: []*analysis.Analyzer{
inspect.Analyzer,
ctrlflow.Analyzer,
},
}
const debug = false
var contextPackage = "context"
// checkLostCancel reports a failure to the call the cancel function
// returned by context.WithCancel, either because the variable was
// assigned to the blank identifier, or because there exists a
// control-flow path from the call to a return statement and that path
// does not "use" the cancel function. Any reference to the variable
// counts as a use, even within a nested function literal.
// If the variable's scope is larger than the function
// containing the assignment, we assume that other uses exist.
//
// checkLostCancel analyzes a single named or literal function.
func run(pass *analysis.Pass) (interface{}, error) {
// Fast path: bypass check if file doesn't use context.WithCancel.
if !hasImport(pass.Pkg, contextPackage) {
return nil, nil
}
// Call runFunc for each Func{Decl,Lit}.
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeTypes := []ast.Node{
(*ast.FuncLit)(nil),
(*ast.FuncDecl)(nil),
}
inspect.Preorder(nodeTypes, func(n ast.Node) {
runFunc(pass, n)
})
return nil, nil
}
func runFunc(pass *analysis.Pass, node ast.Node) {
// Find scope of function node
var funcScope *types.Scope
switch v := node.(type) {
case *ast.FuncLit:
funcScope = pass.TypesInfo.Scopes[v.Type]
case *ast.FuncDecl:
funcScope = pass.TypesInfo.Scopes[v.Type]
}
// Maps each cancel variable to its defining ValueSpec/AssignStmt.
cancelvars := make(map[*types.Var]ast.Node)
// TODO(adonovan): opt: refactor to make a single pass
// over the AST using inspect.WithStack and node types
// {FuncDecl,FuncLit,CallExpr,SelectorExpr}.
// Find the set of cancel vars to analyze.
stack := make([]ast.Node, 0, 32)
ast.Inspect(node, func(n ast.Node) bool {
switch n.(type) {
case *ast.FuncLit:
if len(stack) > 0 {
return false // don't stray into nested functions
}
case nil:
stack = stack[:len(stack)-1] // pop
return true
}
stack = append(stack, n) // push
// Look for [{AssignStmt,ValueSpec} CallExpr SelectorExpr]:
//
// ctx, cancel := context.WithCancel(...)
// ctx, cancel = context.WithCancel(...)
// var ctx, cancel = context.WithCancel(...)
//
if !isContextWithCancel(pass.TypesInfo, n) || !isCall(stack[len(stack)-2]) {
return true
}
var id *ast.Ident // id of cancel var
stmt := stack[len(stack)-3]
switch stmt := stmt.(type) {
case *ast.ValueSpec:
if len(stmt.Names) > 1 {
id = stmt.Names[1]
}
case *ast.AssignStmt:
if len(stmt.Lhs) > 1 {
id, _ = stmt.Lhs[1].(*ast.Ident)
}
}
if id != nil {
if id.Name == "_" {
pass.ReportRangef(id,
"the cancel function returned by context.%s should be called, not discarded, to avoid a context leak",
n.(*ast.SelectorExpr).Sel.Name)
} else if v, ok := pass.TypesInfo.Uses[id].(*types.Var); ok {
// If the cancel variable is defined outside function scope,
// do not analyze it.
if funcScope.Contains(v.Pos()) {
cancelvars[v] = stmt
}
} else if v, ok := pass.TypesInfo.Defs[id].(*types.Var); ok {
cancelvars[v] = stmt
}
}
return true
})
if len(cancelvars) == 0 {
return // no need to inspect CFG
}
// Obtain the CFG.
cfgs := pass.ResultOf[ctrlflow.Analyzer].(*ctrlflow.CFGs)
var g *cfg.CFG
var sig *types.Signature
switch node := node.(type) {
case *ast.FuncDecl:
sig, _ = pass.TypesInfo.Defs[node.Name].Type().(*types.Signature)
if node.Name.Name == "main" && sig.Recv() == nil && pass.Pkg.Name() == "main" {
// Returning from main.main terminates the process,
// so there's no need to cancel contexts.
return
}
g = cfgs.FuncDecl(node)
case *ast.FuncLit:
sig, _ = pass.TypesInfo.Types[node.Type].Type.(*types.Signature)
g = cfgs.FuncLit(node)
}
if sig == nil {
return // missing type information
}
// Print CFG.
if debug {
fmt.Println(g.Format(pass.Fset))
}
// Examine the CFG for each variable in turn.
// (It would be more efficient to analyze all cancelvars in a
// single pass over the AST, but seldom is there more than one.)
for v, stmt := range cancelvars {
if ret := lostCancelPath(pass, g, v, stmt, sig); ret != nil {
lineno := pass.Fset.Position(stmt.Pos()).Line
pass.ReportRangef(stmt, "the %s function is not used on all paths (possible context leak)", v.Name())
pass.ReportRangef(ret, "this return statement may be reached without using the %s var defined on line %d", v.Name(), lineno)
}
}
}
func isCall(n ast.Node) bool { _, ok := n.(*ast.CallExpr); return ok }
func hasImport(pkg *types.Package, path string) bool {
for _, imp := range pkg.Imports() {
if imp.Path() == path {
return true
}
}
return false
}
// isContextWithCancel reports whether n is one of the qualified identifiers
// context.With{Cancel,Timeout,Deadline}.
func isContextWithCancel(info *types.Info, n ast.Node) bool {
sel, ok := n.(*ast.SelectorExpr)
if !ok {
return false
}
switch sel.Sel.Name {
case "WithCancel", "WithTimeout", "WithDeadline":
default:
return false
}
if x, ok := sel.X.(*ast.Ident); ok {
if pkgname, ok := info.Uses[x].(*types.PkgName); ok {
return pkgname.Imported().Path() == contextPackage
}
// Import failed, so we can't check package path.
// Just check the local package name (heuristic).
return x.Name == "context"
}
return false
}
// lostCancelPath finds a path through the CFG, from stmt (which defines
// the 'cancel' variable v) to a return statement, that doesn't "use" v.
// If it finds one, it returns the return statement (which may be synthetic).
// sig is the function's type, if known.
func lostCancelPath(pass *analysis.Pass, g *cfg.CFG, v *types.Var, stmt ast.Node, sig *types.Signature) *ast.ReturnStmt {
vIsNamedResult := sig != nil && tupleContains(sig.Results(), v)
// uses reports whether stmts contain a "use" of variable v.
uses := func(pass *analysis.Pass, v *types.Var, stmts []ast.Node) bool {
found := false
for _, stmt := range stmts {
ast.Inspect(stmt, func(n ast.Node) bool {
switch n := n.(type) {
case *ast.Ident:
if pass.TypesInfo.Uses[n] == v {
found = true
}
case *ast.ReturnStmt:
// A naked return statement counts as a use
// of the named result variables.
if n.Results == nil && vIsNamedResult {
found = true
}
}
return !found
})
}
return found
}
// blockUses computes "uses" for each block, caching the result.
memo := make(map[*cfg.Block]bool)
blockUses := func(pass *analysis.Pass, v *types.Var, b *cfg.Block) bool {
res, ok := memo[b]
if !ok {
res = uses(pass, v, b.Nodes)
memo[b] = res
}
return res
}
// Find the var's defining block in the CFG,
// plus the rest of the statements of that block.
var defblock *cfg.Block
var rest []ast.Node
outer:
for _, b := range g.Blocks {
for i, n := range b.Nodes {
if n == stmt {
defblock = b
rest = b.Nodes[i+1:]
break outer
}
}
}
if defblock == nil {
panic("internal error: can't find defining block for cancel var")
}
// Is v "used" in the remainder of its defining block?
if uses(pass, v, rest) {
return nil
}
// Does the defining block return without using v?
if ret := defblock.Return(); ret != nil {
return ret
}
// Search the CFG depth-first for a path, from defblock to a
// return block, in which v is never "used".
seen := make(map[*cfg.Block]bool)
var search func(blocks []*cfg.Block) *ast.ReturnStmt
search = func(blocks []*cfg.Block) *ast.ReturnStmt {
for _, b := range blocks {
if seen[b] {
continue
}
seen[b] = true
// Prune the search if the block uses v.
if blockUses(pass, v, b) {
continue
}
// Found path to return statement?
if ret := b.Return(); ret != nil {
if debug {
fmt.Printf("found path to return in block %s\n", b)
}
return ret // found
}
// Recur
if ret := search(b.Succs); ret != nil {
if debug {
fmt.Printf(" from block %s\n", b)
}
return ret
}
}
return nil
}
return search(defblock.Succs)
}
func tupleContains(tuple *types.Tuple, v *types.Var) bool {
for i := 0; i < tuple.Len(); i++ {
if tuple.At(i) == v {
return true
}
}
return false
}