// 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 govet

import (
	"fmt"
	"go/ast"
	"go/types"
	"strconv"

	"github.com/golangci/govet/lib/cfg"
)

func init() {
	register("lostcancel",
		"check for failure to call cancelation function returned by context.WithCancel",
		checkLostCancel,
		funcDecl, funcLit)
}

const debugLostCancel = 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.
//
// checkLostCancel analyzes a single named or literal function.
func checkLostCancel(f *File, node ast.Node) {
	// Fast path: bypass check if file doesn't use context.WithCancel.
	if !hasImport(f.file, contextPackage) {
		return
	}

	// Maps each cancel variable to its defining ValueSpec/AssignStmt.
	cancelvars := make(map[*types.Var]ast.Node)

	// 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(f, n) && isCall(stack[len(stack)-2]) {
			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 == "_" {
					f.Badf(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 {
					cancelvars[v] = stmt
				} else if v, ok := f.pkg.defs[id].(*types.Var); ok {
					cancelvars[v] = stmt
				}
			}
		}

		return true
	})

	if len(cancelvars) == 0 {
		return // no need to build 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.
	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)
	case *ast.FuncLit:
		sig, _ = f.pkg.types[node.Type].Type.(*types.Signature)
		g = cfg.New(node.Body, mayReturn)
	}

	// Print CFG.
	if debugLostCancel {
		fmt.Println(g.Format(f.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)
		}
	}
}

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 {
			return true
		}
	}
	return false
}

// isContextWithCancel reports whether n is one of the qualified identifiers
// context.With{Cancel,Timeout,Deadline}.
func isContextWithCancel(f *File, 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 {
					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(f *File, 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 {
		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 {
						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(f *File, v *types.Var, b *cfg.Block) bool {
		res, ok := memo[b]
		if !ok {
			res = uses(f, 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(f, 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] {
				seen[b] = true

				// Prune the search if the block uses v.
				if blockUses(f, v, b) {
					continue
				}

				// Found path to return statement?
				if ret := b.Return(); ret != nil {
					if debugLostCancel {
						fmt.Printf("found path to return in block %s\n", b)
					}
					return ret // found
				}

				// Recur
				if ret := search(b.Succs); ret != nil {
					if debugLostCancel {
						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
}

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 ""
}
