// Copyright 2018 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 packages

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"sync"
	"unicode"

	"golang.org/x/tools/internal/gocommand"
	"golang.org/x/tools/internal/packagesinternal"
)

// debug controls verbose logging.
var debug, _ = strconv.ParseBool(os.Getenv("GOPACKAGESDEBUG"))

// A goTooOldError reports that the go command
// found by exec.LookPath is too old to use the new go list behavior.
type goTooOldError struct {
	error
}

// responseDeduper wraps a DriverResponse, deduplicating its contents.
type responseDeduper struct {
	seenRoots    map[string]bool
	seenPackages map[string]*Package
	dr           *DriverResponse
}

func newDeduper() *responseDeduper {
	return &responseDeduper{
		dr:           &DriverResponse{},
		seenRoots:    map[string]bool{},
		seenPackages: map[string]*Package{},
	}
}

// addAll fills in r with a DriverResponse.
func (r *responseDeduper) addAll(dr *DriverResponse) {
	for _, pkg := range dr.Packages {
		r.addPackage(pkg)
	}
	for _, root := range dr.Roots {
		r.addRoot(root)
	}
	r.dr.GoVersion = dr.GoVersion
}

func (r *responseDeduper) addPackage(p *Package) {
	if prev := r.seenPackages[p.ID]; prev != nil {
		// Package already seen in a previous response. Merge the file lists,
		// removing duplicates. This can happen when the same package appears
		// in multiple driver responses that are being merged together.
		prev.GoFiles = appendUniqueStrings(prev.GoFiles, p.GoFiles)
		prev.CompiledGoFiles = appendUniqueStrings(prev.CompiledGoFiles, p.CompiledGoFiles)
		prev.OtherFiles = appendUniqueStrings(prev.OtherFiles, p.OtherFiles)
		prev.IgnoredFiles = appendUniqueStrings(prev.IgnoredFiles, p.IgnoredFiles)
		prev.EmbedFiles = appendUniqueStrings(prev.EmbedFiles, p.EmbedFiles)
		prev.EmbedPatterns = appendUniqueStrings(prev.EmbedPatterns, p.EmbedPatterns)
		return
	}
	r.seenPackages[p.ID] = p
	r.dr.Packages = append(r.dr.Packages, p)
}

// appendUniqueStrings appends elements from src to dst, skipping duplicates.
func appendUniqueStrings(dst, src []string) []string {
	if len(src) == 0 {
		return dst
	}

	seen := make(map[string]bool, len(dst))
	for _, s := range dst {
		seen[s] = true
	}

	for _, s := range src {
		if !seen[s] {
			dst = append(dst, s)
		}
	}

	return dst
}

func (r *responseDeduper) addRoot(id string) {
	if r.seenRoots[id] {
		return
	}
	r.seenRoots[id] = true
	r.dr.Roots = append(r.dr.Roots, id)
}

type golistState struct {
	cfg *Config
	ctx context.Context

	runner *gocommand.Runner

	// overlay is the JSON file that encodes the Config.Overlay
	// mapping, used by 'go list -overlay=...'.
	overlay string

	envOnce    sync.Once
	goEnvError error
	goEnv      map[string]string

	rootsOnce     sync.Once
	rootDirsError error
	rootDirs      map[string]string

	goVersionOnce  sync.Once
	goVersionError error
	goVersion      int // The X in Go 1.X.

	// vendorDirs caches the (non)existence of vendor directories.
	vendorDirs map[string]bool
}

// getEnv returns Go environment variables. Only specific variables are
// populated -- computing all of them is slow.
func (state *golistState) getEnv() (map[string]string, error) {
	state.envOnce.Do(func() {
		var b *bytes.Buffer
		b, state.goEnvError = state.invokeGo("env", "-json", "GOMOD", "GOPATH")
		if state.goEnvError != nil {
			return
		}

		state.goEnv = make(map[string]string)
		decoder := json.NewDecoder(b)
		if state.goEnvError = decoder.Decode(&state.goEnv); state.goEnvError != nil {
			return
		}
	})
	return state.goEnv, state.goEnvError
}

// mustGetEnv is a convenience function that can be used if getEnv has already succeeded.
func (state *golistState) mustGetEnv() map[string]string {
	env, err := state.getEnv()
	if err != nil {
		panic(fmt.Sprintf("mustGetEnv: %v", err))
	}
	return env
}

// goListDriver uses the go list command to interpret the patterns and produce
// the build system package structure.
// See driver for more details.
//
// overlay is the JSON file that encodes the cfg.Overlay
// mapping, used by 'go list -overlay=...'
func goListDriver(cfg *Config, runner *gocommand.Runner, overlay string, patterns []string) (_ *DriverResponse, err error) {
	// Make sure that any asynchronous go commands are killed when we return.
	parentCtx := cfg.Context
	if parentCtx == nil {
		parentCtx = context.Background()
	}
	ctx, cancel := context.WithCancel(parentCtx)
	defer cancel()

	response := newDeduper()

	state := &golistState{
		cfg:        cfg,
		ctx:        ctx,
		vendorDirs: map[string]bool{},
		overlay:    overlay,
		runner:     runner,
	}

	// Fill in response.Sizes asynchronously if necessary.
	if cfg.Mode&NeedTypesSizes != 0 || cfg.Mode&(NeedTypes|NeedTypesInfo) != 0 {
		errCh := make(chan error)
		go func() {
			compiler, arch, err := getSizesForArgs(ctx, state.cfgInvocation(), runner)
			response.dr.Compiler = compiler
			response.dr.Arch = arch
			errCh <- err
		}()
		defer func() {
			if sizesErr := <-errCh; sizesErr != nil {
				err = sizesErr
			}
		}()
	}

	// Determine files requested in contains patterns
	var containFiles []string
	restPatterns := make([]string, 0, len(patterns))
	// Extract file= and other [querytype]= patterns. Report an error if querytype
	// doesn't exist.
extractQueries:
	for _, pattern := range patterns {
		query, value, ok := strings.Cut(pattern, "=")
		if !ok {
			restPatterns = append(restPatterns, pattern)
		} else {
			switch query {
			case "file":
				containFiles = append(containFiles, value)
			case "pattern":
				restPatterns = append(restPatterns, value)
			case "": // not a reserved query
				restPatterns = append(restPatterns, pattern)
			default:
				for _, rune := range query {
					if rune < 'a' || rune > 'z' { // not a reserved query
						restPatterns = append(restPatterns, pattern)
						continue extractQueries
					}
				}
				// Reject all other patterns containing "="
				return nil, fmt.Errorf("invalid query type %q in query pattern %q", query, pattern)
			}
		}
	}

	// See if we have any patterns to pass through to go list. Zero initial
	// patterns also requires a go list call, since it's the equivalent of
	// ".".
	if len(restPatterns) > 0 || len(patterns) == 0 {
		dr, err := state.createDriverResponse(restPatterns...)
		if err != nil {
			return nil, err
		}
		response.addAll(dr)
	}

	if len(containFiles) != 0 {
		if err := state.runContainsQueries(response, containFiles); err != nil {
			return nil, err
		}
	}

	// (We may yet return an error due to defer.)
	return response.dr, nil
}

// abs returns an absolute representation of path, based on cfg.Dir.
func (cfg *Config) abs(path string) (string, error) {
	if filepath.IsAbs(path) {
		return path, nil
	}
	// In case cfg.Dir is relative, pass it to filepath.Abs.
	return filepath.Abs(filepath.Join(cfg.Dir, path))
}

func (state *golistState) runContainsQueries(response *responseDeduper, queries []string) error {
	for _, query := range queries {
		// TODO(matloob): Do only one query per directory.
		fdir := filepath.Dir(query)
		// Pass absolute path of directory to go list so that it knows to treat it as a directory,
		// not a package path.
		pattern, err := state.cfg.abs(fdir)
		if err != nil {
			return fmt.Errorf("could not determine absolute path of file= query path %q: %v", query, err)
		}
		dirResponse, err := state.createDriverResponse(pattern)

		// If there was an error loading the package, or no packages are returned,
		// or the package is returned with errors, try to load the file as an
		// ad-hoc package.
		// Usually the error will appear in a returned package, but may not if we're
		// in module mode and the ad-hoc is located outside a module.
		if err != nil || len(dirResponse.Packages) == 0 || len(dirResponse.Packages) == 1 && len(dirResponse.Packages[0].GoFiles) == 0 &&
			len(dirResponse.Packages[0].Errors) == 1 {
			var queryErr error
			if dirResponse, queryErr = state.adhocPackage(pattern, query); queryErr != nil {
				return err // return the original error
			}
		}
		isRoot := make(map[string]bool, len(dirResponse.Roots))
		for _, root := range dirResponse.Roots {
			isRoot[root] = true
		}
		for _, pkg := range dirResponse.Packages {
			// Add any new packages to the main set
			// We don't bother to filter packages that will be dropped by the changes of roots,
			// that will happen anyway during graph construction outside this function.
			// Over-reporting packages is not a problem.
			response.addPackage(pkg)
			// if the package was not a root one, it cannot have the file
			if !isRoot[pkg.ID] {
				continue
			}
			for _, pkgFile := range pkg.GoFiles {
				if filepath.Base(query) == filepath.Base(pkgFile) {
					response.addRoot(pkg.ID)
					break
				}
			}
		}
	}
	return nil
}

// adhocPackage attempts to load or construct an ad-hoc package for a given
// query, if the original call to the driver produced inadequate results.
func (state *golistState) adhocPackage(pattern, query string) (*DriverResponse, error) {
	response, err := state.createDriverResponse(query)
	if err != nil {
		return nil, err
	}
	// If we get nothing back from `go list`,
	// try to make this file into its own ad-hoc package.
	// TODO(rstambler): Should this check against the original response?
	if len(response.Packages) == 0 {
		response.Packages = append(response.Packages, &Package{
			ID:              "command-line-arguments",
			PkgPath:         query,
			GoFiles:         []string{query},
			CompiledGoFiles: []string{query},
			Imports:         make(map[string]*Package),
		})
		response.Roots = append(response.Roots, "command-line-arguments")
	}
	// Handle special cases.
	if len(response.Packages) == 1 {
		// golang/go#33482: If this is a file= query for ad-hoc packages where
		// the file only exists on an overlay, and exists outside of a module,
		// add the file to the package and remove the errors.
		if response.Packages[0].ID == "command-line-arguments" ||
			filepath.ToSlash(response.Packages[0].PkgPath) == filepath.ToSlash(query) {
			if len(response.Packages[0].GoFiles) == 0 {
				filename := filepath.Join(pattern, filepath.Base(query)) // avoid recomputing abspath
				// TODO(matloob): check if the file is outside of a root dir?
				for path := range state.cfg.Overlay {
					if path == filename {
						response.Packages[0].Errors = nil
						response.Packages[0].GoFiles = []string{path}
						response.Packages[0].CompiledGoFiles = []string{path}
					}
				}
			}
		}
	}
	return response, nil
}

// Fields must match go list;
// see $GOROOT/src/cmd/go/internal/load/pkg.go.
type jsonPackage struct {
	ImportPath        string
	Dir               string
	Name              string
	Target            string
	Export            string
	GoFiles           []string
	CompiledGoFiles   []string
	IgnoredGoFiles    []string
	IgnoredOtherFiles []string
	EmbedPatterns     []string
	EmbedFiles        []string
	CFiles            []string
	CgoFiles          []string
	CXXFiles          []string
	MFiles            []string
	HFiles            []string
	FFiles            []string
	SFiles            []string
	SwigFiles         []string
	SwigCXXFiles      []string
	SysoFiles         []string
	Imports           []string
	ImportMap         map[string]string
	Deps              []string
	Module            *Module
	TestGoFiles       []string
	TestImports       []string
	XTestGoFiles      []string
	XTestImports      []string
	ForTest           string // q in a "p [q.test]" package, else ""
	DepOnly           bool

	Error      *packagesinternal.PackageError
	DepsErrors []*packagesinternal.PackageError
}

func otherFiles(p *jsonPackage) [][]string {
	return [][]string{p.CFiles, p.CXXFiles, p.MFiles, p.HFiles, p.FFiles, p.SFiles, p.SwigFiles, p.SwigCXXFiles, p.SysoFiles}
}

// createDriverResponse uses the "go list" command to expand the pattern
// words and return a response for the specified packages.
func (state *golistState) createDriverResponse(words ...string) (*DriverResponse, error) {
	// go list uses the following identifiers in ImportPath and Imports:
	//
	// 	"p"			-- importable package or main (command)
	// 	"q.test"		-- q's test executable
	// 	"p [q.test]"		-- variant of p as built for q's test executable
	// 	"q_test [q.test]"	-- q's external test package
	//
	// The packages p that are built differently for a test q.test
	// are q itself, plus any helpers used by the external test q_test,
	// typically including "testing" and all its dependencies.

	// Run "go list" for complete
	// information on the specified packages.
	goVersion, err := state.getGoVersion()
	if err != nil {
		return nil, err
	}
	buf, err := state.invokeGo("list", golistargs(state.cfg, words, goVersion)...)
	if err != nil {
		return nil, err
	}

	seen := make(map[string]*jsonPackage)
	pkgs := make(map[string]*Package)
	additionalErrors := make(map[string][]Error)
	// Decode the JSON and convert it to Package form.
	response := &DriverResponse{
		GoVersion: goVersion,
	}
	for dec := json.NewDecoder(buf); dec.More(); {
		p := new(jsonPackage)
		if err := dec.Decode(p); err != nil {
			return nil, fmt.Errorf("JSON decoding failed: %v", err)
		}

		if p.ImportPath == "" {
			// The documentation for go list says that “[e]rroneous packages will have
			// a non-empty ImportPath”. If for some reason it comes back empty, we
			// prefer to error out rather than silently discarding data or handing
			// back a package without any way to refer to it.
			if p.Error != nil {
				return nil, Error{
					Pos: p.Error.Pos,
					Msg: p.Error.Err,
				}
			}
			return nil, fmt.Errorf("package missing import path: %+v", p)
		}

		// Work around https://golang.org/issue/33157:
		// go list -e, when given an absolute path, will find the package contained at
		// that directory. But when no package exists there, it will return a fake package
		// with an error and the ImportPath set to the absolute path provided to go list.
		// Try to convert that absolute path to what its package path would be if it's
		// contained in a known module or GOPATH entry. This will allow the package to be
		// properly "reclaimed" when overlays are processed.
		if filepath.IsAbs(p.ImportPath) && p.Error != nil {
			pkgPath, ok, err := state.getPkgPath(p.ImportPath)
			if err != nil {
				return nil, err
			}
			if ok {
				p.ImportPath = pkgPath
			}
		}

		if old, found := seen[p.ImportPath]; found {
			// If one version of the package has an error, and the other doesn't, assume
			// that this is a case where go list is reporting a fake dependency variant
			// of the imported package: When a package tries to invalidly import another
			// package, go list emits a variant of the imported package (with the same
			// import path, but with an error on it, and the package will have a
			// DepError set on it). An example of when this can happen is for imports of
			// main packages: main packages can not be imported, but they may be
			// separately matched and listed by another pattern.
			// See golang.org/issue/36188 for more details.

			// The plan is that eventually, hopefully in Go 1.15, the error will be
			// reported on the importing package rather than the duplicate "fake"
			// version of the imported package. Once all supported versions of Go
			// have the new behavior this logic can be deleted.
			// TODO(matloob): delete the workaround logic once all supported versions of
			// Go return the errors on the proper package.

			// There should be exactly one version of a package that doesn't have an
			// error.
			if old.Error == nil && p.Error == nil {
				if !reflect.DeepEqual(p, old) {
					return nil, fmt.Errorf("internal error: go list gives conflicting information for package %v", p.ImportPath)
				}
				continue
			}

			// Determine if this package's error needs to be bubbled up.
			// This is a hack, and we expect for go list to eventually set the error
			// on the package.
			if old.Error != nil {
				var errkind string
				if strings.Contains(old.Error.Err, "not an importable package") {
					errkind = "not an importable package"
				} else if strings.Contains(old.Error.Err, "use of internal package") && strings.Contains(old.Error.Err, "not allowed") {
					errkind = "use of internal package not allowed"
				}
				if errkind != "" {
					if len(old.Error.ImportStack) < 1 {
						return nil, fmt.Errorf(`internal error: go list gave a %q error with empty import stack`, errkind)
					}
					importingPkg := old.Error.ImportStack[len(old.Error.ImportStack)-1]
					if importingPkg == old.ImportPath {
						// Using an older version of Go which put this package itself on top of import
						// stack, instead of the importer. Look for importer in second from top
						// position.
						if len(old.Error.ImportStack) < 2 {
							return nil, fmt.Errorf(`internal error: go list gave a %q error with an import stack without importing package`, errkind)
						}
						importingPkg = old.Error.ImportStack[len(old.Error.ImportStack)-2]
					}
					additionalErrors[importingPkg] = append(additionalErrors[importingPkg], Error{
						Pos:  old.Error.Pos,
						Msg:  old.Error.Err,
						Kind: ListError,
					})
				}
			}

			// Make sure that if there's a version of the package without an error,
			// that's the one reported to the user.
			if old.Error == nil {
				continue
			}

			// This package will replace the old one at the end of the loop.
		}
		seen[p.ImportPath] = p

		pkg := &Package{
			Name:            p.Name,
			ID:              p.ImportPath,
			Dir:             p.Dir,
			Target:          p.Target,
			GoFiles:         absJoin(p.Dir, p.GoFiles, p.CgoFiles),
			CompiledGoFiles: absJoin(p.Dir, p.CompiledGoFiles),
			OtherFiles:      absJoin(p.Dir, otherFiles(p)...),
			EmbedFiles:      absJoin(p.Dir, p.EmbedFiles),
			EmbedPatterns:   absJoin(p.Dir, p.EmbedPatterns),
			IgnoredFiles:    absJoin(p.Dir, p.IgnoredGoFiles, p.IgnoredOtherFiles),
			ForTest:         p.ForTest,
			depsErrors:      p.DepsErrors,
			Module:          p.Module,
		}

		if (state.cfg.Mode&typecheckCgo) != 0 && len(p.CgoFiles) != 0 {
			if len(p.CompiledGoFiles) > len(p.GoFiles) {
				// We need the cgo definitions, which are in the first
				// CompiledGoFile after the non-cgo ones. This is a hack but there
				// isn't currently a better way to find it. We also need the pure
				// Go files and unprocessed cgo files, all of which are already
				// in pkg.GoFiles.
				cgoTypes := p.CompiledGoFiles[len(p.GoFiles)]
				pkg.CompiledGoFiles = append([]string{cgoTypes}, pkg.GoFiles...)
			} else {
				// golang/go#38990: go list silently fails to do cgo processing
				pkg.CompiledGoFiles = nil

				var msg strings.Builder
				fmt.Fprintf(&msg, "go list failed to return CompiledGoFiles for %q.\n", p.Name)

				for _, err := range p.DepsErrors {
					msg.WriteString(strings.TrimSpace(err.Err))
					msg.WriteByte('\n')
				}

				msg.WriteString("This may indicate failure to perform cgo processing; try building at the command line. See https://golang.org/issue/38990.")
				pkg.Errors = append(pkg.Errors, Error{
					Msg:  msg.String(),
					Kind: ListError,
				})
			}
		}

		// Work around https://golang.org/issue/28749:
		// cmd/go puts assembly, C, and C++ files in CompiledGoFiles.
		// Remove files from CompiledGoFiles that are non-go files
		// (or are not files that look like they are from the cache).
		if len(pkg.CompiledGoFiles) > 0 {
			out := pkg.CompiledGoFiles[:0]
			for _, f := range pkg.CompiledGoFiles {
				if ext := filepath.Ext(f); ext != ".go" && ext != "" { // ext == "" means the file is from the cache, so probably cgo-processed file
					continue
				}
				out = append(out, f)
			}
			pkg.CompiledGoFiles = out
		}

		// Extract the PkgPath from the package's ID.
		if i := strings.IndexByte(pkg.ID, ' '); i >= 0 {
			pkg.PkgPath = pkg.ID[:i]
		} else {
			pkg.PkgPath = pkg.ID
		}

		if pkg.PkgPath == "unsafe" {
			pkg.CompiledGoFiles = nil // ignore fake unsafe.go file (#59929)
		} else if len(pkg.CompiledGoFiles) == 0 {
			// Work around for pre-go.1.11 versions of go list.
			// TODO(matloob): they should be handled by the fallback.
			// Can we delete this?
			pkg.CompiledGoFiles = pkg.GoFiles
		}

		// Assume go list emits only absolute paths for Dir.
		if p.Dir != "" && !filepath.IsAbs(p.Dir) {
			log.Fatalf("internal error: go list returned non-absolute Package.Dir: %s", p.Dir)
		}

		if p.Export != "" && !filepath.IsAbs(p.Export) {
			pkg.ExportFile = filepath.Join(p.Dir, p.Export)
		} else {
			pkg.ExportFile = p.Export
		}

		// imports
		//
		// Imports contains the IDs of all imported packages.
		// ImportsMap records (path, ID) only where they differ.
		ids := make(map[string]bool)
		for _, id := range p.Imports {
			ids[id] = true
		}
		pkg.Imports = make(map[string]*Package)
		for path, id := range p.ImportMap {
			pkg.Imports[path] = &Package{ID: id} // non-identity import
			delete(ids, id)
		}
		for id := range ids {
			if id == "C" {
				continue
			}

			pkg.Imports[id] = &Package{ID: id} // identity import
		}
		if !p.DepOnly {
			response.Roots = append(response.Roots, pkg.ID)
		}

		// Temporary work-around for golang/go#39986. Parse filenames out of
		// error messages. This happens if there are unrecoverable syntax
		// errors in the source, so we can't match on a specific error message.
		//
		// TODO(rfindley): remove this heuristic, in favor of considering
		// InvalidGoFiles from the list driver.
		if err := p.Error; err != nil && state.shouldAddFilenameFromError(p) {
			addFilenameFromPos := func(pos string) bool {
				split := strings.Split(pos, ":")
				if len(split) < 1 {
					return false
				}
				filename := strings.TrimSpace(split[0])
				if filename == "" {
					return false
				}
				if !filepath.IsAbs(filename) {
					filename = filepath.Join(state.cfg.Dir, filename)
				}
				info, _ := os.Stat(filename)
				if info == nil {
					return false
				}
				pkg.CompiledGoFiles = append(pkg.CompiledGoFiles, filename)
				pkg.GoFiles = append(pkg.GoFiles, filename)
				return true
			}
			found := addFilenameFromPos(err.Pos)
			// In some cases, go list only reports the error position in the
			// error text, not the error position. One such case is when the
			// file's package name is a keyword (see golang.org/issue/39763).
			if !found {
				addFilenameFromPos(err.Err)
			}
		}

		if p.Error != nil {
			msg := strings.TrimSpace(p.Error.Err) // Trim to work around golang.org/issue/32363.
			// Address golang.org/issue/35964 by appending import stack to error message.
			if msg == "import cycle not allowed" && len(p.Error.ImportStack) != 0 {
				msg += fmt.Sprintf(": import stack: %v", p.Error.ImportStack)
			}
			pkg.Errors = append(pkg.Errors, Error{
				Pos:  p.Error.Pos,
				Msg:  msg,
				Kind: ListError,
			})
		}

		pkgs[pkg.ID] = pkg
	}

	for id, errs := range additionalErrors {
		if p, ok := pkgs[id]; ok {
			p.Errors = append(p.Errors, errs...)
		}
	}
	for _, pkg := range pkgs {
		response.Packages = append(response.Packages, pkg)
	}
	sort.Slice(response.Packages, func(i, j int) bool { return response.Packages[i].ID < response.Packages[j].ID })

	return response, nil
}

func (state *golistState) shouldAddFilenameFromError(p *jsonPackage) bool {
	if len(p.GoFiles) > 0 || len(p.CompiledGoFiles) > 0 {
		return false
	}

	goV, err := state.getGoVersion()
	if err != nil {
		return false
	}

	// On Go 1.14 and earlier, only add filenames from errors if the import stack is empty.
	// The import stack behaves differently for these versions than newer Go versions.
	if goV < 15 {
		return len(p.Error.ImportStack) == 0
	}

	// On Go 1.15 and later, only parse filenames out of error if there's no import stack,
	// or the current package is at the top of the import stack. This is not guaranteed
	// to work perfectly, but should avoid some cases where files in errors don't belong to this
	// package.
	return len(p.Error.ImportStack) == 0 || p.Error.ImportStack[len(p.Error.ImportStack)-1] == p.ImportPath
}

// getGoVersion returns the effective minor version of the go command.
func (state *golistState) getGoVersion() (int, error) {
	state.goVersionOnce.Do(func() {
		state.goVersion, state.goVersionError = gocommand.GoVersion(state.ctx, state.cfgInvocation(), state.runner)
	})
	return state.goVersion, state.goVersionError
}

// getPkgPath finds the package path of a directory if it's relative to a root
// directory.
func (state *golistState) getPkgPath(dir string) (string, bool, error) {
	if !filepath.IsAbs(dir) {
		panic("non-absolute dir passed to getPkgPath")
	}
	roots, err := state.determineRootDirs()
	if err != nil {
		return "", false, err
	}

	for rdir, rpath := range roots {
		// Make sure that the directory is in the module,
		// to avoid creating a path relative to another module.
		if !strings.HasPrefix(dir, rdir) {
			continue
		}
		// TODO(matloob): This doesn't properly handle symlinks.
		r, err := filepath.Rel(rdir, dir)
		if err != nil {
			continue
		}
		if rpath != "" {
			// We choose only one root even though the directory even it can belong in multiple modules
			// or GOPATH entries. This is okay because we only need to work with absolute dirs when a
			// file is missing from disk, for instance when gopls calls go/packages in an overlay.
			// Once the file is saved, gopls, or the next invocation of the tool will get the correct
			// result straight from golist.
			// TODO(matloob): Implement module tiebreaking?
			return path.Join(rpath, filepath.ToSlash(r)), true, nil
		}
		return filepath.ToSlash(r), true, nil
	}
	return "", false, nil
}

// absJoin absolutizes and flattens the lists of files.
func absJoin(dir string, fileses ...[]string) (res []string) {
	for _, files := range fileses {
		for _, file := range files {
			if !filepath.IsAbs(file) {
				file = filepath.Join(dir, file)
			}
			res = append(res, file)
		}
	}
	return res
}

func jsonFlag(cfg *Config, goVersion int) string {
	if goVersion < 19 {
		return "-json"
	}
	var fields []string
	added := make(map[string]bool)
	addFields := func(fs ...string) {
		for _, f := range fs {
			if !added[f] {
				added[f] = true
				fields = append(fields, f)
			}
		}
	}
	addFields("Name", "ImportPath", "Error") // These fields are always needed
	if cfg.Mode&NeedFiles != 0 || cfg.Mode&(NeedTypes|NeedTypesInfo) != 0 {
		addFields("Dir", "GoFiles", "IgnoredGoFiles", "IgnoredOtherFiles", "CFiles",
			"CgoFiles", "CXXFiles", "MFiles", "HFiles", "FFiles", "SFiles",
			"SwigFiles", "SwigCXXFiles", "SysoFiles")
		if cfg.Tests {
			addFields("TestGoFiles", "XTestGoFiles")
		}
	}
	if cfg.Mode&(NeedTypes|NeedTypesInfo) != 0 {
		// CompiledGoFiles seems to be required for the test case TestCgoNoSyntax,
		// even when -compiled isn't passed in.
		// TODO(#52435): Should we make the test ask for -compiled, or automatically
		// request CompiledGoFiles in certain circumstances?
		addFields("Dir", "CompiledGoFiles")
	}
	if cfg.Mode&NeedCompiledGoFiles != 0 {
		addFields("Dir", "CompiledGoFiles", "Export")
	}
	if cfg.Mode&NeedImports != 0 {
		// When imports are requested, DepOnly is used to distinguish between packages
		// explicitly requested and transitive imports of those packages.
		addFields("DepOnly", "Imports", "ImportMap")
		if cfg.Tests {
			addFields("TestImports", "XTestImports")
		}
	}
	if cfg.Mode&NeedDeps != 0 {
		addFields("DepOnly")
	}
	if usesExportData(cfg) {
		// Request Dir in the unlikely case Export is not absolute.
		addFields("Dir", "Export")
	}
	if cfg.Mode&NeedForTest != 0 {
		addFields("ForTest")
	}
	if cfg.Mode&needInternalDepsErrors != 0 {
		addFields("DepsErrors")
	}
	if cfg.Mode&NeedModule != 0 {
		addFields("Module")
	}
	if cfg.Mode&NeedEmbedFiles != 0 {
		addFields("EmbedFiles")
	}
	if cfg.Mode&NeedEmbedPatterns != 0 {
		addFields("EmbedPatterns")
	}
	if cfg.Mode&NeedTarget != 0 {
		addFields("Target")
	}
	return "-json=" + strings.Join(fields, ",")
}

func golistargs(cfg *Config, words []string, goVersion int) []string {
	const findFlags = NeedImports | NeedTypes | NeedSyntax | NeedTypesInfo
	fullargs := []string{
		"-e", jsonFlag(cfg, goVersion),
		fmt.Sprintf("-compiled=%t", cfg.Mode&(NeedCompiledGoFiles|NeedSyntax|NeedTypes|NeedTypesInfo|NeedTypesSizes) != 0),
		fmt.Sprintf("-test=%t", cfg.Tests),
		fmt.Sprintf("-export=%t", usesExportData(cfg)),
		fmt.Sprintf("-deps=%t", cfg.Mode&NeedImports != 0),
		// go list doesn't let you pass -test and -find together,
		// probably because you'd just get the TestMain.
		fmt.Sprintf("-find=%t", !cfg.Tests && cfg.Mode&findFlags == 0 && !usesExportData(cfg)),
		// VCS information is not needed when not printing Stale or StaleReason fields
		"-buildvcs=false",
	}

	// golang/go#60456: with go1.21 and later, go list serves pgo variants, which
	// can be costly to compute and may result in redundant processing for the
	// caller. Disable these variants. If someone wants to add e.g. a NeedPGO
	// mode flag, that should be a separate proposal.
	if goVersion >= 21 {
		fullargs = append(fullargs, "-pgo=off")
	}

	fullargs = append(fullargs, cfg.BuildFlags...)
	fullargs = append(fullargs, "--")
	fullargs = append(fullargs, words...)
	return fullargs
}

// cfgInvocation returns an Invocation that reflects cfg's settings.
func (state *golistState) cfgInvocation() gocommand.Invocation {
	cfg := state.cfg
	return gocommand.Invocation{
		BuildFlags: cfg.BuildFlags,
		CleanEnv:   cfg.Env != nil,
		Env:        cfg.Env,
		Logf:       cfg.Logf,
		WorkingDir: cfg.Dir,
		Overlay:    state.overlay,
	}
}

// invokeGo returns the stdout of a go command invocation.
func (state *golistState) invokeGo(verb string, args ...string) (*bytes.Buffer, error) {
	cfg := state.cfg

	inv := state.cfgInvocation()
	inv.Verb = verb
	inv.Args = args

	stdout, stderr, friendlyErr, err := state.runner.RunRaw(cfg.Context, inv)
	if err != nil {
		// Check for 'go' executable not being found.
		if ee, ok := err.(*exec.Error); ok && ee.Err == exec.ErrNotFound {
			return nil, fmt.Errorf("'go list' driver requires 'go', but %s", exec.ErrNotFound)
		}

		exitErr, ok := err.(*exec.ExitError)
		if !ok {
			// Catastrophic error:
			// - context cancellation
			return nil, fmt.Errorf("couldn't run 'go': %w", err)
		}

		// Old go version?
		if strings.Contains(stderr.String(), "flag provided but not defined") {
			return nil, goTooOldError{fmt.Errorf("unsupported version of go: %s: %s", exitErr, stderr)}
		}

		// Related to #24854
		if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "unexpected directory layout") {
			return nil, friendlyErr
		}

		// Return an error if 'go list' failed due to missing tools in
		// $GOROOT/pkg/tool/$GOOS_$GOARCH (#69606).
		if len(stderr.String()) > 0 && strings.Contains(stderr.String(), `go: no such tool`) {
			return nil, friendlyErr
		}

		// Is there an error running the C compiler in cgo? This will be reported in the "Error" field
		// and should be suppressed by go list -e.
		//
		// This condition is not perfect yet because the error message can include other error messages than runtime/cgo.
		isPkgPathRune := func(r rune) bool {
			// From https://golang.org/ref/spec#Import_declarations:
			//    Implementation restriction: A compiler may restrict ImportPaths to non-empty strings
			//    using only characters belonging to Unicode's L, M, N, P, and S general categories
			//    (the Graphic characters without spaces) and may also exclude the
			//    characters !"#$%&'()*,:;<=>?[\]^`{|} and the Unicode replacement character U+FFFD.
			return unicode.IsOneOf([]*unicode.RangeTable{unicode.L, unicode.M, unicode.N, unicode.P, unicode.S}, r) &&
				!strings.ContainsRune("!\"#$%&'()*,:;<=>?[\\]^`{|}\uFFFD", r)
		}
		// golang/go#36770: Handle case where cmd/go prints module download messages before the error.
		msg := stderr.String()
		for strings.HasPrefix(msg, "go: downloading") {
			msg = msg[strings.IndexRune(msg, '\n')+1:]
		}
		if len(stderr.String()) > 0 && strings.HasPrefix(stderr.String(), "# ") {
			msg := msg[len("# "):]
			if strings.HasPrefix(strings.TrimLeftFunc(msg, isPkgPathRune), "\n") {
				return stdout, nil
			}
			// Treat pkg-config errors as a special case (golang.org/issue/36770).
			if strings.HasPrefix(msg, "pkg-config") {
				return stdout, nil
			}
		}

		// This error only appears in stderr. See golang.org/cl/166398 for a fix in go list to show
		// the error in the Err section of stdout in case -e option is provided.
		// This fix is provided for backwards compatibility.
		if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "named files must be .go files") {
			output := fmt.Sprintf(`{"ImportPath": "command-line-arguments","Incomplete": true,"Error": {"Pos": "","Err": %q}}`,
				strings.Trim(stderr.String(), "\n"))
			return bytes.NewBufferString(output), nil
		}

		// Similar to the previous error, but currently lacks a fix in Go.
		if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "named files must all be in one directory") {
			output := fmt.Sprintf(`{"ImportPath": "command-line-arguments","Incomplete": true,"Error": {"Pos": "","Err": %q}}`,
				strings.Trim(stderr.String(), "\n"))
			return bytes.NewBufferString(output), nil
		}

		// Backwards compatibility for Go 1.11 because 1.12 and 1.13 put the directory in the ImportPath.
		// If the package doesn't exist, put the absolute path of the directory into the error message,
		// as Go 1.13 list does.
		const noSuchDirectory = "no such directory"
		if len(stderr.String()) > 0 && strings.Contains(stderr.String(), noSuchDirectory) {
			errstr := stderr.String()
			abspath := strings.TrimSpace(errstr[strings.Index(errstr, noSuchDirectory)+len(noSuchDirectory):])
			output := fmt.Sprintf(`{"ImportPath": %q,"Incomplete": true,"Error": {"Pos": "","Err": %q}}`,
				abspath, strings.Trim(stderr.String(), "\n"))
			return bytes.NewBufferString(output), nil
		}

		// Workaround for #29280: go list -e has incorrect behavior when an ad-hoc package doesn't exist.
		// Note that the error message we look for in this case is different that the one looked for above.
		if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "no such file or directory") {
			output := fmt.Sprintf(`{"ImportPath": "command-line-arguments","Incomplete": true,"Error": {"Pos": "","Err": %q}}`,
				strings.Trim(stderr.String(), "\n"))
			return bytes.NewBufferString(output), nil
		}

		// Workaround for #34273. go list -e with GO111MODULE=on has incorrect behavior when listing a
		// directory outside any module.
		if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "outside available modules") {
			output := fmt.Sprintf(`{"ImportPath": %q,"Incomplete": true,"Error": {"Pos": "","Err": %q}}`,
				// TODO(matloob): command-line-arguments isn't correct here.
				"command-line-arguments", strings.Trim(stderr.String(), "\n"))
			return bytes.NewBufferString(output), nil
		}

		// Another variation of the previous error
		if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "outside module root") {
			output := fmt.Sprintf(`{"ImportPath": %q,"Incomplete": true,"Error": {"Pos": "","Err": %q}}`,
				// TODO(matloob): command-line-arguments isn't correct here.
				"command-line-arguments", strings.Trim(stderr.String(), "\n"))
			return bytes.NewBufferString(output), nil
		}

		// Workaround for an instance of golang.org/issue/26755: go list -e  will return a non-zero exit
		// status if there's a dependency on a package that doesn't exist. But it should return
		// a zero exit status and set an error on that package.
		if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "no Go files in") {
			// Don't clobber stdout if `go list` actually returned something.
			if len(stdout.String()) > 0 {
				return stdout, nil
			}
			// try to extract package name from string
			stderrStr := stderr.String()
			var importPath string
			colon := strings.Index(stderrStr, ":")
			if colon > 0 && strings.HasPrefix(stderrStr, "go build ") {
				importPath = stderrStr[len("go build "):colon]
			}
			output := fmt.Sprintf(`{"ImportPath": %q,"Incomplete": true,"Error": {"Pos": "","Err": %q}}`,
				importPath, strings.Trim(stderrStr, "\n"))
			return bytes.NewBufferString(output), nil
		}

		// Export mode entails a build.
		// If that build fails, errors appear on stderr
		// (despite the -e flag) and the Export field is blank.
		// Do not fail in that case.
		// The same is true if an ad-hoc package given to go list doesn't exist.
		// TODO(matloob): Remove these once we can depend on go list to exit with a zero status with -e even when
		// packages don't exist or a build fails.
		if !usesExportData(cfg) && !containsGoFile(args) {
			return nil, friendlyErr
		}
	}
	return stdout, nil
}

func containsGoFile(s []string) bool {
	for _, f := range s {
		if strings.HasSuffix(f, ".go") {
			return true
		}
	}
	return false
}

func cmdDebugStr(cmd *exec.Cmd) string {
	env := make(map[string]string)
	for _, kv := range cmd.Env {
		split := strings.SplitN(kv, "=", 2)
		k, v := split[0], split[1]
		env[k] = v
	}

	var args []string
	for _, arg := range cmd.Args {
		quoted := strconv.Quote(arg)
		if quoted[1:len(quoted)-1] != arg || strings.Contains(arg, " ") {
			args = append(args, quoted)
		} else {
			args = append(args, arg)
		}
	}
	return fmt.Sprintf("GOROOT=%v GOPATH=%v GO111MODULE=%v GOPROXY=%v PWD=%v %v", env["GOROOT"], env["GOPATH"], env["GO111MODULE"], env["GOPROXY"], env["PWD"], strings.Join(args, " "))
}

// getSizesForArgs queries 'go list' for the appropriate
// Compiler and GOARCH arguments to pass to [types.SizesFor].
func getSizesForArgs(ctx context.Context, inv gocommand.Invocation, gocmdRunner *gocommand.Runner) (string, string, error) {
	inv.Verb = "list"
	inv.Args = []string{"-f", "{{context.GOARCH}} {{context.Compiler}}", "--", "unsafe"}
	stdout, stderr, friendlyErr, rawErr := gocmdRunner.RunRaw(ctx, inv)
	var goarch, compiler string
	if rawErr != nil {
		rawErrMsg := rawErr.Error()
		if strings.Contains(rawErrMsg, "cannot find main module") ||
			strings.Contains(rawErrMsg, "go.mod file not found") {
			// User's running outside of a module.
			// All bets are off. Get GOARCH and guess compiler is gc.
			// TODO(matloob): Is this a problem in practice?
			inv.Verb = "env"
			inv.Args = []string{"GOARCH"}
			envout, enverr := gocmdRunner.Run(ctx, inv)
			if enverr != nil {
				return "", "", enverr
			}
			goarch = strings.TrimSpace(envout.String())
			compiler = "gc"
		} else if friendlyErr != nil {
			return "", "", friendlyErr
		} else {
			// This should be unreachable, but be defensive
			// in case RunRaw's error results are inconsistent.
			return "", "", rawErr
		}
	} else {
		fields := strings.Fields(stdout.String())
		if len(fields) < 2 {
			return "", "", fmt.Errorf("could not parse GOARCH and Go compiler in format \"<GOARCH> <compiler>\":\nstdout: <<%s>>\nstderr: <<%s>>",
				stdout.String(), stderr.String())
		}
		goarch = fields[0]
		compiler = fields[1]
	}
	return compiler, goarch, nil
}
