...

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  
   176  			pos, end := ret.Pos(), ret.End()
   177  			// golang/go#64547: cfg.Block.Return may return a synthetic
   178  			// ReturnStmt that overflows the file.
   179  			if pass.Fset.File(pos) != pass.Fset.File(end) {
   180  				end = pos
   181  			}
   182  			pass.Report(analysis.Diagnostic{
   183  				Pos:     pos,
   184  				End:     end,
   185  				Message: fmt.Sprintf("this return statement may be reached without using the %s var defined on line %d", v.Name(), lineno),
   186  			})
   187  		}
   188  	}
   189  }
   190  
   191  func isCall(n ast.Node) bool { _, ok := n.(*ast.CallExpr); return ok }
   192  
   193  // isContextWithCancel reports whether n is one of the qualified identifiers
   194  // context.With{Cancel,Timeout,Deadline}.
   195  func isContextWithCancel(info *types.Info, n ast.Node) bool {
   196  	sel, ok := n.(*ast.SelectorExpr)
   197  	if !ok {
   198  		return false
   199  	}
   200  	switch sel.Sel.Name {
   201  	case "WithCancel", "WithTimeout", "WithDeadline":
   202  	default:
   203  		return false
   204  	}
   205  	if x, ok := sel.X.(*ast.Ident); ok {
   206  		if pkgname, ok := info.Uses[x].(*types.PkgName); ok {
   207  			return pkgname.Imported().Path() == contextPackage
   208  		}
   209  		// Import failed, so we can't check package path.
   210  		// Just check the local package name (heuristic).
   211  		return x.Name == "context"
   212  	}
   213  	return false
   214  }
   215  
   216  // lostCancelPath finds a path through the CFG, from stmt (which defines
   217  // the 'cancel' variable v) to a return statement, that doesn't "use" v.
   218  // If it finds one, it returns the return statement (which may be synthetic).
   219  // sig is the function's type, if known.
   220  func lostCancelPath(pass *analysis.Pass, g *cfg.CFG, v *types.Var, stmt ast.Node, sig *types.Signature) *ast.ReturnStmt {
   221  	vIsNamedResult := sig != nil && tupleContains(sig.Results(), v)
   222  
   223  	// uses reports whether stmts contain a "use" of variable v.
   224  	uses := func(pass *analysis.Pass, v *types.Var, stmts []ast.Node) bool {
   225  		found := false
   226  		for _, stmt := range stmts {
   227  			ast.Inspect(stmt, func(n ast.Node) bool {
   228  				switch n := n.(type) {
   229  				case *ast.Ident:
   230  					if pass.TypesInfo.Uses[n] == v {
   231  						found = true
   232  					}
   233  				case *ast.ReturnStmt:
   234  					// A naked return statement counts as a use
   235  					// of the named result variables.
   236  					if n.Results == nil && vIsNamedResult {
   237  						found = true
   238  					}
   239  				}
   240  				return !found
   241  			})
   242  		}
   243  		return found
   244  	}
   245  
   246  	// blockUses computes "uses" for each block, caching the result.
   247  	memo := make(map[*cfg.Block]bool)
   248  	blockUses := func(pass *analysis.Pass, v *types.Var, b *cfg.Block) bool {
   249  		res, ok := memo[b]
   250  		if !ok {
   251  			res = uses(pass, v, b.Nodes)
   252  			memo[b] = res
   253  		}
   254  		return res
   255  	}
   256  
   257  	// Find the var's defining block in the CFG,
   258  	// plus the rest of the statements of that block.
   259  	var defblock *cfg.Block
   260  	var rest []ast.Node
   261  outer:
   262  	for _, b := range g.Blocks {
   263  		for i, n := range b.Nodes {
   264  			if n == stmt {
   265  				defblock = b
   266  				rest = b.Nodes[i+1:]
   267  				break outer
   268  			}
   269  		}
   270  	}
   271  	if defblock == nil {
   272  		panic("internal error: can't find defining block for cancel var")
   273  	}
   274  
   275  	// Is v "used" in the remainder of its defining block?
   276  	if uses(pass, v, rest) {
   277  		return nil
   278  	}
   279  
   280  	// Does the defining block return without using v?
   281  	if ret := defblock.Return(); ret != nil {
   282  		return ret
   283  	}
   284  
   285  	// Search the CFG depth-first for a path, from defblock to a
   286  	// return block, in which v is never "used".
   287  	seen := make(map[*cfg.Block]bool)
   288  	var search func(blocks []*cfg.Block) *ast.ReturnStmt
   289  	search = func(blocks []*cfg.Block) *ast.ReturnStmt {
   290  		for _, b := range blocks {
   291  			if seen[b] {
   292  				continue
   293  			}
   294  			seen[b] = true
   295  
   296  			// Prune the search if the block uses v.
   297  			if blockUses(pass, v, b) {
   298  				continue
   299  			}
   300  
   301  			// Found path to return statement?
   302  			if ret := b.Return(); ret != nil {
   303  				if debug {
   304  					fmt.Printf("found path to return in block %s\n", b)
   305  				}
   306  				return ret // found
   307  			}
   308  
   309  			// Recur
   310  			if ret := search(b.Succs); ret != nil {
   311  				if debug {
   312  					fmt.Printf(" from block %s\n", b)
   313  				}
   314  				return ret
   315  			}
   316  		}
   317  		return nil
   318  	}
   319  	return search(defblock.Succs)
   320  }
   321  
   322  func tupleContains(tuple *types.Tuple, v *types.Var) bool {
   323  	for i := 0; i < tuple.Len(); i++ {
   324  		if tuple.At(i) == v {
   325  			return true
   326  		}
   327  	}
   328  	return false
   329  }
   330  

View as plain text