--- /dev/null
+// Copyright 2019 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 cache
+
+import (
+ "context"
+ "fmt"
+ "go/types"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/tools/internal/event"
+ "golang.org/x/tools/internal/lsp/debug/tag"
+ "golang.org/x/tools/internal/lsp/source"
+ "golang.org/x/tools/internal/packagesinternal"
+ "golang.org/x/tools/internal/span"
+ errors "golang.org/x/xerrors"
+)
+
+// metadata holds package metadata extracted from a call to packages.Load.
+type metadata struct {
+ id packageID
+ pkgPath packagePath
+ name packageName
+ goFiles []span.URI
+ compiledGoFiles []span.URI
+ forTest packagePath
+ typesSizes types.Sizes
+ errors []packages.Error
+ deps []packageID
+ missingDeps map[packagePath]struct{}
+ module *packages.Module
+
+ // config is the *packages.Config associated with the loaded package.
+ config *packages.Config
+}
+
+// load calls packages.Load for the given scopes, updating package metadata,
+// import graph, and mapped files with the result.
+func (s *snapshot) load(ctx context.Context, scopes ...interface{}) error {
+ var query []string
+ var containsDir bool // for logging
+ for _, scope := range scopes {
+ switch scope := scope.(type) {
+ case packagePath:
+ if scope == "command-line-arguments" {
+ panic("attempted to load command-line-arguments")
+ }
+ // The only time we pass package paths is when we're doing a
+ // partial workspace load. In those cases, the paths came back from
+ // go list and should already be GOPATH-vendorized when appropriate.
+ query = append(query, string(scope))
+ case fileURI:
+ uri := span.URI(scope)
+ // Don't try to load a file that doesn't exist.
+ fh := s.FindFile(uri)
+ if fh == nil || fh.Kind() != source.Go {
+ continue
+ }
+ query = append(query, fmt.Sprintf("file=%s", uri.Filename()))
+ case moduleLoadScope:
+ query = append(query, fmt.Sprintf("%s/...", scope))
+ case viewLoadScope:
+ // If we are outside of GOPATH, a module, or some other known
+ // build system, don't load subdirectories.
+ if !s.ValidBuildConfiguration() {
+ query = append(query, "./")
+ } else {
+ query = append(query, "./...")
+ }
+ default:
+ panic(fmt.Sprintf("unknown scope type %T", scope))
+ }
+ switch scope.(type) {
+ case viewLoadScope:
+ containsDir = true
+ }
+ }
+ if len(query) == 0 {
+ return nil
+ }
+ sort.Strings(query) // for determinism
+
+ ctx, done := event.Start(ctx, "cache.view.load", tag.Query.Of(query))
+ defer done()
+
+ cleanup := func() {}
+ wdir := s.view.rootURI.Filename()
+
+ var modFile string
+ var modURI span.URI
+ var modContent []byte
+ switch {
+ case s.workspaceMode()&usesWorkspaceModule != 0:
+ var (
+ tmpDir span.URI
+ err error
+ )
+ tmpDir, cleanup, err = s.tempWorkspaceModule(ctx)
+ if err != nil {
+ return err
+ }
+ wdir = tmpDir.Filename()
+ modURI = span.URIFromPath(filepath.Join(wdir, "go.mod"))
+ modContent, err = ioutil.ReadFile(modURI.Filename())
+ if err != nil {
+ return err
+ }
+ case s.workspaceMode()&tempModfile != 0:
+ // -modfile is unsupported when there are > 1 modules in the workspace.
+ if len(s.modules) != 1 {
+ panic(fmt.Sprintf("unsupported use of -modfile, expected 1 module, got %v", len(s.modules)))
+ }
+ var mod *moduleRoot
+ for _, m := range s.modules { // range to access the only element
+ mod = m
+ }
+ modURI = mod.modURI
+ modFH, err := s.GetFile(ctx, mod.modURI)
+ if err != nil {
+ return err
+ }
+ modContent, err = modFH.Read()
+ if err != nil {
+ return err
+ }
+ var sumFH source.FileHandle
+ if mod.sumURI != "" {
+ sumFH, err = s.GetFile(ctx, mod.sumURI)
+ if err != nil {
+ return err
+ }
+ }
+ var tmpURI span.URI
+ tmpURI, cleanup, err = tempModFile(modFH, sumFH)
+ if err != nil {
+ return err
+ }
+ modFile = tmpURI.Filename()
+ }
+
+ cfg := s.config(ctx, wdir)
+ packagesinternal.SetModFile(cfg, modFile)
+ modMod, err := s.needsModEqualsMod(ctx, modURI, modContent)
+ if err != nil {
+ return err
+ }
+ if modMod {
+ packagesinternal.SetModFlag(cfg, "mod")
+ }
+
+ pkgs, err := packages.Load(cfg, query...)
+ cleanup()
+
+ // If the context was canceled, return early. Otherwise, we might be
+ // type-checking an incomplete result. Check the context directly,
+ // because go/packages adds extra information to the error.
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+ if err != nil {
+ // Match on common error messages. This is really hacky, but I'm not sure
+ // of any better way. This can be removed when golang/go#39164 is resolved.
+ if strings.Contains(err.Error(), "inconsistent vendoring") {
+ return source.InconsistentVendoring
+ }
+ event.Error(ctx, "go/packages.Load", err, tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs)))
+ } else {
+ event.Log(ctx, "go/packages.Load", tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs)))
+ }
+ if len(pkgs) == 0 {
+ if err != nil {
+ // Try to extract the error into a diagnostic.
+ if srcErrs := s.parseLoadError(ctx, err); srcErrs != nil {
+ return srcErrs
+ }
+ } else {
+ err = fmt.Errorf("no packages returned")
+ }
+ return errors.Errorf("%v: %w", err, source.PackagesLoadError)
+ }
+ for _, pkg := range pkgs {
+ if !containsDir || s.view.Options().VerboseOutput {
+ event.Log(ctx, "go/packages.Load", tag.Snapshot.Of(s.ID()), tag.PackagePath.Of(pkg.PkgPath), tag.Files.Of(pkg.CompiledGoFiles))
+ }
+ // Ignore packages with no sources, since we will never be able to
+ // correctly invalidate that metadata.
+ if len(pkg.GoFiles) == 0 && len(pkg.CompiledGoFiles) == 0 {
+ continue
+ }
+ // Special case for the builtin package, as it has no dependencies.
+ if pkg.PkgPath == "builtin" {
+ if err := s.buildBuiltinPackage(ctx, pkg.GoFiles); err != nil {
+ return err
+ }
+ continue
+ }
+ // Skip test main packages.
+ if isTestMain(pkg, s.view.gocache) {
+ continue
+ }
+ // Set the metadata for this package.
+ m, err := s.setMetadata(ctx, packagePath(pkg.PkgPath), pkg, cfg, map[packageID]struct{}{})
+ if err != nil {
+ return err
+ }
+ if _, err := s.buildPackageHandle(ctx, m.id, s.workspaceParseMode(m.id)); err != nil {
+ return err
+ }
+ }
+ // Rebuild the import graph when the metadata is updated.
+ s.clearAndRebuildImportGraph()
+
+ return nil
+}
+
+func (s *snapshot) parseLoadError(ctx context.Context, loadErr error) *source.ErrorList {
+ var srcErrs *source.ErrorList
+ for _, uri := range s.ModFiles() {
+ fh, err := s.GetFile(ctx, uri)
+ if err != nil {
+ continue
+ }
+ srcErr := extractGoCommandError(ctx, s, fh, loadErr)
+ if srcErr == nil {
+ continue
+ }
+ if srcErrs == nil {
+ srcErrs = &source.ErrorList{}
+ }
+ *srcErrs = append(*srcErrs, srcErr)
+ }
+ return srcErrs
+}
+
+// tempWorkspaceModule creates a temporary directory for use with
+// packages.Loads that occur from within the workspace module.
+func (s *snapshot) tempWorkspaceModule(ctx context.Context) (_ span.URI, cleanup func(), err error) {
+ cleanup = func() {}
+ if s.workspaceMode()&usesWorkspaceModule == 0 {
+ return "", cleanup, nil
+ }
+ wsModuleHandle, err := s.getWorkspaceModuleHandle(ctx)
+ if err != nil {
+ return "", nil, err
+ }
+ file, err := wsModuleHandle.build(ctx, s)
+ if err != nil {
+ return "", nil, err
+ }
+ content, err := file.Format()
+ if err != nil {
+ return "", cleanup, err
+ }
+ // Create a temporary working directory for the go command that contains
+ // the workspace module file.
+ name, err := ioutil.TempDir("", "gopls-mod")
+ if err != nil {
+ return "", cleanup, err
+ }
+ cleanup = func() {
+ os.RemoveAll(name)
+ }
+ filename := filepath.Join(name, "go.mod")
+ if err := ioutil.WriteFile(filename, content, 0644); err != nil {
+ cleanup()
+ return "", cleanup, err
+ }
+ return span.URIFromPath(filepath.Dir(filename)), cleanup, nil
+}
+
+// setMetadata extracts metadata from pkg and records it in s. It
+// recurses through pkg.Imports to ensure that metadata exists for all
+// dependencies.
+func (s *snapshot) setMetadata(ctx context.Context, pkgPath packagePath, pkg *packages.Package, cfg *packages.Config, seen map[packageID]struct{}) (*metadata, error) {
+ id := packageID(pkg.ID)
+ if _, ok := seen[id]; ok {
+ return nil, errors.Errorf("import cycle detected: %q", id)
+ }
+ // Recreate the metadata rather than reusing it to avoid locking.
+ m := &metadata{
+ id: id,
+ pkgPath: pkgPath,
+ name: packageName(pkg.Name),
+ forTest: packagePath(packagesinternal.GetForTest(pkg)),
+ typesSizes: pkg.TypesSizes,
+ errors: pkg.Errors,
+ config: cfg,
+ module: pkg.Module,
+ }
+
+ for _, filename := range pkg.CompiledGoFiles {
+ uri := span.URIFromPath(filename)
+ m.compiledGoFiles = append(m.compiledGoFiles, uri)
+ s.addID(uri, m.id)
+ }
+ for _, filename := range pkg.GoFiles {
+ uri := span.URIFromPath(filename)
+ m.goFiles = append(m.goFiles, uri)
+ s.addID(uri, m.id)
+ }
+
+ // TODO(rstambler): is this still necessary?
+ copied := map[packageID]struct{}{
+ id: {},
+ }
+ for k, v := range seen {
+ copied[k] = v
+ }
+ for importPath, importPkg := range pkg.Imports {
+ importPkgPath := packagePath(importPath)
+ importID := packageID(importPkg.ID)
+
+ m.deps = append(m.deps, importID)
+
+ // Don't remember any imports with significant errors.
+ if importPkgPath != "unsafe" && len(importPkg.CompiledGoFiles) == 0 {
+ if m.missingDeps == nil {
+ m.missingDeps = make(map[packagePath]struct{})
+ }
+ m.missingDeps[importPkgPath] = struct{}{}
+ continue
+ }
+ if s.getMetadata(importID) == nil {
+ if _, err := s.setMetadata(ctx, importPkgPath, importPkg, cfg, copied); err != nil {
+ event.Error(ctx, "error in dependency", err)
+ }
+ }
+ }
+
+ // Add the metadata to the cache.
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ // TODO: We should make sure not to set duplicate metadata,
+ // and instead panic here. This can be done by making sure not to
+ // reset metadata information for packages we've already seen.
+ if original, ok := s.metadata[m.id]; ok {
+ m = original
+ } else {
+ s.metadata[m.id] = m
+ }
+
+ // Set the workspace packages. If any of the package's files belong to the
+ // view, then the package may be a workspace package.
+ for _, uri := range append(m.compiledGoFiles, m.goFiles...) {
+ if !s.view.contains(uri) {
+ continue
+ }
+
+ // The package's files are in this view. It may be a workspace package.
+ if strings.Contains(string(uri), "/vendor/") {
+ // Vendored packages are not likely to be interesting to the user.
+ continue
+ }
+
+ switch {
+ case m.forTest == "":
+ // A normal package.
+ s.workspacePackages[m.id] = pkgPath
+ case m.forTest == m.pkgPath, m.forTest+"_test" == m.pkgPath:
+ // The test variant of some workspace package or its x_test.
+ // To load it, we need to load the non-test variant with -test.
+ s.workspacePackages[m.id] = m.forTest
+ default:
+ // A test variant of some intermediate package. We don't care about it.
+ }
+ }
+ return m, nil
+}
+
+func isTestMain(pkg *packages.Package, gocache string) bool {
+ // Test mains must have an import path that ends with ".test".
+ if !strings.HasSuffix(pkg.PkgPath, ".test") {
+ return false
+ }
+ // Test main packages are always named "main".
+ if pkg.Name != "main" {
+ return false
+ }
+ // Test mains always have exactly one GoFile that is in the build cache.
+ if len(pkg.GoFiles) > 1 {
+ return false
+ }
+ if !strings.HasPrefix(pkg.GoFiles[0], gocache) {
+ return false
+ }
+ return true
+}