// Copyright 2023 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 inline

import (
	"fmt"
	"go/ast"
	"go/token"
	"go/types"
)

// escape implements a simple "address-taken" escape analysis. It
// calls f for each local variable that appears on the left side of an
// assignment (escapes=false) or has its address taken (escapes=true).
// The initialization of a variable by its declaration does not count
// as an assignment.
func escape(info *types.Info, root ast.Node, f func(v *types.Var, escapes bool)) {

	// lvalue is called for each address-taken expression or LHS of assignment.
	// Supported forms are: x, (x), x[i], x.f, *x, T{}.
	var lvalue func(e ast.Expr, escapes bool)
	lvalue = func(e ast.Expr, escapes bool) {
		switch e := e.(type) {
		case *ast.Ident:
			if v, ok := info.Uses[e].(*types.Var); ok {
				if !isPkgLevel(v) {
					f(v, escapes)
				}
			}
		case *ast.ParenExpr:
			lvalue(e.X, escapes)
		case *ast.IndexExpr:
			// TODO(adonovan): support generics without assuming e.X has a core type.
			// Consider:
			//
			// func Index[T interface{ [3]int | []int }](t T, i int) *int {
			//     return &t[i]
			// }
			//
			// We must traverse the normal terms and check
			// whether any of them is an array.
			if _, ok := info.TypeOf(e.X).Underlying().(*types.Array); ok {
				lvalue(e.X, escapes) // &a[i] on array
			}
		case *ast.SelectorExpr:
			if _, ok := info.TypeOf(e.X).Underlying().(*types.Struct); ok {
				lvalue(e.X, escapes) // &s.f on struct
			}
		case *ast.StarExpr:
			// *ptr indirects an existing pointer
		case *ast.CompositeLit:
			// &T{...} creates a new variable
		default:
			panic(fmt.Sprintf("&x on %T", e)) // unreachable in well-typed code
		}
	}

	// Search function body for operations &x, x.f(), x++, and x = y
	// where x is a parameter. Each of these treats x as an address.
	ast.Inspect(root, func(n ast.Node) bool {
		switch n := n.(type) {
		case *ast.UnaryExpr:
			if n.Op == token.AND {
				lvalue(n.X, true) // &x
			}

		case *ast.CallExpr:
			// implicit &x in method call x.f(),
			// where x has type T and method is (*T).f
			if sel, ok := n.Fun.(*ast.SelectorExpr); ok {
				if seln, ok := info.Selections[sel]; ok &&
					seln.Kind() == types.MethodVal &&
					isPointer(seln.Obj().Type().Underlying().(*types.Signature).Recv().Type()) {
					tArg, indirect := effectiveReceiver(seln)
					if !indirect && !isPointer(tArg) {
						lvalue(sel.X, true) // &x.f
					}
				}
			}

		case *ast.AssignStmt:
			for _, lhs := range n.Lhs {
				if id, ok := lhs.(*ast.Ident); ok &&
					info.Defs[id] != nil &&
					n.Tok == token.DEFINE {
					// declaration: doesn't count
				} else {
					lvalue(lhs, false)
				}
			}

		case *ast.IncDecStmt:
			lvalue(n.X, false)
		}
		return true
	})
}
