// Copyright 2019 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package check implements binary analysis similar to bazel's nogo, or the
// unitchecker package. It exists in order to provide additional facilities for
// analysis, namely plumbing through the output from dumping the generated
// binary (to analyze actual produced code).
package check

import (
	"fmt"
	"go/ast"
	"go/build"
	"go/parser"
	"go/token"
	"go/types"
	"io"
	"os"
	"path"
	"path/filepath"
	"reflect"
	"regexp"
	"runtime/debug"
	"slices"
	"strings"
	"sync"

	"golang.org/x/tools/go/analysis"
	"golang.org/x/tools/go/gcexportdata"
	"gvisor.dev/gvisor/runsc/flag"
	"gvisor.dev/gvisor/tools/nogo/facts"
	"gvisor.dev/gvisor/tools/nogo/flags"
)

var (
	// showTimes indicates we should show analyzer times.
	showTimes = flag.Bool("show_times", false, "show all analyzer times")
)

var (
	tagsOnce       sync.Once
	buildTags      []string
	releaseTagsVal []string
	releaseTagsErr error
)

// Hack! factFacts only provides facts loaded from directly imported packages
// for efficiency (see importer.cache). In general, if you need a fact from a
// package that isn't otherwise imported, the expectation is that you will add
// a dummy import/use of the desired package to ensure it is a dependency.
//
// Unfortunately, some packages need facts from internal packages. Since
// internal packages cannot be imported we explicitly import in this tool to
// ensure the facts are available to ImportPackageFact.
var internalPackages = []string{
	// Required by pkg/sync for internal/abi.MapType.
	"internal/abi",
}

// shouldInclude indicates whether the file should be included.
func shouldInclude(path string) (bool, error) {
	tagsOnce.Do(func() {
		if len(flags.BuildTags) > 0 {
			buildTags = strings.Split(flags.BuildTags, ",")
		}
		releaseTagsVal, releaseTagsErr = releaseTags()
	})
	if releaseTagsErr != nil {
		return false, releaseTagsErr
	}
	// Users should set GOEXPERIMENT, GOARM64, GOAMD64, etc in the
	// environment as necessary to ensure build.Default picks the correct
	// values.
	//
	// TODO(mpratt): remove the GOOS and GOARCH flags? Those can also come
	// from the environment.
	ctx := build.Default
	ctx.GOOS = flags.GOOS
	ctx.GOARCH = flags.GOARCH
	ctx.BuildTags = buildTags
	ctx.ReleaseTags = releaseTagsVal
	// Force disable cgo, as we don't have the Bazel infrastructure to
	// analyze cgo dependencies.
	ctx.CgoEnabled = false
	// Remove the boringcrypto build tag, as it uses cgo. CgoEnabled =
	// false should be sufficient to exclude the cgo files, but it doesn't
	// seem to be.
	toolTags := slices.Clone(ctx.ToolTags)
	toolTags = slices.DeleteFunc(toolTags, func(t string) bool { return t == "goexperiment.boringcrypto" })
	ctx.ToolTags = toolTags
	return ctx.MatchFile(filepath.Dir(path), filepath.Base(path))
}

// sortSrcs sorts a set of src files into Go files and non-Go files.
func sortSrcs(srcs []string) (goFiles []string, nonGoFiles []string) {
	for _, filename := range srcs {
		if strings.HasSuffix(filename, ".go") {
			goFiles = append(goFiles, filename)
		} else {
			nonGoFiles = append(nonGoFiles, filename)
		}
	}
	return
}

// importerEntry is a single entry in the importer.
type importerEntry struct {
	ready    sync.WaitGroup
	pkg      *types.Package
	findings FindingSet
	err      error
	factsMu  sync.Mutex
	facts    *facts.Package
}

// importer is an almost-implementation of go/types.Importer.
//
// This wraps a configuration, which provides the map of package names to
// files, and the facts. Note that this importer implementation will always
// pass when a given package is not available.
type importer struct {
	fset *token.FileSet

	// sources is a map from package path to the source file paths in that
	// package.
	//
	// N.B. key is the package path (including GOROOT vendor/ prefix if any),
	// not the import path (what appears in the import declaration).
	sources map[string][]string

	// roots is the set of potential GOROOT prefixes on the file paths in sources.
	// GOROOT is effectively a merged version of these roots.
	roots []string

	analyzers map[*analysis.Analyzer]analyzer

	// mu protects cache & bundles (see below).
	mu    sync.Mutex
	cache map[string]*importerEntry // key is package path (see sources above).

	// bundles is protected by mu, but once set is immutable.
	bundles []*facts.Bundle

	// importsMu protects imports.
	importsMu sync.Mutex
	imports   map[string]*types.Package // key is package path (see sources above).
}

// loadBundles loads all bundle files.
//
// This should only be called from loadFacts, below. After calling this
// function, i.bundles may be read freely without holding a lock.
func (i *importer) loadBundles() error {
	i.mu.Lock()
	defer i.mu.Unlock()

	// Are bundles already available?
	if i.bundles != nil {
		return nil
	}

	// Scan all bundle files.
	for _, filename := range flags.Bundles {
		// Open the given filename as a bundle.
		loadedFacts, err := facts.BundleFrom(filename)
		if err != nil {
			return fmt.Errorf("error loading bundled facts: %w", err)
		}

		// Add to the set of available bundles.
		i.bundles = append(i.bundles, loadedFacts)
	}

	return nil
}

// loadFacts returns all package facts for the given name.
//
// This should be called only from importPackage, as this may deserialize a
// facts file (which is an expensive operation). Callers should generally rely
// on fastFacts to access facts for packages that have already been imported.
func (i *importer) loadFacts(pkg *types.Package) (*facts.Package, error) {
	// Attempt to load from the fact map.
	filename, ok := flags.FactMap[pkg.Path()]
	if ok {
		r, openErr := os.Open(filename)
		if openErr != nil {
			return nil, fmt.Errorf("error loading facts from %q: %w", filename, openErr)
		}
		defer r.Close()
		loadedFacts := facts.NewPackage()
		if readErr := loadedFacts.ReadFrom(pkg, r); readErr != nil {
			return nil, fmt.Errorf("error loading facts: %w", readErr)
		}
		return loadedFacts, nil
	}

	// Attempt to load any bundles.
	if err := i.loadBundles(); err != nil {
		return nil, fmt.Errorf("error loading bundles: %w", err)
	}

	// Try to import from the bundle.
	for _, bundleFacts := range i.bundles {
		localFacts, err := bundleFacts.Package(pkg)
		if err != nil {
			return nil, fmt.Errorf("error loading from a bundle: %w", err)
		}
		if localFacts != nil {
			return localFacts, nil
		}
	}

	// Nothing available for this package?
	return nil, nil
}

// fastFacts returns facts for the given package.
//
// This relies exclusively on loaded packages, as the parameter is
// *types.Package and therefore the package data must already be available.
func (i *importer) fastFacts(pkg *types.Package) *facts.Package {
	i.mu.Lock()
	e, ok := i.cache[pkg.Path()]
	i.mu.Unlock()
	if !ok {
		return nil
	}

	e.factsMu.Lock()
	defer e.factsMu.Unlock()

	// Do we have them already?
	if e.facts != nil {
		return e.facts
	}

	// Load the facts.
	facts, err := i.loadFacts(pkg)
	if err != nil {
		// There are no facts available, but no good way to propagate
		// this minor error. It may be intentional that no analysis was
		// performed on some part of the standard library, for example.
		return nil
	}
	e.facts = facts // Cache the result.
	return facts
}

// findArchive finds the archive for the given package.
func (i *importer) findArchive(path string) (rc io.ReadCloser, err error) {
	realPath, ok := flags.ArchiveMap[path]
	if !ok {
		return i.findBinary(path)
	}
	return os.Open(realPath)
}

// findBinary finds the binary for the given package.
func (i *importer) findBinary(path string) (rc io.ReadCloser, err error) {
	realPath, ok := flags.ImportMap[path]
	if !ok {
		// Not found in the import path. Attempt to find the package
		// via the standard library.
		rc, err = findStdPkg(path)
	} else {
		// Open the file.
		rc, err = os.Open(realPath)
	}
	return rc, err
}

// importPackage almost-implements types.ImporterFrom.ImportFrom.
//
// This must be called by other methods directly.
func (i *importer) importPackage(importPath, dir string) (*types.Package, error) {
	if importPath == "unsafe" {
		// Special case: go/types has pre-defined type information for
		// unsafe. We ensure that this package is correct, in case any
		// analyzers are specifically looking for this.
		return types.Unsafe, nil
	}

	pkgPath := importPath
	if dir != "" && len(i.roots) > 0 {
		// Vendor directories get first chance to satisfy import.
		//
		// N.B. The goal here is to support vendored packages in the standard
		// library (i.roots non-empty), which has a single top-level
		// vendor directory. We may not handle more complex vendor
		// situations correctly.
		// Check if a package path is available without actually loading it.
		pkgExists := func(pkgPath string) bool {
			// 1. In cache?
			i.mu.Lock()
			_, ok := i.cache[pkgPath]
			i.mu.Unlock()
			if ok {
				return true
			}

			// 2. Source available?
			if _, ok := i.sources[pkgPath]; ok {
				return true
			}

			// 3. Binary available?
			rc, err := i.findBinary(pkgPath)
			if err != nil {
				return false
			}
			// TODO(mpratt): Add a more efficient option that doesn't
			// actually open the binary if this gets too expensive.
			rc.Close()
			return true
		}

		// Determine which root we are in.
		var root string
		var dirPath string
		for _, cand := range i.roots {
			var ok bool
			dirPath, ok = strings.CutPrefix(dir, cand)
			if ok {
				root = cand
				break
			}
		}
		if root == "" {
			return nil, fmt.Errorf("importPackage dir %q outside of known roots %s", dir, i.roots)
		}

		for {
			candPkgPath := path.Join(dirPath, "vendor", importPath)
			if pkgExists(candPkgPath) {
				// Package is vendored. Continue import below
				// using the full vendor path.
				pkgPath = candPkgPath
				break
			}

			if dirPath == "" {
				// Already at root. Nothing else to search.
				// Package is not vendored.
				break
			}

			// Walk up one directory.
			i := strings.LastIndex(dirPath, "/")
			if i >= 0 {
				dirPath = dirPath[:i]
			} else {
				dirPath = ""
			}
		}
	}

	// Pull the internal entry.
	i.mu.Lock()
	entry, ok := i.cache[pkgPath]
	if ok && entry.pkg != nil {
		i.mu.Unlock()
		entry.ready.Wait()
		return entry.pkg, entry.err
	}

	// Start preparing this entry.
	entry = new(importerEntry)
	entry.ready.Add(1)
	defer entry.ready.Done()
	i.cache[pkgPath] = entry
	i.mu.Unlock()

	// If we have the srcs for this package, then we can actually do an
	// analysis from first principles to validate the package and derive
	// the types. We strictly prefer this to the gcexportdata.
	if srcs, ok := i.sources[pkgPath]; ok && len(srcs) > 0 {
		entry.pkg, entry.findings, entry.facts, entry.err = i.checkPackage(pkgPath, srcs)
		if entry.err != nil {
			return nil, entry.err
		}
		i.importsMu.Lock()
		defer i.importsMu.Unlock()
		i.imports[pkgPath] = entry.pkg
		return entry.pkg, entry.err
	}

	// Load all exported data. Unfortunately, we will have to hold the lock
	// during this time. The imported may access imports directly.
	rc, err := i.findBinary(pkgPath)
	if err != nil {
		return nil, err
	}
	defer rc.Close()
	r, err := gcexportdata.NewReader(rc)
	if err != nil {
		return nil, err
	}
	i.importsMu.Lock()
	defer i.importsMu.Unlock()
	entry.pkg, entry.err = gcexportdata.Read(r, i.fset, i.imports, pkgPath)
	return entry.pkg, entry.err
}

// Import implements types.Importer.Import.
func (i *importer) Import(path string) (*types.Package, error) {
	return i.ImportFrom(path, "", 0)
}

// ImportFrom implements types.ImporterFrom.ImportFrom.
func (i *importer) ImportFrom(path, dir string, mode types.ImportMode) (*types.Package, error) {
	return i.importPackage(path, dir)
}

// errorImporter tracks the last error.
type errorImporter struct {
	*importer
	lastErr error
}

// Import implements types.Importer.Import.
func (i *errorImporter) Import(path string) (*types.Package, error) {
	return i.ImportFrom(path, "", 0)
}

// ImportFrom implements types.ImporterFrom.ImportFrom.
func (i *errorImporter) ImportFrom(path, dir string, mode types.ImportMode) (*types.Package, error) {
	pkg, err := i.importer.importPackage(path, dir)
	if err != nil {
		i.lastErr = err
	}
	return pkg, err
}

// checkPackage is the backing implementation for CheckPackage and others.
//
// The implementation was adapted from [1], which was in turn adapted from [2].
// This returns a list of matching analysis issues, or an error if the analysis
// could not be completed.
//
// Note that a partial result may be returned if an error occurred on at least
// one analyzer. This may be expected if e.g. a binary is not provided but a
// binaryAnalyzer is used.
//
// [1] bazelbuid/rules_go/tools/builders/nogo_main.go
// [2] golang.org/x/tools/go/checker/internal/checker
func (i *importer) checkPackage(path string, srcs []string) (*types.Package, FindingSet, *facts.Package, error) {
	// Load all source files.
	goFiles, nonGoFiles := sortSrcs(srcs)
	syntax := make([]*ast.File, 0, len(goFiles))
	for _, file := range goFiles {
		include, err := shouldInclude(file)
		if err != nil {
			return nil, nil, nil, fmt.Errorf("error evaluating file %q: %w", file, err)
		}
		if !include {
			continue
		}
		s, err := parser.ParseFile(i.fset, file, nil, parser.ParseComments)
		if err != nil {
			return nil, nil, nil, fmt.Errorf("error parsing file %q: %w", file, err)
		}
		syntax = append(syntax, s)
	}
	otherFiles := make([]string, 0, len(nonGoFiles))
	for _, file := range nonGoFiles {
		include, err := shouldInclude(file)
		if err != nil {
			return nil, nil, nil, fmt.Errorf("error evaluating non-Go file %q: %w", file, err)
		}
		if !include {
			continue
		}
		otherFiles = append(otherFiles, file)
	}

	// Check type information.
	ei := &errorImporter{
		importer: i,
	}
	typesSizes := types.SizesFor("gc", flags.GOARCH)
	typeConfig := types.Config{
		Importer:  ei,
		Error:     func(error) {},
		GoVersion: flags.GOVERSION,
	}
	typesInfo := &types.Info{
		Types:        make(map[ast.Expr]types.TypeAndValue),
		Instances:    make(map[*ast.Ident]types.Instance),
		Uses:         make(map[*ast.Ident]types.Object),
		Defs:         make(map[*ast.Ident]types.Object),
		Implicits:    make(map[ast.Node]types.Object),
		Scopes:       make(map[ast.Node]*types.Scope),
		Selections:   make(map[*ast.SelectorExpr]*types.Selection),
		FileVersions: make(map[*ast.File]string),
	}
	astPackage, err := typeConfig.Check(path, i.fset, syntax, typesInfo)
	if err != nil {
		return nil, nil, nil, fmt.Errorf("error checking types: %w", err)
	}

	// Note that facts should be reconcilable between types as of go/tools
	// commit ee04797aa0b6be5ce3d5f7ac0f91e34716b3acdf. We previously used
	// to do a sanity check to ensure that binary import data was
	// compatible with ast-derived data, but this is no longer necessary.
	// If packages are available locally, we can refer to those directly.
	astFacts := facts.NewPackage()

	// Recursively visit all analyzers.
	var (
		resultsMu sync.RWMutex // protects results & errs, findings.
		factsMu   sync.RWMutex // protects facts.
		ready     = make(map[*analysis.Analyzer]*sync.WaitGroup)
		results   = make(map[*analysis.Analyzer]any)
		errs      = make(map[*analysis.Analyzer]error)
		findings  = make(FindingSet, 0)
	)
	for a := range i.analyzers {
		wg := new(sync.WaitGroup)
		wg.Add(1) // For analysis.
		ready[a] = wg
	}
	limit := make(chan struct{}, 1)
	for a, wg := range ready {
		go func(a *analysis.Analyzer, wg *sync.WaitGroup) {
			defer wg.Done()

			// Wait for all requirements.
			for _, orig := range a.Requires {
				ready[orig].Wait()

				// Should we bail early?
				resultsMu.RLock()
				if err := errs[orig]; err != nil {
					resultsMu.RUnlock()
					resultsMu.Lock()
					defer resultsMu.Unlock()
					errs[a] = err
					return
				}
				resultsMu.RUnlock()
			}

			limit <- struct{}{}
			defer func() { <-limit }()

			// Collect local fact types.
			localFactTypes := make(map[reflect.Type]bool)
			for _, ft := range a.FactTypes {
				localFactTypes[reflect.TypeOf(ft)] = true
			}

			// Run the analysis.
			var localFindings FindingSet
			p := &analysis.Pass{
				Analyzer:   a,
				Fset:       i.fset,
				Files:      syntax,
				OtherFiles: otherFiles,
				Pkg:        astPackage,
				TypesInfo:  typesInfo,
				ResultOf:   results, // All results.
				Report: func(d analysis.Diagnostic) {
					localFindings = append(localFindings, Finding{
						Category: a.Name,
						Position: i.fset.Position(d.Pos),
						Message:  d.Message,
						GOOS:     flags.GOOS,
						GOARCH:   flags.GOARCH,
					})
				},
				ImportPackageFact: func(pkg *types.Package, ptr analysis.Fact) bool {
					if pkg != astPackage {
						if f := i.fastFacts(pkg); f != nil {
							return f.ImportFact(nil, ptr)
						}
						return false
					}
					factsMu.RLock()
					defer factsMu.RUnlock()
					return astFacts.ImportFact(nil, ptr)
				},
				ExportPackageFact: func(fact analysis.Fact) {
					factsMu.Lock()
					defer factsMu.Unlock()
					astFacts.ExportFact(nil, fact)
				},
				ImportObjectFact: func(obj types.Object, ptr analysis.Fact) bool {
					if pkg := obj.Pkg(); pkg != nil && pkg != astPackage {
						if f := i.fastFacts(pkg); f != nil {
							return f.ImportFact(obj, ptr)
						}
						return false
					}
					factsMu.RLock()
					defer factsMu.RUnlock()
					return astFacts.ImportFact(obj, ptr)
				},
				ExportObjectFact: func(obj types.Object, fact analysis.Fact) {
					if obj == nil {
						// Tried to export nil object?
						return
					}
					if obj.Pkg() != astPackage {
						// This is not allowed: the
						// built-in facts library will
						// also panic in this case.
						return
					}
					factsMu.Lock()
					defer factsMu.Unlock()
					astFacts.ExportFact(obj, fact)
				},
				AllPackageFacts: func() (rv []analysis.PackageFact) {
					factsMu.RLock()
					defer factsMu.RUnlock()
					// Pull all dependencies.
					for _, importedPkg := range astPackage.Imports() {
						otherFacts := i.fastFacts(importedPkg)
						if otherFacts == nil {
							continue
						}
						for typ := range localFactTypes {
							v := reflect.New(typ.Elem())
							if otherFacts.ImportFact(nil, v.Interface().(analysis.Fact)) {
								rv = append(rv, analysis.PackageFact{
									Package: importedPkg,
									Fact:    v.Interface().(analysis.Fact),
								})
							}
						}
					}
					// Pull all local facts.
					for typ := range localFactTypes {
						v := reflect.New(typ.Elem())
						if astFacts.ImportFact(nil, v.Interface().(analysis.Fact)) {
							rv = append(rv, analysis.PackageFact{
								Package: astPackage,
								Fact:    v.Interface().(analysis.Fact),
							})
						}
					}
					return
				},
				AllObjectFacts: func() (rv []analysis.ObjectFact) {
					factsMu.RLock()
					defer factsMu.RUnlock()
					// Pull all local facts.
					for obj := range astFacts.Objects {
						for typ := range localFactTypes {
							v := reflect.New(typ.Elem())
							if astFacts.ImportFact(obj, v.Interface().(analysis.Fact)) {
								rv = append(rv, analysis.ObjectFact{
									Object: obj,
									Fact:   v.Interface().(analysis.Fact),
								})
							}
						}
					}
					return
				},
				TypesSizes: typesSizes,
			}

			// Ensure any analyzer panics are captured. This may
			// happen for packages that are not supported by
			// specific analyzers. The only panic that can happen
			// is while resultsMu is held as a read-only lock.
			var (
				result any
				err    error
			)
			defer func() {
				if r := recover(); r != nil {
					// In order to make the multiple
					// analyzers running concurrently
					// debuggable, capture panic exceptions
					// and propagate as an analyzer error.
					err = fmt.Errorf("panic recovered: %s (%s)", r, debug.Stack())
					resultsMu.RUnlock() // +checklocksignore
				}
				resultsMu.Lock()
				findings = append(findings, localFindings...)
				results[a] = result
				errs[a] = err
				resultsMu.Unlock()
			}()
			found := findAnalyzer(i.analyzers, a)
			resultsMu.RLock()
			if ba, ok := found.(binaryAnalyzer); ok {
				// Load the binary and analyze.
				rc, loadErr := i.findArchive(path)
				if loadErr != nil {
					err = loadErr
				} else {
					result, err = ba.Run(p, rc)
					rc.Close()
				}
			} else {
				result, err = a.Run(p)
			}
			resultsMu.RUnlock()
		}(a, wg)
	}
	for _, wg := range ready {
		// Wait for completion.
		wg.Wait()
	}
	for a := range ready {
		// Check the error. If we generate an error here, we report
		// this as a finding that can be suppressed. Some analyzers
		// will fail on some packages.
		if errs[a] != nil {
			filename := ""
			if len(srcs) > 0 {
				filename = srcs[0]
			}
			findings = append(findings, Finding{
				Category: a.Name,
				Position: token.Position{Filename: filename},
				Message:  errs[a].Error(),
				GOOS:     flags.GOOS,
				GOARCH:   flags.GOARCH,
			})
			continue
		}

		// Check the result. Per above, we check that the type is what
		// we expected and that an error did not occur during analysis.
		if got, want := reflect.TypeOf(results[a]), a.ResultType; got != want {
			return astPackage, findings, astFacts, fmt.Errorf("error: analyzer %s returned %v (expected type %v)", a.Name, results[a], want)
		}
	}

	// Return all findings.
	return astPackage, findings, astFacts, nil
}

func (i *importer) hardImportPackages(pkgs []string) error {
	for _, pkg := range internalPackages {
		_, err := i.Import(pkg)
		if err != nil {
			return fmt.Errorf("error importing %s: %w", pkg, err)
		}
	}
	return nil
}

// Package runs all analyzer on a single package.
func Package(path string, srcs []string) (FindingSet, facts.Serializer, error) {
	i := &importer{
		fset:      token.NewFileSet(),
		cache:     make(map[string]*importerEntry),
		imports:   make(map[string]*types.Package),
		analyzers: allAnalyzers,
	}

	// See comment on internalPackages.
	if err := i.hardImportPackages(internalPackages); err != nil {
		return nil, nil, err
	}

	_, findings, facts, err := i.checkPackage(path, srcs)
	if err != nil {
		return nil, nil, err
	}
	return findings, facts, nil
}

// allFactsAndFindings returns all factsAndFindings from an importer.
func (i *importer) allFactsAndFindings() (FindingSet, *facts.Bundle) {
	var (
		findings = make(FindingSet, 0)
		allFacts = facts.NewBundle()
	)
	for path, entry := range i.cache {
		findings = append(findings, entry.findings...)
		if entry.facts != nil {
			allFacts.Add(path, entry.facts)
		}
	}
	return findings, allFacts
}

// TemplateFuncs runs all analyzers necessary for template generation, and
// returns a funcmap for template rendering.
//
// Look at facts.ResolveFuncs for a list of supported functions.
func TemplateFuncs(path string, srcs []string) (map[string]any, any, error) {
	i := &importer{
		fset:      token.NewFileSet(),
		cache:     make(map[string]*importerEntry),
		imports:   make(map[string]*types.Package),
		analyzers: renderAnalyzers,
	}

	// See comment on internalPackages.
	if err := i.hardImportPackages(internalPackages); err != nil {
		return nil, nil, err
	}

	pkg, _, localFacts, err := i.checkPackage(path, srcs)
	if err != nil {
		return nil, nil, err
	}
	_, allFacts := i.allFactsAndFindings()
	funcMap, err := facts.ResolveFuncs(pkg, localFacts, allFacts)
	if err != nil {
		return nil, nil, err
	}
	return funcMap, pkg, nil
}

// FindRoots finds a package roots.
func FindRoots(srcs []string, srcRootRegex string) ([]string, error) {
	if srcRootRegex == "" {
		return nil, nil
	}

	// Calculate the root source directories. This is always a directory
	// named 'src'. It's possible that there are more than one of these
	// directories.
	//
	// Note that there may be extra files outside of the root source
	// directory; we simply ignore those.
	re, err := regexp.Compile(srcRootRegex)
	if err != nil {
		return nil, fmt.Errorf("srcRootRegex is not valid: %w", err)
	}
	var srcRootPrefixes []string
	for _, filename := range srcs {
		if s := re.FindString(filename); s != "" && !slices.Contains(srcRootPrefixes, s) {
			srcRootPrefixes = append(srcRootPrefixes, s)
		}
	}
	if len(srcRootPrefixes) == 0 {
		// For whatever reason, we didn't identify a good common prefix to use here.
		return nil, fmt.Errorf("unable to identify src prefix for %v with regex %s", srcs, srcRootRegex)
	}
	return srcRootPrefixes, nil
}

// SplitStdPackages splits a std GOROOT package structure into packages.
func SplitStdPackages(srcs []string, srcRootPrefix string) (map[string][]string, error) {
	sources := make(map[string][]string)
	for _, filename := range srcs {
		if !strings.HasPrefix(filename, srcRootPrefix) {
			continue // Superflouous file.
		}
		d := path.Dir(filename)
		if len(srcRootPrefix) >= len(d) {
			continue // Not a file.
		}
		pkg := d[len(srcRootPrefix):]
		for len(pkg) > 0 && pkg[0] == '/' {
			pkg = pkg[1:]
		}
		if len(pkg) == 0 {
			continue // Also not a file.
		}

		// Ignore any files with /testdata/ in the path.
		if strings.Contains(filename, "/testdata/") {
			continue
		}

		// Ignore all test files since they *may* be in a different
		// package than the rest of the sources.
		if strings.HasSuffix(filename, "_test.go") {
			continue
		}

		// Skip the "builtin" package, which is only for docs and not a
		// real package. Attempting type checking goes crazy.
		if pkg == "builtin" {
			continue
		}

		if strings.Contains(filename, "cmd/compile/internal/typecheck/_builtin/runtime.go") {
			// Place the special runtime package (functions emitted by the
			// compiler itself) into the runtime packages.
			pkg = "runtime"
		} else if strings.HasPrefix(pkg, "cmd") {
			// Ignore packages in cmd, these are packages that are needed to build Go
			// programs (like the Go tool or the compiler), but they are not part of
			// the standard library and can't be in the set of transitive dependencies.
			continue
		}

		// Add to the package.
		sources[pkg] = append(sources[pkg], filename)
	}

	return sources, nil
}

// Bundle checks a bundle of files (typically the standard library).
//
// roots is the set of potential GOROOT prefixes on the file paths in sources.
// GOROOT is effectively a merged version of these roots.
func Bundle(sources map[string][]string, roots []string) (FindingSet, facts.Serializer, error) {
	// Process all packages.
	i := &importer{
		fset:      token.NewFileSet(),
		sources:   sources,
		roots:     roots,
		cache:     make(map[string]*importerEntry),
		imports:   make(map[string]*types.Package),
		analyzers: allAnalyzers,
	}
	for pkg := range sources {
		// Was there an error processing this package?
		if _, err := i.importPackage(pkg, ""); err != nil {
			return nil, nil, err
		}
	}

	findings, facts := i.allFactsAndFindings()
	return findings, facts, nil
}
