...

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

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

View as plain text