// Copyright 2015 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 cgocall defines an Analyzer that detects some violations of
// the cgo pointer passing rules.
package cgocall

import (
	"fmt"
	"go/ast"
	"go/format"
	"go/parser"
	"go/token"
	"go/types"
	"log"
	"os"
	"strconv"

	"golang.org/x/tools/go/analysis"
	"golang.org/x/tools/internal/analysisinternal"
)

const debug = false

const Doc = `detect some violations of the cgo pointer passing rules

Check for invalid cgo pointer passing.
This looks for code that uses cgo to call C code passing values
whose types are almost always invalid according to the cgo pointer
sharing rules.
Specifically, it warns about attempts to pass a Go chan, map, func,
or slice to C, either directly, or via a pointer, array, or struct.`

var Analyzer = &analysis.Analyzer{
	Name:             "cgocall",
	Doc:              Doc,
	URL:              "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/cgocall",
	RunDespiteErrors: true,
	Run:              run,
}

func run(pass *analysis.Pass) (any, error) {
	if !analysisinternal.Imports(pass.Pkg, "runtime/cgo") {
		return nil, nil // doesn't use cgo
	}

	cgofiles, info, err := typeCheckCgoSourceFiles(pass.Fset, pass.Pkg, pass.Files, pass.TypesInfo, pass.TypesSizes)
	if err != nil {
		return nil, err
	}
	for _, f := range cgofiles {
		checkCgo(pass.Fset, f, info, pass.Reportf)
	}
	return nil, nil
}

func checkCgo(fset *token.FileSet, f *ast.File, info *types.Info, reportf func(token.Pos, string, ...any)) {
	ast.Inspect(f, func(n ast.Node) bool {
		call, ok := n.(*ast.CallExpr)
		if !ok {
			return true
		}

		// Is this a C.f() call?
		var name string
		if sel, ok := ast.Unparen(call.Fun).(*ast.SelectorExpr); ok {
			if id, ok := sel.X.(*ast.Ident); ok && id.Name == "C" {
				name = sel.Sel.Name
			}
		}
		if name == "" {
			return true // not a call we need to check
		}

		// A call to C.CBytes passes a pointer but is always safe.
		if name == "CBytes" {
			return true
		}

		if debug {
			log.Printf("%s: call to C.%s", fset.Position(call.Lparen), name)
		}

		for _, arg := range call.Args {
			if !typeOKForCgoCall(cgoBaseType(info, arg), make(map[types.Type]bool)) {
				reportf(arg.Pos(), "possibly passing Go type with embedded pointer to C")
				break
			}

			// Check for passing the address of a bad type.
			if conv, ok := arg.(*ast.CallExpr); ok && len(conv.Args) == 1 &&
				isUnsafePointer(info, conv.Fun) {
				arg = conv.Args[0]
			}
			if u, ok := arg.(*ast.UnaryExpr); ok && u.Op == token.AND {
				if !typeOKForCgoCall(cgoBaseType(info, u.X), make(map[types.Type]bool)) {
					reportf(arg.Pos(), "possibly passing Go type with embedded pointer to C")
					break
				}
			}
		}
		return true
	})
}

// typeCheckCgoSourceFiles returns type-checked syntax trees for the raw
// cgo files of a package (those that import "C"). Such files are not
// Go, so there may be gaps in type information around C.f references.
//
// This checker was initially written in vet to inspect raw cgo source
// files using partial type information. However, Analyzers in the new
// analysis API are presented with the type-checked, "cooked" Go ASTs
// resulting from cgo-processing files, so we must choose between
// working with the cooked file generated by cgo (which was tried but
// proved fragile) or locating the raw cgo file (e.g. from //line
// directives) and working with that, as we now do.
//
// Specifically, we must type-check the raw cgo source files (or at
// least the subtrees needed for this analyzer) in an environment that
// simulates the rest of the already type-checked package.
//
// For example, for each raw cgo source file in the original package,
// such as this one:
//
//	package p
//	import "C"
//	import "fmt"
//	type T int
//	const k = 3
//	var x, y = fmt.Println()
//	func f() { ... }
//	func g() { ... C.malloc(k) ... }
//	func (T) f(int) string { ... }
//
// we synthesize a new ast.File, shown below, that dot-imports the
// original "cooked" package using a special name ("·this·"), so that all
// references to package members resolve correctly. (References to
// unexported names cause an "unexported" error, which we ignore.)
//
// To avoid shadowing names imported from the cooked package,
// package-level declarations in the new source file are modified so
// that they do not declare any names.
// (The cgocall analysis is concerned with uses, not declarations.)
// Specifically, type declarations are discarded;
// all names in each var and const declaration are blanked out;
// each method is turned into a regular function by turning
// the receiver into the first parameter;
// and all functions are renamed to "_".
//
//	package p
//	import . "·this·" // declares T, k, x, y, f, g, T.f
//	import "C"
//	import "fmt"
//	const _ = 3
//	var _, _ = fmt.Println()
//	func _() { ... }
//	func _() { ... C.malloc(k) ... }
//	func _(T, int) string { ... }
//
// In this way, the raw function bodies and const/var initializer
// expressions are preserved but refer to the "cooked" objects imported
// from "·this·", and none of the transformed package-level declarations
// actually declares anything. In the example above, the reference to k
// in the argument of the call to C.malloc resolves to "·this·".k, which
// has an accurate type.
//
// This approach could in principle be generalized to more complex
// analyses on raw cgo files. One could synthesize a "C" package so that
// C.f would resolve to "·this·"._C_func_f, for example. But we have
// limited ourselves here to preserving function bodies and initializer
// expressions since that is all that the cgocall analyzer needs.
func typeCheckCgoSourceFiles(fset *token.FileSet, pkg *types.Package, files []*ast.File, info *types.Info, sizes types.Sizes) ([]*ast.File, *types.Info, error) {
	const thispkg = "·this·"

	// Which files are cgo files?
	var cgoFiles []*ast.File
	importMap := map[string]*types.Package{thispkg: pkg}
	for _, raw := range files {
		// If f is a cgo-generated file, Position reports
		// the original file, honoring //line directives.
		filename := fset.Position(raw.Pos()).Filename // sic: Pos, not FileStart
		f, err := parser.ParseFile(fset, filename, nil, parser.SkipObjectResolution)
		if err != nil {
			return nil, nil, fmt.Errorf("can't parse raw cgo file: %v", err)
		}
		found := false
		for _, spec := range f.Imports {
			if spec.Path.Value == `"C"` {
				found = true
				break
			}
		}
		if !found {
			continue // not a cgo file
		}

		// Record the original import map.
		for _, spec := range raw.Imports {
			path, _ := strconv.Unquote(spec.Path.Value)
			importMap[path] = imported(info, spec)
		}

		// Add special dot-import declaration:
		//    import . "·this·"
		var decls []ast.Decl
		decls = append(decls, &ast.GenDecl{
			Tok: token.IMPORT,
			Specs: []ast.Spec{
				&ast.ImportSpec{
					Name: &ast.Ident{Name: "."},
					Path: &ast.BasicLit{
						Kind:  token.STRING,
						Value: strconv.Quote(thispkg),
					},
				},
			},
		})

		// Transform declarations from the raw cgo file.
		for _, decl := range f.Decls {
			switch decl := decl.(type) {
			case *ast.GenDecl:
				switch decl.Tok {
				case token.TYPE:
					// Discard type declarations.
					continue
				case token.IMPORT:
					// Keep imports.
				case token.VAR, token.CONST:
					// Blank the declared var/const names.
					for _, spec := range decl.Specs {
						spec := spec.(*ast.ValueSpec)
						for i := range spec.Names {
							spec.Names[i].Name = "_"
						}
					}
				}
			case *ast.FuncDecl:
				// Blank the declared func name.
				decl.Name.Name = "_"

				// Turn a method receiver:  func (T) f(P) R {...}
				// into regular parameter:  func _(T, P) R {...}
				if decl.Recv != nil {
					var params []*ast.Field
					params = append(params, decl.Recv.List...)
					params = append(params, decl.Type.Params.List...)
					decl.Type.Params.List = params
					decl.Recv = nil
				}
			}
			decls = append(decls, decl)
		}
		f.Decls = decls
		if debug {
			format.Node(os.Stderr, fset, f) // debugging
		}
		cgoFiles = append(cgoFiles, f)
	}
	if cgoFiles == nil {
		return nil, nil, nil // nothing to do (can't happen?)
	}

	// Type-check the synthetic files.
	tc := &types.Config{
		FakeImportC: true,
		Importer: importerFunc(func(path string) (*types.Package, error) {
			return importMap[path], nil
		}),
		Sizes: sizes,
		Error: func(error) {}, // ignore errors (e.g. unused import)
	}
	setGoVersion(tc, pkg)

	// It's tempting to record the new types in the
	// existing pass.TypesInfo, but we don't own it.
	altInfo := &types.Info{
		Types: make(map[ast.Expr]types.TypeAndValue),
	}
	tc.Check(pkg.Path(), fset, cgoFiles, altInfo)

	return cgoFiles, altInfo, nil
}

// cgoBaseType tries to look through type conversions involving
// unsafe.Pointer to find the real type. It converts:
//
//	unsafe.Pointer(x) => x
//	*(*unsafe.Pointer)(unsafe.Pointer(&x)) => x
func cgoBaseType(info *types.Info, arg ast.Expr) types.Type {
	switch arg := arg.(type) {
	case *ast.CallExpr:
		if len(arg.Args) == 1 && isUnsafePointer(info, arg.Fun) {
			return cgoBaseType(info, arg.Args[0])
		}
	case *ast.StarExpr:
		call, ok := arg.X.(*ast.CallExpr)
		if !ok || len(call.Args) != 1 {
			break
		}
		// Here arg is *f(v).
		t := info.Types[call.Fun].Type
		if t == nil {
			break
		}
		ptr, ok := t.Underlying().(*types.Pointer)
		if !ok {
			break
		}
		// Here arg is *(*p)(v)
		elem, ok := ptr.Elem().Underlying().(*types.Basic)
		if !ok || elem.Kind() != types.UnsafePointer {
			break
		}
		// Here arg is *(*unsafe.Pointer)(v)
		call, ok = call.Args[0].(*ast.CallExpr)
		if !ok || len(call.Args) != 1 {
			break
		}
		// Here arg is *(*unsafe.Pointer)(f(v))
		if !isUnsafePointer(info, call.Fun) {
			break
		}
		// Here arg is *(*unsafe.Pointer)(unsafe.Pointer(v))
		u, ok := call.Args[0].(*ast.UnaryExpr)
		if !ok || u.Op != token.AND {
			break
		}
		// Here arg is *(*unsafe.Pointer)(unsafe.Pointer(&v))
		return cgoBaseType(info, u.X)
	}

	return info.Types[arg].Type
}

// typeOKForCgoCall reports whether the type of arg is OK to pass to a
// C function using cgo. This is not true for Go types with embedded
// pointers. m is used to avoid infinite recursion on recursive types.
func typeOKForCgoCall(t types.Type, m map[types.Type]bool) bool {
	if t == nil || m[t] {
		return true
	}
	m[t] = true
	switch t := t.Underlying().(type) {
	case *types.Chan, *types.Map, *types.Signature, *types.Slice:
		return false
	case *types.Pointer:
		return typeOKForCgoCall(t.Elem(), m)
	case *types.Array:
		return typeOKForCgoCall(t.Elem(), m)
	case *types.Struct:
		for i := 0; i < t.NumFields(); i++ {
			if !typeOKForCgoCall(t.Field(i).Type(), m) {
				return false
			}
		}
	}
	return true
}

func isUnsafePointer(info *types.Info, e ast.Expr) bool {
	t := info.Types[e].Type
	return t != nil && t.Underlying() == types.Typ[types.UnsafePointer]
}

type importerFunc func(path string) (*types.Package, error)

func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) }

// TODO(adonovan): make this a library function or method of Info.
func imported(info *types.Info, spec *ast.ImportSpec) *types.Package {
	obj, ok := info.Implicits[spec]
	if !ok {
		obj = info.Defs[spec.Name] // renaming import
	}
	return obj.(*types.PkgName).Imported()
}
