go/analysis/passes/lostcancel: split out from vet

Change-Id: Id19410d1e81ae29813404cd2e9781f57813110ef
Reviewed-on: https://go-review.googlesource.com/c/139677
Reviewed-by: Michael Matloob <matloob@golang.org>
Run-TryBot: Michael Matloob <matloob@golang.org>
diff --git a/go/analysis/passes/lostcancel/cmd/lostcancel/main.go b/go/analysis/passes/lostcancel/cmd/lostcancel/main.go
new file mode 100644
index 0000000..593567b
--- /dev/null
+++ b/go/analysis/passes/lostcancel/cmd/lostcancel/main.go
@@ -0,0 +1,10 @@
+// The nilness command applies the golang.org/x/tools/go/analysis/passes/lostcancel
+// analysis to the specified packages of Go source code.
+package main
+
+import (
+	"golang.org/x/tools/go/analysis/passes/lostcancel"
+	"golang.org/x/tools/go/analysis/singlechecker"
+)
+
+func main() { singlechecker.Main(lostcancel.Analyzer) }
diff --git a/go/analysis/passes/vet/lostcancel.go b/go/analysis/passes/lostcancel/lostcancel.go
similarity index 60%
rename from go/analysis/passes/vet/lostcancel.go
rename to go/analysis/passes/lostcancel/lostcancel.go
index f82db47..d022b34 100644
--- a/go/analysis/passes/vet/lostcancel.go
+++ b/go/analysis/passes/lostcancel/lostcancel.go
@@ -1,27 +1,39 @@
-// +build ignore
-
 // 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 main
+package lostcancel
 
 import (
-	"cmd/vet/internal/cfg"
 	"fmt"
 	"go/ast"
 	"go/types"
-	"strconv"
+
+	"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"
 )
 
-func init() {
-	register("lostcancel",
-		"check for failure to call cancelation function returned by context.WithCancel",
-		checkLostCancel,
-		funcDecl, funcLit)
+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 debugLostCancel = false
+const debug = false
 
 var contextPackage = "context"
 
@@ -33,15 +45,32 @@
 // counts as a use, even within a nested function literal.
 //
 // checkLostCancel analyzes a single named or literal function.
-func checkLostCancel(f *File, node ast.Node) {
+func run(pass *analysis.Pass) (interface{}, error) {
 	// Fast path: bypass check if file doesn't use context.WithCancel.
-	if !hasImport(f.file, contextPackage) {
-		return
+	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) {
 	// 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 {
@@ -62,7 +91,7 @@
 		//   ctx, cancel     = context.WithCancel(...)
 		//   var ctx, cancel = context.WithCancel(...)
 		//
-		if isContextWithCancel(f, n) && isCall(stack[len(stack)-2]) {
+		if isContextWithCancel(pass.TypesInfo, n) && isCall(stack[len(stack)-2]) {
 			var id *ast.Ident // id of cancel var
 			stmt := stack[len(stack)-3]
 			switch stmt := stmt.(type) {
@@ -77,11 +106,12 @@
 			}
 			if id != nil {
 				if id.Name == "_" {
-					f.Badf(id.Pos(), "the cancel function returned by context.%s should be called, not discarded, to avoid a context leak",
+					pass.Reportf(id.Pos(),
+						"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 := f.pkg.uses[id].(*types.Var); ok {
+				} else if v, ok := pass.TypesInfo.Uses[id].(*types.Var); ok {
 					cancelvars[v] = stmt
-				} else if v, ok := f.pkg.defs[id].(*types.Var); ok {
+				} else if v, ok := pass.TypesInfo.Defs[id].(*types.Var); ok {
 					cancelvars[v] = stmt
 				}
 			}
@@ -91,55 +121,47 @@
 	})
 
 	if len(cancelvars) == 0 {
-		return // no need to build CFG
+		return // no need to inspect CFG
 	}
 
-	// Tell the CFG builder which functions never return.
-	info := &types.Info{Uses: f.pkg.uses, Selections: f.pkg.selectors}
-	mayReturn := func(call *ast.CallExpr) bool {
-		name := callName(info, call)
-		return !noReturnFuncs[name]
-	}
-
-	// Build the 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:
-		obj := f.pkg.defs[node.Name]
-		if obj == nil {
-			return // type error (e.g. duplicate function declaration)
-		}
-		sig, _ = obj.Type().(*types.Signature)
-		g = cfg.New(node.Body, mayReturn)
+		g = cfgs.FuncDecl(node)
+		sig, _ = pass.TypesInfo.Defs[node.Name].Type().(*types.Signature)
 	case *ast.FuncLit:
-		sig, _ = f.pkg.types[node.Type].Type.(*types.Signature)
-		g = cfg.New(node.Body, mayReturn)
+		g = cfgs.FuncLit(node)
+		sig, _ = pass.TypesInfo.Types[node.Type].Type.(*types.Signature)
+	}
+	if sig == nil {
+		return // missing type information
 	}
 
 	// Print CFG.
-	if debugLostCancel {
-		fmt.Println(g.Format(f.fset))
+	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(f, g, v, stmt, sig); ret != nil {
-			lineno := f.fset.Position(stmt.Pos()).Line
-			f.Badf(stmt.Pos(), "the %s function is not used on all paths (possible context leak)", v.Name())
-			f.Badf(ret.Pos(), "this return statement may be reached without using the %s var defined on line %d", v.Name(), lineno)
+		if ret := lostCancelPath(pass, g, v, stmt, sig); ret != nil {
+			lineno := pass.Fset.Position(stmt.Pos()).Line
+			pass.Reportf(stmt.Pos(), "the %s function is not used on all paths (possible context leak)", v.Name())
+			pass.Reportf(ret.Pos(), "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(f *ast.File, path string) bool {
-	for _, imp := range f.Imports {
-		v, _ := strconv.Unquote(imp.Path.Value)
-		if v == path {
+func hasImport(pkg *types.Package, path string) bool {
+	for _, imp := range pkg.Imports() {
+		if imp.Path() == path {
 			return true
 		}
 	}
@@ -148,12 +170,12 @@
 
 // isContextWithCancel reports whether n is one of the qualified identifiers
 // context.With{Cancel,Timeout,Deadline}.
-func isContextWithCancel(f *File, n ast.Node) bool {
+func isContextWithCancel(info *types.Info, n ast.Node) bool {
 	if sel, ok := n.(*ast.SelectorExpr); ok {
 		switch sel.Sel.Name {
 		case "WithCancel", "WithTimeout", "WithDeadline":
 			if x, ok := sel.X.(*ast.Ident); ok {
-				if pkgname, ok := f.pkg.uses[x].(*types.PkgName); ok {
+				if pkgname, ok := info.Uses[x].(*types.PkgName); ok {
 					return pkgname.Imported().Path() == contextPackage
 				}
 				// Import failed, so we can't check package path.
@@ -169,17 +191,17 @@
 // 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(f *File, g *cfg.CFG, v *types.Var, stmt ast.Node, sig *types.Signature) *ast.ReturnStmt {
+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(f *File, v *types.Var, stmts []ast.Node) bool {
+	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 f.pkg.uses[n] == v {
+					if pass.TypesInfo.Uses[n] == v {
 						found = true
 					}
 				case *ast.ReturnStmt:
@@ -197,10 +219,10 @@
 
 	// blockUses computes "uses" for each block, caching the result.
 	memo := make(map[*cfg.Block]bool)
-	blockUses := func(f *File, v *types.Var, b *cfg.Block) bool {
+	blockUses := func(pass *analysis.Pass, v *types.Var, b *cfg.Block) bool {
 		res, ok := memo[b]
 		if !ok {
-			res = uses(f, v, b.Nodes)
+			res = uses(pass, v, b.Nodes)
 			memo[b] = res
 		}
 		return res
@@ -225,7 +247,7 @@
 	}
 
 	// Is v "used" in the remainder of its defining block?
-	if uses(f, v, rest) {
+	if uses(pass, v, rest) {
 		return nil
 	}
 
@@ -244,13 +266,13 @@
 				seen[b] = true
 
 				// Prune the search if the block uses v.
-				if blockUses(f, v, b) {
+				if blockUses(pass, v, b) {
 					continue
 				}
 
 				// Found path to return statement?
 				if ret := b.Return(); ret != nil {
-					if debugLostCancel {
+					if debug {
 						fmt.Printf("found path to return in block %s\n", b)
 					}
 					return ret // found
@@ -258,7 +280,7 @@
 
 				// Recur
 				if ret := search(b.Succs); ret != nil {
-					if debugLostCancel {
+					if debug {
 						fmt.Printf(" from block %s\n", b)
 					}
 					return ret
@@ -278,47 +300,3 @@
 	}
 	return false
 }
-
-var noReturnFuncs = map[string]bool{
-	"(*testing.common).FailNow": true,
-	"(*testing.common).Fatal":   true,
-	"(*testing.common).Fatalf":  true,
-	"(*testing.common).Skip":    true,
-	"(*testing.common).SkipNow": true,
-	"(*testing.common).Skipf":   true,
-	"log.Fatal":                 true,
-	"log.Fatalf":                true,
-	"log.Fatalln":               true,
-	"os.Exit":                   true,
-	"panic":                     true,
-	"runtime.Goexit":            true,
-}
-
-// callName returns the canonical name of the builtin, method, or
-// function called by call, if known.
-func callName(info *types.Info, call *ast.CallExpr) string {
-	switch fun := call.Fun.(type) {
-	case *ast.Ident:
-		// builtin, e.g. "panic"
-		if obj, ok := info.Uses[fun].(*types.Builtin); ok {
-			return obj.Name()
-		}
-	case *ast.SelectorExpr:
-		if sel, ok := info.Selections[fun]; ok && sel.Kind() == types.MethodVal {
-			// method call, e.g. "(*testing.common).Fatal"
-			meth := sel.Obj()
-			return fmt.Sprintf("(%s).%s",
-				meth.Type().(*types.Signature).Recv().Type(),
-				meth.Name())
-		}
-		if obj, ok := info.Uses[fun.Sel]; ok {
-			// qualified identifier, e.g. "os.Exit"
-			return fmt.Sprintf("%s.%s",
-				obj.Pkg().Path(),
-				obj.Name())
-		}
-	}
-
-	// function with no name, or defined in missing imported package
-	return ""
-}
diff --git a/go/analysis/passes/lostcancel/lostcancel_test.go b/go/analysis/passes/lostcancel/lostcancel_test.go
new file mode 100644
index 0000000..759634b
--- /dev/null
+++ b/go/analysis/passes/lostcancel/lostcancel_test.go
@@ -0,0 +1,13 @@
+package lostcancel_test
+
+import (
+	"testing"
+
+	"golang.org/x/tools/go/analysis/analysistest"
+	"golang.org/x/tools/go/analysis/passes/lostcancel"
+)
+
+func Test(t *testing.T) {
+	testdata := analysistest.TestData()
+	analysistest.Run(t, testdata, lostcancel.Analyzer, "a") // load testdata/src/a/a.go
+}
diff --git a/go/analysis/passes/lostcancel/testdata/src/a/a.go b/go/analysis/passes/lostcancel/testdata/src/a/a.go
new file mode 100644
index 0000000..f9277ba
--- /dev/null
+++ b/go/analysis/passes/lostcancel/testdata/src/a/a.go
@@ -0,0 +1,173 @@
+// 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 a
+
+import (
+	"context"
+	"log"
+	"os"
+	"testing"
+	"time"
+)
+
+var bg = context.Background()
+
+// Check the three functions and assignment forms (var, :=, =) we look for.
+// (Do these early: line numbers are fragile.)
+func _() {
+	var _, cancel = context.WithCancel(bg) // want `the cancel function is not used on all paths \(possible context leak\)`
+	if false {
+		_ = cancel
+	}
+} // want "this return statement may be reached without using the cancel var defined on line 20"
+
+func _() {
+	_, cancel2 := context.WithDeadline(bg, time.Time{}) // want "the cancel2 function is not used..."
+	if false {
+		_ = cancel2
+	}
+} // want "may be reached without using the cancel2 var defined on line 27"
+
+func _() {
+	var cancel3 func()
+	_, cancel3 = context.WithTimeout(bg, 0) // want "function is not used..."
+	if false {
+		_ = cancel3
+	}
+} // want "this return statement may be reached without using the cancel3 var defined on line 35"
+
+func _() {
+	ctx, _ := context.WithCancel(bg)               // want "the cancel function returned by context.WithCancel should be called, not discarded, to avoid a context leak"
+	ctx, _ = context.WithTimeout(bg, 0)            // want "the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak"
+	ctx, _ = context.WithDeadline(bg, time.Time{}) // want "the cancel function returned by context.WithDeadline should be called, not discarded, to avoid a context leak"
+	_ = ctx
+}
+
+func _() {
+	_, cancel := context.WithCancel(bg)
+	defer cancel() // ok
+}
+
+func _() {
+	_, cancel := context.WithCancel(bg) // want "not used on all paths"
+	if condition {
+		cancel()
+	}
+	return // want "this return statement may be reached without using the cancel var"
+}
+
+func _() {
+	_, cancel := context.WithCancel(bg)
+	if condition {
+		cancel()
+	} else {
+		// ok: infinite loop
+		for {
+			print(0)
+		}
+	}
+}
+
+func _() {
+	_, cancel := context.WithCancel(bg) // want "not used on all paths"
+	if condition {
+		cancel()
+	} else {
+		for i := 0; i < 10; i++ {
+			print(0)
+		}
+	}
+} // want "this return statement may be reached without using the cancel var"
+
+func _() {
+	_, cancel := context.WithCancel(bg)
+	// ok: used on all paths
+	switch someInt {
+	case 0:
+		new(testing.T).FailNow()
+	case 1:
+		log.Fatal()
+	case 2:
+		cancel()
+	case 3:
+		print("hi")
+		fallthrough
+	default:
+		os.Exit(1)
+	}
+}
+
+func _() {
+	_, cancel := context.WithCancel(bg) // want "not used on all paths"
+	switch someInt {
+	case 0:
+		new(testing.T).FailNow()
+	case 1:
+		log.Fatal()
+	case 2:
+		cancel()
+	case 3:
+		print("hi") // falls through to implicit return
+	default:
+		os.Exit(1)
+	}
+} // want "this return statement may be reached without using the cancel var"
+
+func _(ch chan int) {
+	_, cancel := context.WithCancel(bg) // want "not used on all paths"
+	select {
+	case <-ch:
+		new(testing.T).FailNow()
+	case ch <- 2:
+		print("hi") // falls through to implicit return
+	case ch <- 1:
+		cancel()
+	default:
+		os.Exit(1)
+	}
+} // want "this return statement may be reached without using the cancel var"
+
+func _(ch chan int) {
+	_, cancel := context.WithCancel(bg)
+	// A blocking select must execute one of its cases.
+	select {
+	case <-ch:
+		panic(0)
+	}
+	if false {
+		_ = cancel
+	}
+}
+
+func _() {
+	go func() {
+		ctx, cancel := context.WithCancel(bg) // want "not used on all paths"
+		if false {
+			_ = cancel
+		}
+		print(ctx)
+	}() // want "may be reached without using the cancel var"
+}
+
+var condition bool
+var someInt int
+
+// Regression test for Go issue 16143.
+func _() {
+	var x struct{ f func() }
+	x.f()
+}
+
+// Regression test for Go issue 16230.
+func _() (ctx context.Context, cancel func()) {
+	ctx, cancel = context.WithCancel(bg)
+	return // a naked return counts as a load of the named result values
+}
+
+// Same as above, but for literal function.
+var _ = func() (ctx context.Context, cancel func()) {
+	ctx, cancel = context.WithCancel(bg)
+	return
+}
diff --git a/go/analysis/passes/vet/testdata/lostcancel.go b/go/analysis/passes/vet/testdata/lostcancel.go
deleted file mode 100644
index b7549c0..0000000
--- a/go/analysis/passes/vet/testdata/lostcancel.go
+++ /dev/null
@@ -1,155 +0,0 @@
-// 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 testdata
-
-import (
-	"context"
-	"log"
-	"os"
-	"testing"
-)
-
-// Check the three functions and assignment forms (var, :=, =) we look for.
-// (Do these early: line numbers are fragile.)
-func _() {
-	var ctx, cancel = context.WithCancel() // ERROR "the cancel function is not used on all paths \(possible context leak\)"
-} // ERROR "this return statement may be reached without using the cancel var defined on line 17"
-
-func _() {
-	ctx, cancel2 := context.WithDeadline() // ERROR "the cancel2 function is not used..."
-} // ERROR "may be reached without using the cancel2 var defined on line 21"
-
-func _() {
-	var ctx context.Context
-	var cancel3 func()
-	ctx, cancel3 = context.WithTimeout() // ERROR "function is not used..."
-} // ERROR "this return statement may be reached without using the cancel3 var defined on line 27"
-
-func _() {
-	ctx, _ := context.WithCancel()  // ERROR "the cancel function returned by context.WithCancel should be called, not discarded, to avoid a context leak"
-	ctx, _ = context.WithTimeout()  // ERROR "the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak"
-	ctx, _ = context.WithDeadline() // ERROR "the cancel function returned by context.WithDeadline should be called, not discarded, to avoid a context leak"
-}
-
-func _() {
-	ctx, cancel := context.WithCancel()
-	defer cancel() // ok
-}
-
-func _() {
-	ctx, cancel := context.WithCancel() // ERROR "not used on all paths"
-	if condition {
-		cancel()
-	}
-	return // ERROR "this return statement may be reached without using the cancel var"
-}
-
-func _() {
-	ctx, cancel := context.WithCancel()
-	if condition {
-		cancel()
-	} else {
-		// ok: infinite loop
-		for {
-			print(0)
-		}
-	}
-}
-
-func _() {
-	ctx, cancel := context.WithCancel() // ERROR "not used on all paths"
-	if condition {
-		cancel()
-	} else {
-		for i := 0; i < 10; i++ {
-			print(0)
-		}
-	}
-} // ERROR "this return statement may be reached without using the cancel var"
-
-func _() {
-	ctx, cancel := context.WithCancel()
-	// ok: used on all paths
-	switch someInt {
-	case 0:
-		new(testing.T).FailNow()
-	case 1:
-		log.Fatal()
-	case 2:
-		cancel()
-	case 3:
-		print("hi")
-		fallthrough
-	default:
-		os.Exit(1)
-	}
-}
-
-func _() {
-	ctx, cancel := context.WithCancel() // ERROR "not used on all paths"
-	switch someInt {
-	case 0:
-		new(testing.T).FailNow()
-	case 1:
-		log.Fatal()
-	case 2:
-		cancel()
-	case 3:
-		print("hi") // falls through to implicit return
-	default:
-		os.Exit(1)
-	}
-} // ERROR "this return statement may be reached without using the cancel var"
-
-func _(ch chan int) int {
-	ctx, cancel := context.WithCancel() // ERROR "not used on all paths"
-	select {
-	case <-ch:
-		new(testing.T).FailNow()
-	case y <- ch:
-		print("hi") // falls through to implicit return
-	case ch <- 1:
-		cancel()
-	default:
-		os.Exit(1)
-	}
-} // ERROR "this return statement may be reached without using the cancel var"
-
-func _(ch chan int) int {
-	ctx, cancel := context.WithCancel()
-	// A blocking select must execute one of its cases.
-	select {
-	case <-ch:
-		panic()
-	}
-}
-
-func _() {
-	go func() {
-		ctx, cancel := context.WithCancel() // ERROR "not used on all paths"
-		print(ctx)
-	}() // ERROR "may be reached without using the cancel var"
-}
-
-var condition bool
-var someInt int
-
-// Regression test for Go issue 16143.
-func _() {
-	var x struct{ f func() }
-	x.f()
-}
-
-// Regression test for Go issue 16230.
-func _() (ctx context.Context, cancel func()) {
-	ctx, cancel = context.WithCancel()
-	return // a naked return counts as a load of the named result values
-}
-
-// Same as above, but for literal function.
-var _ = func() (ctx context.Context, cancel func()) {
-	ctx, cancel = context.WithCancel()
-	return
-}