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