github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/internal/lsp/cache/load.go (about)

     1  // Copyright 2019 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package cache
     6  
     7  import (
     8  	"context"
     9  	"crypto/sha256"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"os"
    13  	"path/filepath"
    14  	"sort"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/jhump/golang-x-tools/go/packages"
    19  	"github.com/jhump/golang-x-tools/internal/event"
    20  	"github.com/jhump/golang-x-tools/internal/gocommand"
    21  	"github.com/jhump/golang-x-tools/internal/lsp/debug/tag"
    22  	"github.com/jhump/golang-x-tools/internal/lsp/protocol"
    23  	"github.com/jhump/golang-x-tools/internal/lsp/source"
    24  	"github.com/jhump/golang-x-tools/internal/memoize"
    25  	"github.com/jhump/golang-x-tools/internal/packagesinternal"
    26  	"github.com/jhump/golang-x-tools/internal/span"
    27  	errors "golang.org/x/xerrors"
    28  )
    29  
    30  // load calls packages.Load for the given scopes, updating package metadata,
    31  // import graph, and mapped files with the result.
    32  func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interface{}) (err error) {
    33  	var query []string
    34  	var containsDir bool // for logging
    35  	for _, scope := range scopes {
    36  		if !s.shouldLoad(scope) {
    37  			continue
    38  		}
    39  		// Unless the context was canceled, set "shouldLoad" to false for all
    40  		// of the metadata we attempted to load.
    41  		defer func() {
    42  			if errors.Is(err, context.Canceled) {
    43  				return
    44  			}
    45  			s.clearShouldLoad(scope)
    46  		}()
    47  		switch scope := scope.(type) {
    48  		case PackagePath:
    49  			if source.IsCommandLineArguments(string(scope)) {
    50  				panic("attempted to load command-line-arguments")
    51  			}
    52  			// The only time we pass package paths is when we're doing a
    53  			// partial workspace load. In those cases, the paths came back from
    54  			// go list and should already be GOPATH-vendorized when appropriate.
    55  			query = append(query, string(scope))
    56  		case fileURI:
    57  			uri := span.URI(scope)
    58  			// Don't try to load a file that doesn't exist.
    59  			fh := s.FindFile(uri)
    60  			if fh == nil || s.View().FileKind(fh) != source.Go {
    61  				continue
    62  			}
    63  			query = append(query, fmt.Sprintf("file=%s", uri.Filename()))
    64  		case moduleLoadScope:
    65  			switch scope {
    66  			case "std", "cmd":
    67  				query = append(query, string(scope))
    68  			default:
    69  				query = append(query, fmt.Sprintf("%s/...", scope))
    70  			}
    71  		case viewLoadScope:
    72  			// If we are outside of GOPATH, a module, or some other known
    73  			// build system, don't load subdirectories.
    74  			if !s.ValidBuildConfiguration() {
    75  				query = append(query, "./")
    76  			} else {
    77  				query = append(query, "./...")
    78  			}
    79  		default:
    80  			panic(fmt.Sprintf("unknown scope type %T", scope))
    81  		}
    82  		switch scope.(type) {
    83  		case viewLoadScope, moduleLoadScope:
    84  			containsDir = true
    85  		}
    86  	}
    87  	if len(query) == 0 {
    88  		return nil
    89  	}
    90  	sort.Strings(query) // for determinism
    91  
    92  	if s.view.Options().VerboseWorkDoneProgress {
    93  		work := s.view.session.progress.Start(ctx, "Load", fmt.Sprintf("Loading query=%s", query), nil, nil)
    94  		defer func() {
    95  			work.End("Done.")
    96  		}()
    97  	}
    98  
    99  	ctx, done := event.Start(ctx, "cache.view.load", tag.Query.Of(query))
   100  	defer done()
   101  
   102  	flags := source.LoadWorkspace
   103  	if allowNetwork {
   104  		flags |= source.AllowNetwork
   105  	}
   106  	_, inv, cleanup, err := s.goCommandInvocation(ctx, flags, &gocommand.Invocation{
   107  		WorkingDir: s.view.rootURI.Filename(),
   108  	})
   109  	if err != nil {
   110  		return err
   111  	}
   112  
   113  	// Set a last resort deadline on packages.Load since it calls the go
   114  	// command, which may hang indefinitely if it has a bug. golang/go#42132
   115  	// and golang/go#42255 have more context.
   116  	ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
   117  	defer cancel()
   118  
   119  	cfg := s.config(ctx, inv)
   120  	pkgs, err := packages.Load(cfg, query...)
   121  	cleanup()
   122  
   123  	// If the context was canceled, return early. Otherwise, we might be
   124  	// type-checking an incomplete result. Check the context directly,
   125  	// because go/packages adds extra information to the error.
   126  	if ctx.Err() != nil {
   127  		return ctx.Err()
   128  	}
   129  	if err != nil {
   130  		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)))
   131  	} else {
   132  		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)))
   133  	}
   134  	if len(pkgs) == 0 {
   135  		if err == nil {
   136  			err = fmt.Errorf("no packages returned")
   137  		}
   138  		return errors.Errorf("%v: %w", err, source.PackagesLoadError)
   139  	}
   140  	for _, pkg := range pkgs {
   141  		if !containsDir || s.view.Options().VerboseOutput {
   142  			event.Log(ctx, "go/packages.Load",
   143  				tag.Snapshot.Of(s.ID()),
   144  				tag.Package.Of(pkg.ID),
   145  				tag.Files.Of(pkg.CompiledGoFiles))
   146  		}
   147  		// Ignore packages with no sources, since we will never be able to
   148  		// correctly invalidate that metadata.
   149  		if len(pkg.GoFiles) == 0 && len(pkg.CompiledGoFiles) == 0 {
   150  			continue
   151  		}
   152  		// Special case for the builtin package, as it has no dependencies.
   153  		if pkg.PkgPath == "builtin" {
   154  			if len(pkg.GoFiles) != 1 {
   155  				return errors.Errorf("only expected 1 file for builtin, got %v", len(pkg.GoFiles))
   156  			}
   157  			s.setBuiltin(pkg.GoFiles[0])
   158  			continue
   159  		}
   160  		// Skip test main packages.
   161  		if isTestMain(pkg, s.view.gocache) {
   162  			continue
   163  		}
   164  		// Skip filtered packages. They may be added anyway if they're
   165  		// dependencies of non-filtered packages.
   166  		if s.view.allFilesExcluded(pkg) {
   167  			continue
   168  		}
   169  		// Set the metadata for this package.
   170  		s.mu.Lock()
   171  		m, err := s.setMetadataLocked(ctx, PackagePath(pkg.PkgPath), pkg, cfg, query, map[PackageID]struct{}{})
   172  		s.mu.Unlock()
   173  		if err != nil {
   174  			return err
   175  		}
   176  		if _, err := s.buildPackageHandle(ctx, m.ID, s.workspaceParseMode(m.ID)); err != nil {
   177  			return err
   178  		}
   179  	}
   180  	// Rebuild the import graph when the metadata is updated.
   181  	s.clearAndRebuildImportGraph()
   182  
   183  	return nil
   184  }
   185  
   186  // workspaceLayoutErrors returns a diagnostic for every open file, as well as
   187  // an error message if there are no open files.
   188  func (s *snapshot) workspaceLayoutError(ctx context.Context) *source.CriticalError {
   189  	if len(s.workspace.getKnownModFiles()) == 0 {
   190  		return nil
   191  	}
   192  	if s.view.userGo111Module == off {
   193  		return nil
   194  	}
   195  	if s.workspace.moduleSource != legacyWorkspace {
   196  		return nil
   197  	}
   198  	// If the user has one module per view, there is nothing to warn about.
   199  	if s.ValidBuildConfiguration() && len(s.workspace.getKnownModFiles()) == 1 {
   200  		return nil
   201  	}
   202  
   203  	// Apply diagnostics about the workspace configuration to relevant open
   204  	// files.
   205  	openFiles := s.openFiles()
   206  
   207  	// If the snapshot does not have a valid build configuration, it may be
   208  	// that the user has opened a directory that contains multiple modules.
   209  	// Check for that an warn about it.
   210  	if !s.ValidBuildConfiguration() {
   211  		msg := `gopls requires a module at the root of your workspace.
   212  You can work with multiple modules by opening each one as a workspace folder.
   213  Improvements to this workflow will be coming soon, and you can learn more here:
   214  https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`
   215  		return &source.CriticalError{
   216  			MainError: errors.Errorf(msg),
   217  			DiagList:  s.applyCriticalErrorToFiles(ctx, msg, openFiles),
   218  		}
   219  	}
   220  
   221  	// If the user has one active go.mod file, they may still be editing files
   222  	// in nested modules. Check the module of each open file and add warnings
   223  	// that the nested module must be opened as a workspace folder.
   224  	if len(s.workspace.getActiveModFiles()) == 1 {
   225  		// Get the active root go.mod file to compare against.
   226  		var rootModURI span.URI
   227  		for uri := range s.workspace.getActiveModFiles() {
   228  			rootModURI = uri
   229  		}
   230  		nestedModules := map[string][]source.VersionedFileHandle{}
   231  		for _, fh := range openFiles {
   232  			modURI := moduleForURI(s.workspace.knownModFiles, fh.URI())
   233  			if modURI != rootModURI {
   234  				modDir := filepath.Dir(modURI.Filename())
   235  				nestedModules[modDir] = append(nestedModules[modDir], fh)
   236  			}
   237  		}
   238  		// Add a diagnostic to each file in a nested module to mark it as
   239  		// "orphaned". Don't show a general diagnostic in the progress bar,
   240  		// because the user may still want to edit a file in a nested module.
   241  		var srcDiags []*source.Diagnostic
   242  		for modDir, uris := range nestedModules {
   243  			msg := fmt.Sprintf(`This file is in %s, which is a nested module in the %s module.
   244  gopls currently requires one module per workspace folder.
   245  Please open %s as a separate workspace folder.
   246  You can learn more here: https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.
   247  `, modDir, filepath.Dir(rootModURI.Filename()), modDir)
   248  			srcDiags = append(srcDiags, s.applyCriticalErrorToFiles(ctx, msg, uris)...)
   249  		}
   250  		if len(srcDiags) != 0 {
   251  			return &source.CriticalError{
   252  				MainError: errors.Errorf(`You are working in a nested module.
   253  Please open it as a separate workspace folder. Learn more:
   254  https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`),
   255  				DiagList: srcDiags,
   256  			}
   257  		}
   258  	}
   259  	return nil
   260  }
   261  
   262  func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, files []source.VersionedFileHandle) []*source.Diagnostic {
   263  	var srcDiags []*source.Diagnostic
   264  	for _, fh := range files {
   265  		// Place the diagnostics on the package or module declarations.
   266  		var rng protocol.Range
   267  		switch s.view.FileKind(fh) {
   268  		case source.Go:
   269  			if pgf, err := s.ParseGo(ctx, fh, source.ParseHeader); err == nil {
   270  				pkgDecl := span.NewRange(s.FileSet(), pgf.File.Package, pgf.File.Name.End())
   271  				if spn, err := pkgDecl.Span(); err == nil {
   272  					rng, _ = pgf.Mapper.Range(spn)
   273  				}
   274  			}
   275  		case source.Mod:
   276  			if pmf, err := s.ParseMod(ctx, fh); err == nil {
   277  				if pmf.File.Module != nil && pmf.File.Module.Syntax != nil {
   278  					rng, _ = rangeFromPositions(pmf.Mapper, pmf.File.Module.Syntax.Start, pmf.File.Module.Syntax.End)
   279  				}
   280  			}
   281  		}
   282  		srcDiags = append(srcDiags, &source.Diagnostic{
   283  			URI:      fh.URI(),
   284  			Range:    rng,
   285  			Severity: protocol.SeverityError,
   286  			Source:   source.ListError,
   287  			Message:  msg,
   288  		})
   289  	}
   290  	return srcDiags
   291  }
   292  
   293  type workspaceDirKey string
   294  
   295  type workspaceDirData struct {
   296  	dir string
   297  	err error
   298  }
   299  
   300  // getWorkspaceDir gets the URI for the workspace directory associated with
   301  // this snapshot. The workspace directory is a temp directory containing the
   302  // go.mod file computed from all active modules.
   303  func (s *snapshot) getWorkspaceDir(ctx context.Context) (span.URI, error) {
   304  	s.mu.Lock()
   305  	h := s.workspaceDirHandle
   306  	s.mu.Unlock()
   307  	if h != nil {
   308  		return getWorkspaceDir(ctx, h, s.generation)
   309  	}
   310  	file, err := s.workspace.modFile(ctx, s)
   311  	if err != nil {
   312  		return "", err
   313  	}
   314  	hash := sha256.New()
   315  	modContent, err := file.Format()
   316  	if err != nil {
   317  		return "", err
   318  	}
   319  	sumContent, err := s.workspace.sumFile(ctx, s)
   320  	if err != nil {
   321  		return "", err
   322  	}
   323  	hash.Write(modContent)
   324  	hash.Write(sumContent)
   325  	key := workspaceDirKey(hash.Sum(nil))
   326  	s.mu.Lock()
   327  	h = s.generation.Bind(key, func(context.Context, memoize.Arg) interface{} {
   328  		tmpdir, err := ioutil.TempDir("", "gopls-workspace-mod")
   329  		if err != nil {
   330  			return &workspaceDirData{err: err}
   331  		}
   332  
   333  		for name, content := range map[string][]byte{
   334  			"go.mod": modContent,
   335  			"go.sum": sumContent,
   336  		} {
   337  			filename := filepath.Join(tmpdir, name)
   338  			if err := ioutil.WriteFile(filename, content, 0644); err != nil {
   339  				os.RemoveAll(tmpdir)
   340  				return &workspaceDirData{err: err}
   341  			}
   342  		}
   343  
   344  		return &workspaceDirData{dir: tmpdir}
   345  	}, func(v interface{}) {
   346  		d := v.(*workspaceDirData)
   347  		if d.dir != "" {
   348  			if err := os.RemoveAll(d.dir); err != nil {
   349  				event.Error(context.Background(), "cleaning workspace dir", err)
   350  			}
   351  		}
   352  	})
   353  	s.workspaceDirHandle = h
   354  	s.mu.Unlock()
   355  	return getWorkspaceDir(ctx, h, s.generation)
   356  }
   357  
   358  func getWorkspaceDir(ctx context.Context, h *memoize.Handle, g *memoize.Generation) (span.URI, error) {
   359  	v, err := h.Get(ctx, g, nil)
   360  	if err != nil {
   361  		return "", err
   362  	}
   363  	return span.URIFromPath(v.(*workspaceDirData).dir), nil
   364  }
   365  
   366  // setMetadataLocked extracts metadata from pkg and records it in s. It
   367  // recurses through pkg.Imports to ensure that metadata exists for all
   368  // dependencies.
   369  func (s *snapshot) setMetadataLocked(ctx context.Context, pkgPath PackagePath, pkg *packages.Package, cfg *packages.Config, query []string, seen map[PackageID]struct{}) (*Metadata, error) {
   370  	id := PackageID(pkg.ID)
   371  	if source.IsCommandLineArguments(pkg.ID) {
   372  		suffix := ":" + strings.Join(query, ",")
   373  		id = PackageID(string(id) + suffix)
   374  		pkgPath = PackagePath(string(pkgPath) + suffix)
   375  	}
   376  	if _, ok := seen[id]; ok {
   377  		return nil, errors.Errorf("import cycle detected: %q", id)
   378  	}
   379  	// Recreate the metadata rather than reusing it to avoid locking.
   380  	m := &Metadata{
   381  		ID:         id,
   382  		PkgPath:    pkgPath,
   383  		Name:       PackageName(pkg.Name),
   384  		ForTest:    PackagePath(packagesinternal.GetForTest(pkg)),
   385  		TypesSizes: pkg.TypesSizes,
   386  		Config:     cfg,
   387  		Module:     pkg.Module,
   388  		depsErrors: packagesinternal.GetDepsErrors(pkg),
   389  	}
   390  
   391  	for _, err := range pkg.Errors {
   392  		// Filter out parse errors from go list. We'll get them when we
   393  		// actually parse, and buggy overlay support may generate spurious
   394  		// errors. (See TestNewModule_Issue38207.)
   395  		if strings.Contains(err.Msg, "expected '") {
   396  			continue
   397  		}
   398  		m.Errors = append(m.Errors, err)
   399  	}
   400  
   401  	uris := map[span.URI]struct{}{}
   402  	for _, filename := range pkg.CompiledGoFiles {
   403  		uri := span.URIFromPath(filename)
   404  		m.CompiledGoFiles = append(m.CompiledGoFiles, uri)
   405  		uris[uri] = struct{}{}
   406  	}
   407  	for _, filename := range pkg.GoFiles {
   408  		uri := span.URIFromPath(filename)
   409  		m.GoFiles = append(m.GoFiles, uri)
   410  		uris[uri] = struct{}{}
   411  	}
   412  	s.updateIDForURIsLocked(id, uris)
   413  
   414  	// TODO(rstambler): is this still necessary?
   415  	copied := map[PackageID]struct{}{
   416  		id: {},
   417  	}
   418  	for k, v := range seen {
   419  		copied[k] = v
   420  	}
   421  	for importPath, importPkg := range pkg.Imports {
   422  		importPkgPath := PackagePath(importPath)
   423  		importID := PackageID(importPkg.ID)
   424  
   425  		m.Deps = append(m.Deps, importID)
   426  
   427  		// Don't remember any imports with significant errors.
   428  		if importPkgPath != "unsafe" && len(importPkg.CompiledGoFiles) == 0 {
   429  			if m.MissingDeps == nil {
   430  				m.MissingDeps = make(map[PackagePath]struct{})
   431  			}
   432  			m.MissingDeps[importPkgPath] = struct{}{}
   433  			continue
   434  		}
   435  		if s.noValidMetadataForIDLocked(importID) {
   436  			if _, err := s.setMetadataLocked(ctx, importPkgPath, importPkg, cfg, query, copied); err != nil {
   437  				event.Error(ctx, "error in dependency", err)
   438  			}
   439  		}
   440  	}
   441  
   442  	// Add the metadata to the cache.
   443  
   444  	// If we've already set the metadata for this snapshot, reuse it.
   445  	if original, ok := s.metadata[m.ID]; ok && original.Valid {
   446  		// Since we've just reloaded, clear out shouldLoad.
   447  		original.ShouldLoad = false
   448  		m = original.Metadata
   449  	} else {
   450  		s.metadata[m.ID] = &KnownMetadata{
   451  			Metadata: m,
   452  			Valid:    true,
   453  		}
   454  		// Invalidate any packages we may have associated with this metadata.
   455  		for _, mode := range []source.ParseMode{source.ParseHeader, source.ParseExported, source.ParseFull} {
   456  			key := packageKey{mode, m.ID}
   457  			delete(s.packages, key)
   458  		}
   459  	}
   460  
   461  	// Set the workspace packages. If any of the package's files belong to the
   462  	// view, then the package may be a workspace package.
   463  	for _, uri := range append(m.CompiledGoFiles, m.GoFiles...) {
   464  		if !s.view.contains(uri) {
   465  			continue
   466  		}
   467  
   468  		// The package's files are in this view. It may be a workspace package.
   469  		if strings.Contains(string(uri), "/vendor/") {
   470  			// Vendored packages are not likely to be interesting to the user.
   471  			continue
   472  		}
   473  
   474  		switch {
   475  		case m.ForTest == "":
   476  			// A normal package.
   477  			s.workspacePackages[m.ID] = pkgPath
   478  		case m.ForTest == m.PkgPath, m.ForTest+"_test" == m.PkgPath:
   479  			// The test variant of some workspace package or its x_test.
   480  			// To load it, we need to load the non-test variant with -test.
   481  			s.workspacePackages[m.ID] = m.ForTest
   482  		default:
   483  			// A test variant of some intermediate package. We don't care about it.
   484  			m.IsIntermediateTestVariant = true
   485  		}
   486  	}
   487  	return m, nil
   488  }
   489  
   490  func isTestMain(pkg *packages.Package, gocache string) bool {
   491  	// Test mains must have an import path that ends with ".test".
   492  	if !strings.HasSuffix(pkg.PkgPath, ".test") {
   493  		return false
   494  	}
   495  	// Test main packages are always named "main".
   496  	if pkg.Name != "main" {
   497  		return false
   498  	}
   499  	// Test mains always have exactly one GoFile that is in the build cache.
   500  	if len(pkg.GoFiles) > 1 {
   501  		return false
   502  	}
   503  	if !source.InDir(gocache, pkg.GoFiles[0]) {
   504  		return false
   505  	}
   506  	return true
   507  }