github.com/jd-ly/tools@v0.5.7/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  	"fmt"
    10  	"go/types"
    11  	"io/ioutil"
    12  	"os"
    13  	"path/filepath"
    14  	"sort"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/jd-ly/tools/go/packages"
    19  	"github.com/jd-ly/tools/internal/event"
    20  	"github.com/jd-ly/tools/internal/gocommand"
    21  	"github.com/jd-ly/tools/internal/lsp/debug/tag"
    22  	"github.com/jd-ly/tools/internal/lsp/protocol"
    23  	"github.com/jd-ly/tools/internal/lsp/source"
    24  	"github.com/jd-ly/tools/internal/memoize"
    25  	"github.com/jd-ly/tools/internal/packagesinternal"
    26  	"github.com/jd-ly/tools/internal/span"
    27  	errors "golang.org/x/xerrors"
    28  )
    29  
    30  // metadata holds package metadata extracted from a call to packages.Load.
    31  type metadata struct {
    32  	id              packageID
    33  	pkgPath         packagePath
    34  	name            packageName
    35  	goFiles         []span.URI
    36  	compiledGoFiles []span.URI
    37  	forTest         packagePath
    38  	typesSizes      types.Sizes
    39  	errors          []packages.Error
    40  	deps            []packageID
    41  	missingDeps     map[packagePath]struct{}
    42  	module          *packages.Module
    43  
    44  	// config is the *packages.Config associated with the loaded package.
    45  	config *packages.Config
    46  }
    47  
    48  // load calls packages.Load for the given scopes, updating package metadata,
    49  // import graph, and mapped files with the result.
    50  func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interface{}) error {
    51  	var query []string
    52  	var containsDir bool // for logging
    53  	for _, scope := range scopes {
    54  		switch scope := scope.(type) {
    55  		case packagePath:
    56  			if scope == "command-line-arguments" {
    57  				panic("attempted to load command-line-arguments")
    58  			}
    59  			// The only time we pass package paths is when we're doing a
    60  			// partial workspace load. In those cases, the paths came back from
    61  			// go list and should already be GOPATH-vendorized when appropriate.
    62  			query = append(query, string(scope))
    63  		case fileURI:
    64  			uri := span.URI(scope)
    65  			// Don't try to load a file that doesn't exist.
    66  			fh := s.FindFile(uri)
    67  			if fh == nil || fh.Kind() != source.Go {
    68  				continue
    69  			}
    70  			query = append(query, fmt.Sprintf("file=%s", uri.Filename()))
    71  		case moduleLoadScope:
    72  			query = append(query, fmt.Sprintf("%s/...", scope))
    73  		case viewLoadScope:
    74  			// If we are outside of GOPATH, a module, or some other known
    75  			// build system, don't load subdirectories.
    76  			if !s.ValidBuildConfiguration() {
    77  				query = append(query, "./")
    78  			} else {
    79  				query = append(query, "./...")
    80  			}
    81  		default:
    82  			panic(fmt.Sprintf("unknown scope type %T", scope))
    83  		}
    84  		switch scope.(type) {
    85  		case viewLoadScope, moduleLoadScope:
    86  			containsDir = true
    87  		}
    88  	}
    89  	if len(query) == 0 {
    90  		return nil
    91  	}
    92  	sort.Strings(query) // for determinism
    93  
    94  	ctx, done := event.Start(ctx, "cache.view.load", tag.Query.Of(query))
    95  	defer done()
    96  
    97  	flags := source.LoadWorkspace
    98  	if allowNetwork {
    99  		flags |= source.AllowNetwork
   100  	}
   101  	_, inv, cleanup, err := s.goCommandInvocation(ctx, flags, &gocommand.Invocation{
   102  		WorkingDir: s.view.rootURI.Filename(),
   103  	})
   104  	if err != nil {
   105  		return err
   106  	}
   107  
   108  	// Set a last resort deadline on packages.Load since it calls the go
   109  	// command, which may hang indefinitely if it has a bug. golang/go#42132
   110  	// and golang/go#42255 have more context.
   111  	ctx, cancel := context.WithTimeout(ctx, 15*time.Minute)
   112  	defer cancel()
   113  
   114  	cfg := s.config(ctx, inv)
   115  	pkgs, err := packages.Load(cfg, query...)
   116  	cleanup()
   117  
   118  	// If the context was canceled, return early. Otherwise, we might be
   119  	// type-checking an incomplete result. Check the context directly,
   120  	// because go/packages adds extra information to the error.
   121  	if ctx.Err() != nil {
   122  		return ctx.Err()
   123  	}
   124  	if err != nil {
   125  		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)))
   126  	} else {
   127  		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)))
   128  	}
   129  	if len(pkgs) == 0 {
   130  		if err != nil {
   131  			// Try to extract the load error into a structured error with
   132  			// diagnostics.
   133  			if criticalErr := s.parseLoadError(ctx, err); criticalErr != nil {
   134  				return criticalErr
   135  			}
   136  		} else {
   137  			err = fmt.Errorf("no packages returned")
   138  		}
   139  		return errors.Errorf("%v: %w", err, source.PackagesLoadError)
   140  	}
   141  	for _, pkg := range pkgs {
   142  		if !containsDir || s.view.Options().VerboseOutput {
   143  			event.Log(ctx, "go/packages.Load", tag.Snapshot.Of(s.ID()), tag.PackagePath.Of(pkg.PkgPath), tag.Files.Of(pkg.CompiledGoFiles))
   144  		}
   145  		// Ignore packages with no sources, since we will never be able to
   146  		// correctly invalidate that metadata.
   147  		if len(pkg.GoFiles) == 0 && len(pkg.CompiledGoFiles) == 0 {
   148  			continue
   149  		}
   150  		// Special case for the builtin package, as it has no dependencies.
   151  		if pkg.PkgPath == "builtin" {
   152  			if err := s.buildBuiltinPackage(ctx, pkg.GoFiles); err != nil {
   153  				return err
   154  			}
   155  			continue
   156  		}
   157  		// Skip test main packages.
   158  		if isTestMain(pkg, s.view.gocache) {
   159  			continue
   160  		}
   161  		// Set the metadata for this package.
   162  		m, err := s.setMetadata(ctx, packagePath(pkg.PkgPath), pkg, cfg, map[packageID]struct{}{})
   163  		if err != nil {
   164  			return err
   165  		}
   166  		if _, err := s.buildPackageHandle(ctx, m.id, s.workspaceParseMode(m.id)); err != nil {
   167  			return err
   168  		}
   169  	}
   170  	// Rebuild the import graph when the metadata is updated.
   171  	s.clearAndRebuildImportGraph()
   172  
   173  	return nil
   174  }
   175  
   176  func (s *snapshot) parseLoadError(ctx context.Context, loadErr error) *source.CriticalError {
   177  	// The error may be a result of the user's workspace layout. Check for
   178  	// a valid workspace configuration first.
   179  	if criticalErr := s.workspaceLayoutErrors(ctx, loadErr); criticalErr != nil {
   180  		return criticalErr
   181  	}
   182  	criticalErr := &source.CriticalError{
   183  		MainError: loadErr,
   184  	}
   185  	// Attempt to place diagnostics in the relevant go.mod files, if any.
   186  	for _, uri := range s.ModFiles() {
   187  		fh, err := s.GetFile(ctx, uri)
   188  		if err != nil {
   189  			continue
   190  		}
   191  		srcErr := s.extractGoCommandError(ctx, s, fh, loadErr.Error())
   192  		if srcErr == nil {
   193  			continue
   194  		}
   195  		criticalErr.ErrorList = append(criticalErr.ErrorList, srcErr)
   196  	}
   197  	return criticalErr
   198  }
   199  
   200  // workspaceLayoutErrors returns a diagnostic for every open file, as well as
   201  // an error message if there are no open files.
   202  func (s *snapshot) workspaceLayoutErrors(ctx context.Context, err error) *source.CriticalError {
   203  	// Assume the workspace is misconfigured only if we've detected an invalid
   204  	// build configuration. Currently, a valid build configuration is either a
   205  	// module at the root of the view or a GOPATH workspace.
   206  	if s.ValidBuildConfiguration() {
   207  		return nil
   208  	}
   209  	if len(s.workspace.getKnownModFiles()) == 0 {
   210  		return nil
   211  	}
   212  	// TODO(rstambler): Handle GO111MODULE=auto.
   213  	if s.view.userGo111Module != on {
   214  		return nil
   215  	}
   216  	if s.workspace.moduleSource != legacyWorkspace {
   217  		return nil
   218  	}
   219  	// The user's workspace contains go.mod files and they have
   220  	// GO111MODULE=on, so we should guide them to create a
   221  	// workspace folder for each module.
   222  
   223  	// Add a diagnostic to every open file, or return a general error if
   224  	// there aren't any.
   225  	var open []source.VersionedFileHandle
   226  	s.mu.Lock()
   227  	for _, fh := range s.files {
   228  		if s.isOpenLocked(fh.URI()) {
   229  			open = append(open, fh)
   230  		}
   231  	}
   232  	s.mu.Unlock()
   233  
   234  	msg := `gopls requires a module at the root of your workspace.
   235  You can work with multiple modules by opening each one as a workspace folder.
   236  Improvements to this workflow will be coming soon (https://github.com/golang/go/issues/32394),
   237  and you can learn more here: https://github.com/golang/go/issues/36899.`
   238  
   239  	criticalError := &source.CriticalError{
   240  		MainError: errors.New(msg),
   241  	}
   242  	if len(open) == 0 {
   243  		return criticalError
   244  	}
   245  	for _, fh := range open {
   246  		// Place the diagnostics on the package or module declarations.
   247  		var rng protocol.Range
   248  		switch fh.Kind() {
   249  		case source.Go:
   250  			if pgf, err := s.ParseGo(ctx, fh, source.ParseHeader); err == nil {
   251  				pkgDecl := span.NewRange(s.FileSet(), pgf.File.Package, pgf.File.Name.End())
   252  				if spn, err := pkgDecl.Span(); err == nil {
   253  					rng, _ = pgf.Mapper.Range(spn)
   254  				}
   255  			}
   256  		case source.Mod:
   257  			if pmf, err := s.ParseMod(ctx, fh); err == nil {
   258  				if pmf.File.Module != nil && pmf.File.Module.Syntax != nil {
   259  					rng, _ = rangeFromPositions(pmf.Mapper, pmf.File.Module.Syntax.Start, pmf.File.Module.Syntax.End)
   260  				}
   261  			}
   262  		}
   263  		criticalError.ErrorList = append(criticalError.ErrorList, &source.Error{
   264  			URI:     fh.URI(),
   265  			Range:   rng,
   266  			Kind:    source.ListError,
   267  			Message: msg,
   268  		})
   269  	}
   270  	return criticalError
   271  }
   272  
   273  type workspaceDirKey string
   274  
   275  type workspaceDirData struct {
   276  	dir string
   277  	err error
   278  }
   279  
   280  // getWorkspaceDir gets the URI for the workspace directory associated with
   281  // this snapshot. The workspace directory is a temp directory containing the
   282  // go.mod file computed from all active modules.
   283  func (s *snapshot) getWorkspaceDir(ctx context.Context) (span.URI, error) {
   284  	s.mu.Lock()
   285  	h := s.workspaceDirHandle
   286  	s.mu.Unlock()
   287  	if h != nil {
   288  		return getWorkspaceDir(ctx, h, s.generation)
   289  	}
   290  	file, err := s.workspace.modFile(ctx, s)
   291  	if err != nil {
   292  		return "", err
   293  	}
   294  	content, err := file.Format()
   295  	if err != nil {
   296  		return "", err
   297  	}
   298  	key := workspaceDirKey(hashContents(content))
   299  	s.mu.Lock()
   300  	h = s.generation.Bind(key, func(context.Context, memoize.Arg) interface{} {
   301  		tmpdir, err := ioutil.TempDir("", "gopls-workspace-mod")
   302  		if err != nil {
   303  			return &workspaceDirData{err: err}
   304  		}
   305  		filename := filepath.Join(tmpdir, "go.mod")
   306  		if err := ioutil.WriteFile(filename, content, 0644); err != nil {
   307  			os.RemoveAll(tmpdir)
   308  			return &workspaceDirData{err: err}
   309  		}
   310  		return &workspaceDirData{dir: tmpdir}
   311  	}, func(v interface{}) {
   312  		d := v.(*workspaceDirData)
   313  		if d.dir != "" {
   314  			if err := os.RemoveAll(d.dir); err != nil {
   315  				event.Error(context.Background(), "cleaning workspace dir", err)
   316  			}
   317  		}
   318  	})
   319  	s.workspaceDirHandle = h
   320  	s.mu.Unlock()
   321  	return getWorkspaceDir(ctx, h, s.generation)
   322  }
   323  
   324  func getWorkspaceDir(ctx context.Context, h *memoize.Handle, g *memoize.Generation) (span.URI, error) {
   325  	v, err := h.Get(ctx, g, nil)
   326  	if err != nil {
   327  		return "", err
   328  	}
   329  	return span.URIFromPath(v.(*workspaceDirData).dir), nil
   330  }
   331  
   332  // setMetadata extracts metadata from pkg and records it in s. It
   333  // recurses through pkg.Imports to ensure that metadata exists for all
   334  // dependencies.
   335  func (s *snapshot) setMetadata(ctx context.Context, pkgPath packagePath, pkg *packages.Package, cfg *packages.Config, seen map[packageID]struct{}) (*metadata, error) {
   336  	id := packageID(pkg.ID)
   337  	if _, ok := seen[id]; ok {
   338  		return nil, errors.Errorf("import cycle detected: %q", id)
   339  	}
   340  	// Recreate the metadata rather than reusing it to avoid locking.
   341  	m := &metadata{
   342  		id:         id,
   343  		pkgPath:    pkgPath,
   344  		name:       packageName(pkg.Name),
   345  		forTest:    packagePath(packagesinternal.GetForTest(pkg)),
   346  		typesSizes: pkg.TypesSizes,
   347  		errors:     pkg.Errors,
   348  		config:     cfg,
   349  		module:     pkg.Module,
   350  	}
   351  
   352  	for _, filename := range pkg.CompiledGoFiles {
   353  		uri := span.URIFromPath(filename)
   354  		m.compiledGoFiles = append(m.compiledGoFiles, uri)
   355  		s.addID(uri, m.id)
   356  	}
   357  	for _, filename := range pkg.GoFiles {
   358  		uri := span.URIFromPath(filename)
   359  		m.goFiles = append(m.goFiles, uri)
   360  		s.addID(uri, m.id)
   361  	}
   362  
   363  	// TODO(rstambler): is this still necessary?
   364  	copied := map[packageID]struct{}{
   365  		id: {},
   366  	}
   367  	for k, v := range seen {
   368  		copied[k] = v
   369  	}
   370  	for importPath, importPkg := range pkg.Imports {
   371  		importPkgPath := packagePath(importPath)
   372  		importID := packageID(importPkg.ID)
   373  
   374  		m.deps = append(m.deps, importID)
   375  
   376  		// Don't remember any imports with significant errors.
   377  		if importPkgPath != "unsafe" && len(importPkg.CompiledGoFiles) == 0 {
   378  			if m.missingDeps == nil {
   379  				m.missingDeps = make(map[packagePath]struct{})
   380  			}
   381  			m.missingDeps[importPkgPath] = struct{}{}
   382  			continue
   383  		}
   384  		if s.getMetadata(importID) == nil {
   385  			if _, err := s.setMetadata(ctx, importPkgPath, importPkg, cfg, copied); err != nil {
   386  				event.Error(ctx, "error in dependency", err)
   387  			}
   388  		}
   389  	}
   390  
   391  	// Add the metadata to the cache.
   392  	s.mu.Lock()
   393  	defer s.mu.Unlock()
   394  
   395  	// TODO: We should make sure not to set duplicate metadata,
   396  	// and instead panic here. This can be done by making sure not to
   397  	// reset metadata information for packages we've already seen.
   398  	if original, ok := s.metadata[m.id]; ok {
   399  		m = original
   400  	} else {
   401  		s.metadata[m.id] = m
   402  	}
   403  
   404  	// Set the workspace packages. If any of the package's files belong to the
   405  	// view, then the package may be a workspace package.
   406  	for _, uri := range append(m.compiledGoFiles, m.goFiles...) {
   407  		if !s.view.contains(uri) {
   408  			continue
   409  		}
   410  
   411  		// The package's files are in this view. It may be a workspace package.
   412  		if strings.Contains(string(uri), "/vendor/") {
   413  			// Vendored packages are not likely to be interesting to the user.
   414  			continue
   415  		}
   416  
   417  		switch {
   418  		case m.forTest == "":
   419  			// A normal package.
   420  			s.workspacePackages[m.id] = pkgPath
   421  		case m.forTest == m.pkgPath, m.forTest+"_test" == m.pkgPath:
   422  			// The test variant of some workspace package or its x_test.
   423  			// To load it, we need to load the non-test variant with -test.
   424  			s.workspacePackages[m.id] = m.forTest
   425  		default:
   426  			// A test variant of some intermediate package. We don't care about it.
   427  		}
   428  	}
   429  	return m, nil
   430  }
   431  
   432  func isTestMain(pkg *packages.Package, gocache string) bool {
   433  	// Test mains must have an import path that ends with ".test".
   434  	if !strings.HasSuffix(pkg.PkgPath, ".test") {
   435  		return false
   436  	}
   437  	// Test main packages are always named "main".
   438  	if pkg.Name != "main" {
   439  		return false
   440  	}
   441  	// Test mains always have exactly one GoFile that is in the build cache.
   442  	if len(pkg.GoFiles) > 1 {
   443  		return false
   444  	}
   445  	if !strings.HasPrefix(pkg.GoFiles[0], gocache) {
   446  		return false
   447  	}
   448  	return true
   449  }