1078 lines
33 KiB
Go
1078 lines
33 KiB
Go
// Copyright 2013 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 loader
|
|
|
|
// See doc.go for package documentation and implementation notes.
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/parser"
|
|
"go/token"
|
|
"go/types"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/tools/go/ast/astutil"
|
|
"golang.org/x/tools/go/internal/cgo"
|
|
)
|
|
|
|
var ignoreVendor build.ImportMode
|
|
|
|
const trace = false // show timing info for type-checking
|
|
|
|
// Config specifies the configuration for loading a whole program from
|
|
// Go source code.
|
|
// The zero value for Config is a ready-to-use default configuration.
|
|
type Config struct {
|
|
// Fset is the file set for the parser to use when loading the
|
|
// program. If nil, it may be lazily initialized by any
|
|
// method of Config.
|
|
Fset *token.FileSet
|
|
|
|
// ParserMode specifies the mode to be used by the parser when
|
|
// loading source packages.
|
|
ParserMode parser.Mode
|
|
|
|
// TypeChecker contains options relating to the type checker.
|
|
//
|
|
// The supplied IgnoreFuncBodies is not used; the effective
|
|
// value comes from the TypeCheckFuncBodies func below.
|
|
// The supplied Import function is not used either.
|
|
TypeChecker types.Config
|
|
|
|
// TypeCheckFuncBodies is a predicate over package paths.
|
|
// A package for which the predicate is false will
|
|
// have its package-level declarations type checked, but not
|
|
// its function bodies; this can be used to quickly load
|
|
// dependencies from source. If nil, all func bodies are type
|
|
// checked.
|
|
TypeCheckFuncBodies func(path string) bool
|
|
|
|
// If Build is non-nil, it is used to locate source packages.
|
|
// Otherwise &build.Default is used.
|
|
//
|
|
// By default, cgo is invoked to preprocess Go files that
|
|
// import the fake package "C". This behaviour can be
|
|
// disabled by setting CGO_ENABLED=0 in the environment prior
|
|
// to startup, or by setting Build.CgoEnabled=false.
|
|
Build *build.Context
|
|
|
|
// The current directory, used for resolving relative package
|
|
// references such as "./go/loader". If empty, os.Getwd will be
|
|
// used instead.
|
|
Cwd string
|
|
|
|
// If DisplayPath is non-nil, it is used to transform each
|
|
// file name obtained from Build.Import(). This can be used
|
|
// to prevent a virtualized build.Config's file names from
|
|
// leaking into the user interface.
|
|
DisplayPath func(path string) string
|
|
|
|
// If AllowErrors is true, Load will return a Program even
|
|
// if some of the its packages contained I/O, parser or type
|
|
// errors; such errors are accessible via PackageInfo.Errors. If
|
|
// false, Load will fail if any package had an error.
|
|
AllowErrors bool
|
|
|
|
// CreatePkgs specifies a list of non-importable initial
|
|
// packages to create. The resulting packages will appear in
|
|
// the corresponding elements of the Program.Created slice.
|
|
CreatePkgs []PkgSpec
|
|
|
|
// ImportPkgs specifies a set of initial packages to load.
|
|
// The map keys are package paths.
|
|
//
|
|
// The map value indicates whether to load tests. If true, Load
|
|
// will add and type-check two lists of files to the package:
|
|
// non-test files followed by in-package *_test.go files. In
|
|
// addition, it will append the external test package (if any)
|
|
// to Program.Created.
|
|
ImportPkgs map[string]bool
|
|
|
|
// FindPackage is called during Load to create the build.Package
|
|
// for a given import path from a given directory.
|
|
// If FindPackage is nil, (*build.Context).Import is used.
|
|
// A client may use this hook to adapt to a proprietary build
|
|
// system that does not follow the "go build" layout
|
|
// conventions, for example.
|
|
//
|
|
// It must be safe to call concurrently from multiple goroutines.
|
|
FindPackage func(ctxt *build.Context, importPath, fromDir string, mode build.ImportMode) (*build.Package, error)
|
|
|
|
// AfterTypeCheck is called immediately after a list of files
|
|
// has been type-checked and appended to info.Files.
|
|
//
|
|
// This optional hook function is the earliest opportunity for
|
|
// the client to observe the output of the type checker,
|
|
// which may be useful to reduce analysis latency when loading
|
|
// a large program.
|
|
//
|
|
// The function is permitted to modify info.Info, for instance
|
|
// to clear data structures that are no longer needed, which can
|
|
// dramatically reduce peak memory consumption.
|
|
//
|
|
// The function may be called twice for the same PackageInfo:
|
|
// once for the files of the package and again for the
|
|
// in-package test files.
|
|
//
|
|
// It must be safe to call concurrently from multiple goroutines.
|
|
AfterTypeCheck func(info *PackageInfo, files []*ast.File)
|
|
}
|
|
|
|
// A PkgSpec specifies a non-importable package to be created by Load.
|
|
// Files are processed first, but typically only one of Files and
|
|
// Filenames is provided. The path needn't be globally unique.
|
|
//
|
|
// For vendoring purposes, the package's directory is the one that
|
|
// contains the first file.
|
|
type PkgSpec struct {
|
|
Path string // package path ("" => use package declaration)
|
|
Files []*ast.File // ASTs of already-parsed files
|
|
Filenames []string // names of files to be parsed
|
|
}
|
|
|
|
// A Program is a Go program loaded from source as specified by a Config.
|
|
type Program struct {
|
|
Fset *token.FileSet // the file set for this program
|
|
|
|
// Created[i] contains the initial package whose ASTs or
|
|
// filenames were supplied by Config.CreatePkgs[i], followed by
|
|
// the external test package, if any, of each package in
|
|
// Config.ImportPkgs ordered by ImportPath.
|
|
//
|
|
// NOTE: these files must not import "C". Cgo preprocessing is
|
|
// only performed on imported packages, not ad hoc packages.
|
|
//
|
|
// TODO(adonovan): we need to copy and adapt the logic of
|
|
// goFilesPackage (from $GOROOT/src/cmd/go/build.go) and make
|
|
// Config.Import and Config.Create methods return the same kind
|
|
// of entity, essentially a build.Package.
|
|
// Perhaps we can even reuse that type directly.
|
|
Created []*PackageInfo
|
|
|
|
// Imported contains the initially imported packages,
|
|
// as specified by Config.ImportPkgs.
|
|
Imported map[string]*PackageInfo
|
|
|
|
// AllPackages contains the PackageInfo of every package
|
|
// encountered by Load: all initial packages and all
|
|
// dependencies, including incomplete ones.
|
|
AllPackages map[*types.Package]*PackageInfo
|
|
|
|
// importMap is the canonical mapping of package paths to
|
|
// packages. It contains all Imported initial packages, but not
|
|
// Created ones, and all imported dependencies.
|
|
importMap map[string]*types.Package
|
|
}
|
|
|
|
// PackageInfo holds the ASTs and facts derived by the type-checker
|
|
// for a single package.
|
|
//
|
|
// Not mutated once exposed via the API.
|
|
//
|
|
type PackageInfo struct {
|
|
Pkg *types.Package
|
|
Importable bool // true if 'import "Pkg.Path()"' would resolve to this
|
|
TransitivelyErrorFree bool // true if Pkg and all its dependencies are free of errors
|
|
Files []*ast.File // syntax trees for the package's files
|
|
Errors []error // non-nil if the package had errors
|
|
types.Info // type-checker deductions.
|
|
dir string // package directory
|
|
|
|
checker *types.Checker // transient type-checker state
|
|
errorFunc func(error)
|
|
}
|
|
|
|
func (info *PackageInfo) String() string { return info.Pkg.Path() }
|
|
|
|
func (info *PackageInfo) appendError(err error) {
|
|
if info.errorFunc != nil {
|
|
info.errorFunc(err)
|
|
} else {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
}
|
|
info.Errors = append(info.Errors, err)
|
|
}
|
|
|
|
func (conf *Config) fset() *token.FileSet {
|
|
if conf.Fset == nil {
|
|
conf.Fset = token.NewFileSet()
|
|
}
|
|
return conf.Fset
|
|
}
|
|
|
|
// ParseFile is a convenience function (intended for testing) that invokes
|
|
// the parser using the Config's FileSet, which is initialized if nil.
|
|
//
|
|
// src specifies the parser input as a string, []byte, or io.Reader, and
|
|
// filename is its apparent name. If src is nil, the contents of
|
|
// filename are read from the file system.
|
|
//
|
|
func (conf *Config) ParseFile(filename string, src interface{}) (*ast.File, error) {
|
|
// TODO(adonovan): use conf.build() etc like parseFiles does.
|
|
return parser.ParseFile(conf.fset(), filename, src, conf.ParserMode)
|
|
}
|
|
|
|
// FromArgsUsage is a partial usage message that applications calling
|
|
// FromArgs may wish to include in their -help output.
|
|
const FromArgsUsage = `
|
|
<args> is a list of arguments denoting a set of initial packages.
|
|
It may take one of two forms:
|
|
|
|
1. A list of *.go source files.
|
|
|
|
All of the specified files are loaded, parsed and type-checked
|
|
as a single package. All the files must belong to the same directory.
|
|
|
|
2. A list of import paths, each denoting a package.
|
|
|
|
The package's directory is found relative to the $GOROOT and
|
|
$GOPATH using similar logic to 'go build', and the *.go files in
|
|
that directory are loaded, parsed and type-checked as a single
|
|
package.
|
|
|
|
In addition, all *_test.go files in the directory are then loaded
|
|
and parsed. Those files whose package declaration equals that of
|
|
the non-*_test.go files are included in the primary package. Test
|
|
files whose package declaration ends with "_test" are type-checked
|
|
as another package, the 'external' test package, so that a single
|
|
import path may denote two packages. (Whether this behaviour is
|
|
enabled is tool-specific, and may depend on additional flags.)
|
|
|
|
A '--' argument terminates the list of packages.
|
|
`
|
|
|
|
// FromArgs interprets args as a set of initial packages to load from
|
|
// source and updates the configuration. It returns the list of
|
|
// unconsumed arguments.
|
|
//
|
|
// It is intended for use in command-line interfaces that require a
|
|
// set of initial packages to be specified; see FromArgsUsage message
|
|
// for details.
|
|
//
|
|
// Only superficial errors are reported at this stage; errors dependent
|
|
// on I/O are detected during Load.
|
|
//
|
|
func (conf *Config) FromArgs(args []string, xtest bool) ([]string, error) {
|
|
var rest []string
|
|
for i, arg := range args {
|
|
if arg == "--" {
|
|
rest = args[i+1:]
|
|
args = args[:i]
|
|
break // consume "--" and return the remaining args
|
|
}
|
|
}
|
|
|
|
if len(args) > 0 && strings.HasSuffix(args[0], ".go") {
|
|
// Assume args is a list of a *.go files
|
|
// denoting a single ad hoc package.
|
|
for _, arg := range args {
|
|
if !strings.HasSuffix(arg, ".go") {
|
|
return nil, fmt.Errorf("named files must be .go files: %s", arg)
|
|
}
|
|
}
|
|
conf.CreateFromFilenames("", args...)
|
|
} else {
|
|
// Assume args are directories each denoting a
|
|
// package and (perhaps) an external test, iff xtest.
|
|
for _, arg := range args {
|
|
if xtest {
|
|
conf.ImportWithTests(arg)
|
|
} else {
|
|
conf.Import(arg)
|
|
}
|
|
}
|
|
}
|
|
|
|
return rest, nil
|
|
}
|
|
|
|
// CreateFromFilenames is a convenience function that adds
|
|
// a conf.CreatePkgs entry to create a package of the specified *.go
|
|
// files.
|
|
//
|
|
func (conf *Config) CreateFromFilenames(path string, filenames ...string) {
|
|
conf.CreatePkgs = append(conf.CreatePkgs, PkgSpec{Path: path, Filenames: filenames})
|
|
}
|
|
|
|
// CreateFromFiles is a convenience function that adds a conf.CreatePkgs
|
|
// entry to create package of the specified path and parsed files.
|
|
//
|
|
func (conf *Config) CreateFromFiles(path string, files ...*ast.File) {
|
|
conf.CreatePkgs = append(conf.CreatePkgs, PkgSpec{Path: path, Files: files})
|
|
}
|
|
|
|
// ImportWithTests is a convenience function that adds path to
|
|
// ImportPkgs, the set of initial source packages located relative to
|
|
// $GOPATH. The package will be augmented by any *_test.go files in
|
|
// its directory that contain a "package x" (not "package x_test")
|
|
// declaration.
|
|
//
|
|
// In addition, if any *_test.go files contain a "package x_test"
|
|
// declaration, an additional package comprising just those files will
|
|
// be added to CreatePkgs.
|
|
//
|
|
func (conf *Config) ImportWithTests(path string) { conf.addImport(path, true) }
|
|
|
|
// Import is a convenience function that adds path to ImportPkgs, the
|
|
// set of initial packages that will be imported from source.
|
|
//
|
|
func (conf *Config) Import(path string) { conf.addImport(path, false) }
|
|
|
|
func (conf *Config) addImport(path string, tests bool) {
|
|
if path == "C" {
|
|
return // ignore; not a real package
|
|
}
|
|
if conf.ImportPkgs == nil {
|
|
conf.ImportPkgs = make(map[string]bool)
|
|
}
|
|
conf.ImportPkgs[path] = conf.ImportPkgs[path] || tests
|
|
}
|
|
|
|
// PathEnclosingInterval returns the PackageInfo and ast.Node that
|
|
// contain source interval [start, end), and all the node's ancestors
|
|
// up to the AST root. It searches all ast.Files of all packages in prog.
|
|
// exact is defined as for astutil.PathEnclosingInterval.
|
|
//
|
|
// The zero value is returned if not found.
|
|
//
|
|
func (prog *Program) PathEnclosingInterval(start, end token.Pos) (pkg *PackageInfo, path []ast.Node, exact bool) {
|
|
for _, info := range prog.AllPackages {
|
|
for _, f := range info.Files {
|
|
if f.Pos() == token.NoPos {
|
|
// This can happen if the parser saw
|
|
// too many errors and bailed out.
|
|
// (Use parser.AllErrors to prevent that.)
|
|
continue
|
|
}
|
|
if !tokenFileContainsPos(prog.Fset.File(f.Pos()), start) {
|
|
continue
|
|
}
|
|
if path, exact := astutil.PathEnclosingInterval(f, start, end); path != nil {
|
|
return info, path, exact
|
|
}
|
|
}
|
|
}
|
|
return nil, nil, false
|
|
}
|
|
|
|
// InitialPackages returns a new slice containing the set of initial
|
|
// packages (Created + Imported) in unspecified order.
|
|
//
|
|
func (prog *Program) InitialPackages() []*PackageInfo {
|
|
infos := make([]*PackageInfo, 0, len(prog.Created)+len(prog.Imported))
|
|
infos = append(infos, prog.Created...)
|
|
for _, info := range prog.Imported {
|
|
infos = append(infos, info)
|
|
}
|
|
return infos
|
|
}
|
|
|
|
// Package returns the ASTs and results of type checking for the
|
|
// specified package.
|
|
func (prog *Program) Package(path string) *PackageInfo {
|
|
if info, ok := prog.AllPackages[prog.importMap[path]]; ok {
|
|
return info
|
|
}
|
|
for _, info := range prog.Created {
|
|
if path == info.Pkg.Path() {
|
|
return info
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ---------- Implementation ----------
|
|
|
|
// importer holds the working state of the algorithm.
|
|
type importer struct {
|
|
conf *Config // the client configuration
|
|
start time.Time // for logging
|
|
|
|
progMu sync.Mutex // guards prog
|
|
prog *Program // the resulting program
|
|
|
|
// findpkg is a memoization of FindPackage.
|
|
findpkgMu sync.Mutex // guards findpkg
|
|
findpkg map[findpkgKey]*findpkgValue
|
|
|
|
importedMu sync.Mutex // guards imported
|
|
imported map[string]*importInfo // all imported packages (incl. failures) by import path
|
|
|
|
// import dependency graph: graph[x][y] => x imports y
|
|
//
|
|
// Since non-importable packages cannot be cyclic, we ignore
|
|
// their imports, thus we only need the subgraph over importable
|
|
// packages. Nodes are identified by their import paths.
|
|
graphMu sync.Mutex
|
|
graph map[string]map[string]bool
|
|
}
|
|
|
|
type findpkgKey struct {
|
|
importPath string
|
|
fromDir string
|
|
mode build.ImportMode
|
|
}
|
|
|
|
type findpkgValue struct {
|
|
ready chan struct{} // closed to broadcast readiness
|
|
bp *build.Package
|
|
err error
|
|
}
|
|
|
|
// importInfo tracks the success or failure of a single import.
|
|
//
|
|
// Upon completion, exactly one of info and err is non-nil:
|
|
// info on successful creation of a package, err otherwise.
|
|
// A successful package may still contain type errors.
|
|
//
|
|
type importInfo struct {
|
|
path string // import path
|
|
info *PackageInfo // results of typechecking (including errors)
|
|
complete chan struct{} // closed to broadcast that info is set.
|
|
}
|
|
|
|
// awaitCompletion blocks until ii is complete,
|
|
// i.e. the info field is safe to inspect.
|
|
func (ii *importInfo) awaitCompletion() {
|
|
<-ii.complete // wait for close
|
|
}
|
|
|
|
// Complete marks ii as complete.
|
|
// Its info and err fields will not be subsequently updated.
|
|
func (ii *importInfo) Complete(info *PackageInfo) {
|
|
if info == nil {
|
|
panic("info == nil")
|
|
}
|
|
ii.info = info
|
|
close(ii.complete)
|
|
}
|
|
|
|
type importError struct {
|
|
path string // import path
|
|
err error // reason for failure to create a package
|
|
}
|
|
|
|
// Load creates the initial packages specified by conf.{Create,Import}Pkgs,
|
|
// loading their dependencies packages as needed.
|
|
//
|
|
// On success, Load returns a Program containing a PackageInfo for
|
|
// each package. On failure, it returns an error.
|
|
//
|
|
// If AllowErrors is true, Load will return a Program even if some
|
|
// packages contained I/O, parser or type errors, or if dependencies
|
|
// were missing. (Such errors are accessible via PackageInfo.Errors. If
|
|
// false, Load will fail if any package had an error.
|
|
//
|
|
// It is an error if no packages were loaded.
|
|
//
|
|
func (conf *Config) Load() (*Program, error) {
|
|
// Create a simple default error handler for parse/type errors.
|
|
if conf.TypeChecker.Error == nil {
|
|
conf.TypeChecker.Error = func(e error) { fmt.Fprintln(os.Stderr, e) }
|
|
}
|
|
|
|
// Set default working directory for relative package references.
|
|
if conf.Cwd == "" {
|
|
var err error
|
|
conf.Cwd, err = os.Getwd()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Install default FindPackage hook using go/build logic.
|
|
if conf.FindPackage == nil {
|
|
conf.FindPackage = (*build.Context).Import
|
|
}
|
|
|
|
prog := &Program{
|
|
Fset: conf.fset(),
|
|
Imported: make(map[string]*PackageInfo),
|
|
importMap: make(map[string]*types.Package),
|
|
AllPackages: make(map[*types.Package]*PackageInfo),
|
|
}
|
|
|
|
imp := importer{
|
|
conf: conf,
|
|
prog: prog,
|
|
findpkg: make(map[findpkgKey]*findpkgValue),
|
|
imported: make(map[string]*importInfo),
|
|
start: time.Now(),
|
|
graph: make(map[string]map[string]bool),
|
|
}
|
|
|
|
// -- loading proper (concurrent phase) --------------------------------
|
|
|
|
var errpkgs []string // packages that contained errors
|
|
|
|
// Load the initially imported packages and their dependencies,
|
|
// in parallel.
|
|
// No vendor check on packages imported from the command line.
|
|
infos, importErrors := imp.importAll("", conf.Cwd, conf.ImportPkgs, ignoreVendor)
|
|
for _, ie := range importErrors {
|
|
conf.TypeChecker.Error(ie.err) // failed to create package
|
|
errpkgs = append(errpkgs, ie.path)
|
|
}
|
|
for _, info := range infos {
|
|
prog.Imported[info.Pkg.Path()] = info
|
|
}
|
|
|
|
// Augment the designated initial packages by their tests.
|
|
// Dependencies are loaded in parallel.
|
|
var xtestPkgs []*build.Package
|
|
for importPath, augment := range conf.ImportPkgs {
|
|
if !augment {
|
|
continue
|
|
}
|
|
|
|
// No vendor check on packages imported from command line.
|
|
bp, err := imp.findPackage(importPath, conf.Cwd, ignoreVendor)
|
|
if err != nil {
|
|
// Package not found, or can't even parse package declaration.
|
|
// Already reported by previous loop; ignore it.
|
|
continue
|
|
}
|
|
|
|
// Needs external test package?
|
|
if len(bp.XTestGoFiles) > 0 {
|
|
xtestPkgs = append(xtestPkgs, bp)
|
|
}
|
|
|
|
// Consult the cache using the canonical package path.
|
|
path := bp.ImportPath
|
|
imp.importedMu.Lock() // (unnecessary, we're sequential here)
|
|
ii, ok := imp.imported[path]
|
|
// Paranoid checks added due to issue #11012.
|
|
if !ok {
|
|
// Unreachable.
|
|
// The previous loop called importAll and thus
|
|
// startLoad for each path in ImportPkgs, which
|
|
// populates imp.imported[path] with a non-zero value.
|
|
panic(fmt.Sprintf("imported[%q] not found", path))
|
|
}
|
|
if ii == nil {
|
|
// Unreachable.
|
|
// The ii values in this loop are the same as in
|
|
// the previous loop, which enforced the invariant
|
|
// that at least one of ii.err and ii.info is non-nil.
|
|
panic(fmt.Sprintf("imported[%q] == nil", path))
|
|
}
|
|
if ii.info == nil {
|
|
// Unreachable.
|
|
// awaitCompletion has the postcondition
|
|
// ii.info != nil.
|
|
panic(fmt.Sprintf("imported[%q].info = nil", path))
|
|
}
|
|
info := ii.info
|
|
imp.importedMu.Unlock()
|
|
|
|
// Parse the in-package test files.
|
|
files, errs := imp.conf.parsePackageFiles(bp, 't')
|
|
for _, err := range errs {
|
|
info.appendError(err)
|
|
}
|
|
|
|
// The test files augmenting package P cannot be imported,
|
|
// but may import packages that import P,
|
|
// so we must disable the cycle check.
|
|
imp.addFiles(info, files, false)
|
|
}
|
|
|
|
createPkg := func(path, dir string, files []*ast.File, errs []error) {
|
|
info := imp.newPackageInfo(path, dir)
|
|
for _, err := range errs {
|
|
info.appendError(err)
|
|
}
|
|
|
|
// Ad hoc packages are non-importable,
|
|
// so no cycle check is needed.
|
|
// addFiles loads dependencies in parallel.
|
|
imp.addFiles(info, files, false)
|
|
prog.Created = append(prog.Created, info)
|
|
}
|
|
|
|
// Create packages specified by conf.CreatePkgs.
|
|
for _, cp := range conf.CreatePkgs {
|
|
files, errs := parseFiles(conf.fset(), conf.build(), nil, conf.Cwd, cp.Filenames, conf.ParserMode)
|
|
files = append(files, cp.Files...)
|
|
|
|
path := cp.Path
|
|
if path == "" {
|
|
if len(files) > 0 {
|
|
path = files[0].Name.Name
|
|
} else {
|
|
path = "(unnamed)"
|
|
}
|
|
}
|
|
|
|
dir := conf.Cwd
|
|
if len(files) > 0 && files[0].Pos().IsValid() {
|
|
dir = filepath.Dir(conf.fset().File(files[0].Pos()).Name())
|
|
}
|
|
createPkg(path, dir, files, errs)
|
|
}
|
|
|
|
// Create external test packages.
|
|
sort.Sort(byImportPath(xtestPkgs))
|
|
for _, bp := range xtestPkgs {
|
|
files, errs := imp.conf.parsePackageFiles(bp, 'x')
|
|
createPkg(bp.ImportPath+"_test", bp.Dir, files, errs)
|
|
}
|
|
|
|
// -- finishing up (sequential) ----------------------------------------
|
|
|
|
if len(prog.Imported)+len(prog.Created) == 0 {
|
|
return nil, errors.New("no initial packages were loaded")
|
|
}
|
|
|
|
// Create infos for indirectly imported packages.
|
|
// e.g. incomplete packages without syntax, loaded from export data.
|
|
for _, obj := range prog.importMap {
|
|
info := prog.AllPackages[obj]
|
|
if info == nil {
|
|
prog.AllPackages[obj] = &PackageInfo{Pkg: obj, Importable: true}
|
|
} else {
|
|
// finished
|
|
info.checker = nil
|
|
info.errorFunc = nil
|
|
}
|
|
}
|
|
|
|
if !conf.AllowErrors {
|
|
// Report errors in indirectly imported packages.
|
|
for _, info := range prog.AllPackages {
|
|
if len(info.Errors) > 0 {
|
|
errpkgs = append(errpkgs, info.Pkg.Path())
|
|
}
|
|
}
|
|
if errpkgs != nil {
|
|
var more string
|
|
if len(errpkgs) > 3 {
|
|
more = fmt.Sprintf(" and %d more", len(errpkgs)-3)
|
|
errpkgs = errpkgs[:3]
|
|
}
|
|
return nil, fmt.Errorf("couldn't load packages due to errors: %s%s",
|
|
strings.Join(errpkgs, ", "), more)
|
|
}
|
|
}
|
|
|
|
markErrorFreePackages(prog.AllPackages)
|
|
|
|
return prog, nil
|
|
}
|
|
|
|
type byImportPath []*build.Package
|
|
|
|
func (b byImportPath) Len() int { return len(b) }
|
|
func (b byImportPath) Less(i, j int) bool { return b[i].ImportPath < b[j].ImportPath }
|
|
func (b byImportPath) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
|
|
|
// markErrorFreePackages sets the TransitivelyErrorFree flag on all
|
|
// applicable packages.
|
|
func markErrorFreePackages(allPackages map[*types.Package]*PackageInfo) {
|
|
// Build the transpose of the import graph.
|
|
importedBy := make(map[*types.Package]map[*types.Package]bool)
|
|
for P := range allPackages {
|
|
for _, Q := range P.Imports() {
|
|
clients, ok := importedBy[Q]
|
|
if !ok {
|
|
clients = make(map[*types.Package]bool)
|
|
importedBy[Q] = clients
|
|
}
|
|
clients[P] = true
|
|
}
|
|
}
|
|
|
|
// Find all packages reachable from some error package.
|
|
reachable := make(map[*types.Package]bool)
|
|
var visit func(*types.Package)
|
|
visit = func(p *types.Package) {
|
|
if !reachable[p] {
|
|
reachable[p] = true
|
|
for q := range importedBy[p] {
|
|
visit(q)
|
|
}
|
|
}
|
|
}
|
|
for _, info := range allPackages {
|
|
if len(info.Errors) > 0 {
|
|
visit(info.Pkg)
|
|
}
|
|
}
|
|
|
|
// Mark the others as "transitively error-free".
|
|
for _, info := range allPackages {
|
|
if !reachable[info.Pkg] {
|
|
info.TransitivelyErrorFree = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// build returns the effective build context.
|
|
func (conf *Config) build() *build.Context {
|
|
if conf.Build != nil {
|
|
return conf.Build
|
|
}
|
|
return &build.Default
|
|
}
|
|
|
|
// parsePackageFiles enumerates the files belonging to package path,
|
|
// then loads, parses and returns them, plus a list of I/O or parse
|
|
// errors that were encountered.
|
|
//
|
|
// 'which' indicates which files to include:
|
|
// 'g': include non-test *.go source files (GoFiles + processed CgoFiles)
|
|
// 't': include in-package *_test.go source files (TestGoFiles)
|
|
// 'x': include external *_test.go source files. (XTestGoFiles)
|
|
//
|
|
func (conf *Config) parsePackageFiles(bp *build.Package, which rune) ([]*ast.File, []error) {
|
|
if bp.ImportPath == "unsafe" {
|
|
return nil, nil
|
|
}
|
|
var filenames []string
|
|
switch which {
|
|
case 'g':
|
|
filenames = bp.GoFiles
|
|
case 't':
|
|
filenames = bp.TestGoFiles
|
|
case 'x':
|
|
filenames = bp.XTestGoFiles
|
|
default:
|
|
panic(which)
|
|
}
|
|
|
|
files, errs := parseFiles(conf.fset(), conf.build(), conf.DisplayPath, bp.Dir, filenames, conf.ParserMode)
|
|
|
|
// Preprocess CgoFiles and parse the outputs (sequentially).
|
|
if which == 'g' && bp.CgoFiles != nil {
|
|
cgofiles, err := cgo.ProcessFiles(bp, conf.fset(), conf.DisplayPath, conf.ParserMode)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
} else {
|
|
files = append(files, cgofiles...)
|
|
}
|
|
}
|
|
|
|
return files, errs
|
|
}
|
|
|
|
// doImport imports the package denoted by path.
|
|
// It implements the types.Importer signature.
|
|
//
|
|
// It returns an error if a package could not be created
|
|
// (e.g. go/build or parse error), but type errors are reported via
|
|
// the types.Config.Error callback (the first of which is also saved
|
|
// in the package's PackageInfo).
|
|
//
|
|
// Idempotent.
|
|
//
|
|
func (imp *importer) doImport(from *PackageInfo, to string) (*types.Package, error) {
|
|
if to == "C" {
|
|
// This should be unreachable, but ad hoc packages are
|
|
// not currently subject to cgo preprocessing.
|
|
// See https://github.com/golang/go/issues/11627.
|
|
return nil, fmt.Errorf(`the loader doesn't cgo-process ad hoc packages like %q; see Go issue 11627`,
|
|
from.Pkg.Path())
|
|
}
|
|
|
|
bp, err := imp.findPackage(to, from.dir, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// The standard unsafe package is handled specially,
|
|
// and has no PackageInfo.
|
|
if bp.ImportPath == "unsafe" {
|
|
return types.Unsafe, nil
|
|
}
|
|
|
|
// Look for the package in the cache using its canonical path.
|
|
path := bp.ImportPath
|
|
imp.importedMu.Lock()
|
|
ii := imp.imported[path]
|
|
imp.importedMu.Unlock()
|
|
if ii == nil {
|
|
panic("internal error: unexpected import: " + path)
|
|
}
|
|
if ii.info != nil {
|
|
return ii.info.Pkg, nil
|
|
}
|
|
|
|
// Import of incomplete package: this indicates a cycle.
|
|
fromPath := from.Pkg.Path()
|
|
if cycle := imp.findPath(path, fromPath); cycle != nil {
|
|
cycle = append([]string{fromPath}, cycle...)
|
|
return nil, fmt.Errorf("import cycle: %s", strings.Join(cycle, " -> "))
|
|
}
|
|
|
|
panic("internal error: import of incomplete (yet acyclic) package: " + fromPath)
|
|
}
|
|
|
|
// findPackage locates the package denoted by the importPath in the
|
|
// specified directory.
|
|
func (imp *importer) findPackage(importPath, fromDir string, mode build.ImportMode) (*build.Package, error) {
|
|
// We use a non-blocking duplicate-suppressing cache (gopl.io §9.7)
|
|
// to avoid holding the lock around FindPackage.
|
|
key := findpkgKey{importPath, fromDir, mode}
|
|
imp.findpkgMu.Lock()
|
|
v, ok := imp.findpkg[key]
|
|
if ok {
|
|
// cache hit
|
|
imp.findpkgMu.Unlock()
|
|
|
|
<-v.ready // wait for entry to become ready
|
|
} else {
|
|
// Cache miss: this goroutine becomes responsible for
|
|
// populating the map entry and broadcasting its readiness.
|
|
v = &findpkgValue{ready: make(chan struct{})}
|
|
imp.findpkg[key] = v
|
|
imp.findpkgMu.Unlock()
|
|
|
|
ioLimit <- true
|
|
v.bp, v.err = imp.conf.FindPackage(imp.conf.build(), importPath, fromDir, mode)
|
|
<-ioLimit
|
|
|
|
if _, ok := v.err.(*build.NoGoError); ok {
|
|
v.err = nil // empty directory is not an error
|
|
}
|
|
|
|
close(v.ready) // broadcast ready condition
|
|
}
|
|
return v.bp, v.err
|
|
}
|
|
|
|
// importAll loads, parses, and type-checks the specified packages in
|
|
// parallel and returns their completed importInfos in unspecified order.
|
|
//
|
|
// fromPath is the package path of the importing package, if it is
|
|
// importable, "" otherwise. It is used for cycle detection.
|
|
//
|
|
// fromDir is the directory containing the import declaration that
|
|
// caused these imports.
|
|
//
|
|
func (imp *importer) importAll(fromPath, fromDir string, imports map[string]bool, mode build.ImportMode) (infos []*PackageInfo, errors []importError) {
|
|
// TODO(adonovan): opt: do the loop in parallel once
|
|
// findPackage is non-blocking.
|
|
var pending []*importInfo
|
|
for importPath := range imports {
|
|
bp, err := imp.findPackage(importPath, fromDir, mode)
|
|
if err != nil {
|
|
errors = append(errors, importError{
|
|
path: importPath,
|
|
err: err,
|
|
})
|
|
continue
|
|
}
|
|
pending = append(pending, imp.startLoad(bp))
|
|
}
|
|
|
|
if fromPath != "" {
|
|
// We're loading a set of imports.
|
|
//
|
|
// We must record graph edges from the importing package
|
|
// to its dependencies, and check for cycles.
|
|
imp.graphMu.Lock()
|
|
deps, ok := imp.graph[fromPath]
|
|
if !ok {
|
|
deps = make(map[string]bool)
|
|
imp.graph[fromPath] = deps
|
|
}
|
|
for _, ii := range pending {
|
|
deps[ii.path] = true
|
|
}
|
|
imp.graphMu.Unlock()
|
|
}
|
|
|
|
for _, ii := range pending {
|
|
if fromPath != "" {
|
|
if cycle := imp.findPath(ii.path, fromPath); cycle != nil {
|
|
// Cycle-forming import: we must not await its
|
|
// completion since it would deadlock.
|
|
//
|
|
// We don't record the error in ii since
|
|
// the error is really associated with the
|
|
// cycle-forming edge, not the package itself.
|
|
// (Also it would complicate the
|
|
// invariants of importPath completion.)
|
|
if trace {
|
|
fmt.Fprintf(os.Stderr, "import cycle: %q\n", cycle)
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
ii.awaitCompletion()
|
|
infos = append(infos, ii.info)
|
|
}
|
|
|
|
return infos, errors
|
|
}
|
|
|
|
// findPath returns an arbitrary path from 'from' to 'to' in the import
|
|
// graph, or nil if there was none.
|
|
func (imp *importer) findPath(from, to string) []string {
|
|
imp.graphMu.Lock()
|
|
defer imp.graphMu.Unlock()
|
|
|
|
seen := make(map[string]bool)
|
|
var search func(stack []string, importPath string) []string
|
|
search = func(stack []string, importPath string) []string {
|
|
if !seen[importPath] {
|
|
seen[importPath] = true
|
|
stack = append(stack, importPath)
|
|
if importPath == to {
|
|
return stack
|
|
}
|
|
for x := range imp.graph[importPath] {
|
|
if p := search(stack, x); p != nil {
|
|
return p
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
return search(make([]string, 0, 20), from)
|
|
}
|
|
|
|
// startLoad initiates the loading, parsing and type-checking of the
|
|
// specified package and its dependencies, if it has not already begun.
|
|
//
|
|
// It returns an importInfo, not necessarily in a completed state. The
|
|
// caller must call awaitCompletion() before accessing its info field.
|
|
//
|
|
// startLoad is concurrency-safe and idempotent.
|
|
//
|
|
func (imp *importer) startLoad(bp *build.Package) *importInfo {
|
|
path := bp.ImportPath
|
|
imp.importedMu.Lock()
|
|
ii, ok := imp.imported[path]
|
|
if !ok {
|
|
ii = &importInfo{path: path, complete: make(chan struct{})}
|
|
imp.imported[path] = ii
|
|
go func() {
|
|
info := imp.load(bp)
|
|
ii.Complete(info)
|
|
}()
|
|
}
|
|
imp.importedMu.Unlock()
|
|
|
|
return ii
|
|
}
|
|
|
|
// load implements package loading by parsing Go source files
|
|
// located by go/build.
|
|
func (imp *importer) load(bp *build.Package) *PackageInfo {
|
|
info := imp.newPackageInfo(bp.ImportPath, bp.Dir)
|
|
info.Importable = true
|
|
files, errs := imp.conf.parsePackageFiles(bp, 'g')
|
|
for _, err := range errs {
|
|
info.appendError(err)
|
|
}
|
|
|
|
imp.addFiles(info, files, true)
|
|
|
|
imp.progMu.Lock()
|
|
imp.prog.importMap[bp.ImportPath] = info.Pkg
|
|
imp.progMu.Unlock()
|
|
|
|
return info
|
|
}
|
|
|
|
// addFiles adds and type-checks the specified files to info, loading
|
|
// their dependencies if needed. The order of files determines the
|
|
// package initialization order. It may be called multiple times on the
|
|
// same package. Errors are appended to the info.Errors field.
|
|
//
|
|
// cycleCheck determines whether the imports within files create
|
|
// dependency edges that should be checked for potential cycles.
|
|
//
|
|
func (imp *importer) addFiles(info *PackageInfo, files []*ast.File, cycleCheck bool) {
|
|
// Ensure the dependencies are loaded, in parallel.
|
|
var fromPath string
|
|
if cycleCheck {
|
|
fromPath = info.Pkg.Path()
|
|
}
|
|
// TODO(adonovan): opt: make the caller do scanImports.
|
|
// Callers with a build.Package can skip it.
|
|
imp.importAll(fromPath, info.dir, scanImports(files), 0)
|
|
|
|
if trace {
|
|
fmt.Fprintf(os.Stderr, "%s: start %q (%d)\n",
|
|
time.Since(imp.start), info.Pkg.Path(), len(files))
|
|
}
|
|
|
|
// Don't call checker.Files on Unsafe, even with zero files,
|
|
// because it would mutate the package, which is a global.
|
|
if info.Pkg == types.Unsafe {
|
|
if len(files) > 0 {
|
|
panic(`"unsafe" package contains unexpected files`)
|
|
}
|
|
} else {
|
|
// Ignore the returned (first) error since we
|
|
// already collect them all in the PackageInfo.
|
|
info.checker.Files(files)
|
|
info.Files = append(info.Files, files...)
|
|
}
|
|
|
|
if imp.conf.AfterTypeCheck != nil {
|
|
imp.conf.AfterTypeCheck(info, files)
|
|
}
|
|
|
|
if trace {
|
|
fmt.Fprintf(os.Stderr, "%s: stop %q\n",
|
|
time.Since(imp.start), info.Pkg.Path())
|
|
}
|
|
}
|
|
|
|
func (imp *importer) newPackageInfo(path, dir string) *PackageInfo {
|
|
var pkg *types.Package
|
|
if path == "unsafe" {
|
|
pkg = types.Unsafe
|
|
} else {
|
|
pkg = types.NewPackage(path, "")
|
|
}
|
|
info := &PackageInfo{
|
|
Pkg: pkg,
|
|
Info: types.Info{
|
|
Types: make(map[ast.Expr]types.TypeAndValue),
|
|
Defs: make(map[*ast.Ident]types.Object),
|
|
Uses: 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),
|
|
},
|
|
errorFunc: imp.conf.TypeChecker.Error,
|
|
dir: dir,
|
|
}
|
|
|
|
// Copy the types.Config so we can vary it across PackageInfos.
|
|
tc := imp.conf.TypeChecker
|
|
tc.IgnoreFuncBodies = false
|
|
if f := imp.conf.TypeCheckFuncBodies; f != nil {
|
|
tc.IgnoreFuncBodies = !f(path)
|
|
}
|
|
tc.Importer = closure{imp, info}
|
|
tc.Error = info.appendError // appendError wraps the user's Error function
|
|
|
|
info.checker = types.NewChecker(&tc, imp.conf.fset(), pkg, &info.Info)
|
|
imp.progMu.Lock()
|
|
imp.prog.AllPackages[pkg] = info
|
|
imp.progMu.Unlock()
|
|
return info
|
|
}
|
|
|
|
type closure struct {
|
|
imp *importer
|
|
info *PackageInfo
|
|
}
|
|
|
|
func (c closure) Import(to string) (*types.Package, error) { return c.imp.doImport(c.info, to) }
|