...

Source file src/golang.org/x/tools/go/analysis/passes/lostcancel/lostcancel.go

Documentation: golang.org/x/tools/go/analysis/passes/lostcancel

     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.
     7  package lostcancel
     8  
     9  import (
    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  
    21  const Doc = `check cancel func returned by context.WithCancel is called
    22  
    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.)`
    27  
    28  var 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  
    38  const debug = false
    39  
    40  var 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.
    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) {
    55  		return nil, nil
    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(pass, n)
    66  	})
    67  	return nil, nil
    68  }
    69  
    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) {
    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.Node, 0, 32)
    89  	ast.Inspect(node, func(n ast.Node) bool {
    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(stack, n) // 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.TypesInfo, n) || !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 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()) {
   131  					cancelvars[v] = stmt
   132  				}
   133  			} else if v, ok := 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 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)
   179  		}
   180  	}
   181  }
   182  
   183  func isCall(n ast.Node) bool { _, ok := n.(*ast.CallExpr); return ok }
   184  
   185  func hasImport(pkg *types.Package, path string) bool {
   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}.
   196  func isContextWithCancel(info *types.Info, n ast.Node) bool {
   197  	sel, ok := 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 x, ok := sel.X.(*ast.Ident); ok {
   207  		if pkgname, ok := 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.
   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)
   223  
   224  	// uses reports whether stmts contain a "use" of variable v.
   225  	uses := func(pass *analysis.Pass, v *types.Var, stmts []ast.Node) bool {
   226  		found := false
   227  		for _, stmt := range stmts {
   228  			ast.Inspect(stmt, func(n ast.Node) bool {
   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.Pass, v *types.Var, b *cfg.Block) bool {
   250  		res, ok := memo[b]
   251  		if !ok {
   252  			res = uses(pass, v, b.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
   262  outer:
   263  	for _, b := range g.Blocks {
   264  		for i, n := 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(pass, v, rest) {
   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(pass, v, b) {
   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  
   323  func tupleContains(tuple *types.Tuple, v *types.Var) bool {
   324  	for i := 0; i < tuple.Len(); i++ {
   325  		if tuple.At(i) == v {
   326  			return true
   327  		}
   328  	}
   329  	return false
   330  }
   331  

View as plain text