cuelang.org/go@v0.13.0/internal/golangorgx/gopls/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  	"bytes"
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"maps"
    13  	"path/filepath"
    14  	"sort"
    15  
    16  	"cuelang.org/go/cue/build"
    17  	"cuelang.org/go/cue/load"
    18  	"cuelang.org/go/internal/golangorgx/gopls/cache/metadata"
    19  	"cuelang.org/go/internal/golangorgx/gopls/file"
    20  	"cuelang.org/go/internal/golangorgx/gopls/protocol"
    21  	"cuelang.org/go/internal/golangorgx/gopls/util/immutable"
    22  	"cuelang.org/go/internal/golangorgx/gopls/util/pathutil"
    23  	"cuelang.org/go/internal/golangorgx/tools/event"
    24  	"golang.org/x/tools/go/packages"
    25  )
    26  
    27  var loadID uint64 // atomic identifier for loads
    28  
    29  // errNoInstances indicates that a load query returned no instances.
    30  var errNoInstances = errors.New("no instances returned")
    31  
    32  // load calls packages.Load for the given scopes, updating package metadata,
    33  // import graph, and mapped files with the result.
    34  //
    35  // The resulting error may wrap the moduleErrorMap error type, representing
    36  // errors associated with specific modules.
    37  //
    38  // If scopes contains a file scope there must be exactly one scope.
    39  func (s *Snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadScope) (err error) {
    40  	//id := atomic.AddUint64(&loadID, 1)
    41  	//eventName := fmt.Sprintf("go/packages.Load #%d", id) // unique name for logging
    42  
    43  	ctx, done := event.Start(ctx, "cache.snapshot.load")
    44  	defer done()
    45  
    46  	var insts []*build.Instance
    47  
    48  	overlays := s.buildOverlay()
    49  
    50  	for _, scope := range scopes {
    51  		switch scope := scope.(type) {
    52  		case fileLoadScope:
    53  			uri := protocol.DocumentURI(scope)
    54  			fh := s.FindFile(uri)
    55  			if fh == nil || s.FileKind(fh) != file.CUE {
    56  				// Don't try to load a file that doesn't exist, or isn't a go file.
    57  				continue
    58  			}
    59  			_, err := fh.Content()
    60  			if err != nil {
    61  				continue
    62  			}
    63  			cfg := &load.Config{
    64  				Dir:     filepath.Dir(uri.Path()),
    65  				Overlay: overlays,
    66  			}
    67  			// We use ./... because we want to load all instances that
    68  			// involve this particular file. This will include instances
    69  			// from child directories that use the same package as our
    70  			// file.
    71  			//
    72  			// TODO(ms): work out whether we can use the cfg.Package
    73  			// field to limit us to a single package, and thus do less
    74  			// work.
    75  			insts = append(insts, load.Instances([]string{"./..."}, cfg)...)
    76  
    77  		case packageLoadScope:
    78  			cfg := &load.Config{
    79  				Dir:     s.Folder().Path(),
    80  				Overlay: overlays,
    81  			}
    82  			insts = append(insts, load.Instances([]string{string(scope)}, cfg)...)
    83  
    84  		case moduleLoadScope:
    85  			cfg := &load.Config{
    86  				Module:  scope.modulePath,
    87  				Package: "*",
    88  				Dir:     scope.dir,
    89  				Overlay: overlays,
    90  			}
    91  			insts = append(insts, load.Instances([]string{"./..."}, cfg)...)
    92  
    93  		case viewLoadScope:
    94  			// We're loading the workspace
    95  			cfg := &load.Config{
    96  				Package: "*",
    97  				Dir:     s.Folder().Path(),
    98  				Overlay: overlays,
    99  			}
   100  			// TODO(ms): In gopls, if the view is adhoc mode, here they
   101  			// only load the directory and not ./... Why?!
   102  			insts = append(insts, load.Instances([]string{"./..."}, cfg)...)
   103  
   104  		default:
   105  			panic(fmt.Sprintf("unknown scope type %T", scope))
   106  		}
   107  	}
   108  
   109  	if len(insts) == 0 {
   110  		return errNoInstances
   111  	}
   112  
   113  	byImportPath := make(map[ImportPath]*build.Instance)
   114  	for _, inst := range insts {
   115  		buildMetadata(byImportPath, inst)
   116  	}
   117  
   118  	s.mu.Lock()
   119  
   120  	var files []protocol.DocumentURI // files to preload
   121  	seenFiles := make(map[protocol.DocumentURI]struct{})
   122  	updates := maps.Clone(byImportPath)
   123  	for path, inst := range byImportPath {
   124  		if _, exists := s.meta.Packages[metadata.ImportPath(inst.ImportPath)]; exists {
   125  			delete(updates, path)
   126  			continue
   127  		}
   128  		for _, path := range inst.BuildFiles {
   129  			uri := protocol.URIFromPath(path.Filename)
   130  			if _, seen := seenFiles[uri]; seen {
   131  				continue
   132  			}
   133  			seenFiles[uri] = struct{}{}
   134  			files = append(files, uri)
   135  		}
   136  		s.shouldLoad.Delete(ImportPath(inst.ImportPath))
   137  	}
   138  
   139  	s.meta = s.meta.Update(updates)
   140  
   141  	s.mu.Unlock()
   142  
   143  	s.preloadFiles(ctx, files)
   144  
   145  	return nil
   146  }
   147  
   148  func buildMetadata(byImportPath map[ImportPath]*build.Instance, inst *build.Instance) {
   149  	// Note that in cue, it is not considered that a child package
   150  	// "imports" an ancestor package with the same package
   151  	// name. E.g. cue/load.Instances of an importPath "foo/bar:x" will
   152  	// automatically find and include "foo:x" if it exists. Further, we
   153  	// will find the cue files from the parent directory in the
   154  	// child-instance's BuildFiles. But it is not the case that the
   155  	// parent package will appear in the child's Imports.
   156  	importPath := ImportPath(inst.ImportPath)
   157  	if _, seen := byImportPath[importPath]; seen {
   158  		// think: diamond imports - A imports B, A imports C. B imports
   159  		// D, C imports D. We'll see D twice then.
   160  		return
   161  	}
   162  	byImportPath[importPath] = inst
   163  	for _, inst := range inst.Imports {
   164  		buildMetadata(byImportPath, inst)
   165  	}
   166  }
   167  
   168  type moduleErrorMap struct {
   169  	errs map[string][]packages.Error // module path -> errors
   170  }
   171  
   172  func (m *moduleErrorMap) Error() string {
   173  	var paths []string // sort for stability
   174  	for path, errs := range m.errs {
   175  		if len(errs) > 0 { // should always be true, but be cautious
   176  			paths = append(paths, path)
   177  		}
   178  	}
   179  	sort.Strings(paths)
   180  
   181  	var buf bytes.Buffer
   182  	fmt.Fprintf(&buf, "%d modules have errors:\n", len(paths))
   183  	for _, path := range paths {
   184  		fmt.Fprintf(&buf, "\t%s:%s\n", path, m.errs[path][0].Msg)
   185  	}
   186  
   187  	return buf.String()
   188  }
   189  
   190  // isWorkspacePackageLocked reports whether p is a workspace package for the
   191  // snapshot s.
   192  //
   193  // Workspace packages are packages that we consider the user to be actively
   194  // working on. As such, they are re-diagnosed on every keystroke, and searched
   195  // for various workspace-wide queries such as references or workspace symbols.
   196  //
   197  // See the commentary inline for a description of the workspace package
   198  // heuristics.
   199  //
   200  // s.mu must be held while calling this function.
   201  func isWorkspacePackageLocked(s *Snapshot, meta *metadata.Graph, inst *build.Instance) bool {
   202  	if metadata.IsCommandLineArguments(metadata.ImportPath(inst.ImportPath)) {
   203  		// TODO(ms) original is more complex; decide what we're doing about commandlineargs in general.
   204  		return false
   205  	}
   206  
   207  	// Apply filtering logic.
   208  	//
   209  	// Workspace packages must contain at least one non-filtered file.
   210  	filterFunc := s.view.filterFunc()
   211  	uris := make(map[protocol.DocumentURI]unit) // filtered package URIs
   212  	for _, file := range inst.BuildFiles {
   213  		uri := protocol.URIFromPath(file.Filename)
   214  		if !filterFunc(uri) {
   215  			uris[uri] = struct{}{}
   216  		}
   217  	}
   218  	if len(uris) == 0 {
   219  		return false // no non-filtered files
   220  	}
   221  
   222  	// For non-module views (of type GOPATH or AdHoc), or if
   223  	// expandWorkspaceToModule is unset, workspace packages must be contained in
   224  	// the workspace folder.
   225  	//
   226  	// For module views (of type GoMod or GoWork), packages must in any case be
   227  	// in a workspace module (enforced below).
   228  	if !s.view.moduleMode() || !s.Options().ExpandWorkspaceToModule {
   229  		folder := s.view.folder.Dir.Path()
   230  		inFolder := false
   231  		for uri := range uris {
   232  			if pathutil.InDir(folder, uri.Path()) {
   233  				inFolder = true
   234  				break
   235  			}
   236  		}
   237  		if !inFolder {
   238  			return false
   239  		}
   240  	}
   241  
   242  	// In module mode, a workspace package must be contained in a workspace
   243  	// module.
   244  	if s.view.moduleMode() {
   245  		if inst.Module == "" {
   246  			return false
   247  		}
   248  		modURI := protocol.URIFromPath(inst.Root)
   249  		_, ok := s.view.workspaceModFiles[modURI]
   250  		return ok
   251  	}
   252  
   253  	return true // an ad-hoc package or GOPATH package
   254  }
   255  
   256  // containsOpenFileLocked reports whether any file referenced by inst
   257  // is open in the snapshot s.
   258  //
   259  // s.mu must be held while calling this function.
   260  func containsOpenFileLocked(s *Snapshot, inst *build.Instance) bool {
   261  	for _, file := range inst.BuildFiles {
   262  		fh, _ := s.files.get(protocol.URIFromPath(file.Filename))
   263  		if _, open := fh.(*overlay); open {
   264  			return true
   265  		}
   266  	}
   267  	return false
   268  }
   269  
   270  // computeWorkspacePackagesLocked computes workspace packages in the
   271  // snapshot s for the given metadata graph. The result does not
   272  // contain intermediate test variants.
   273  //
   274  // s.mu must be held while calling this function.
   275  func computeWorkspacePackagesLocked(s *Snapshot, meta *metadata.Graph) immutable.Map[ImportPath, unit] {
   276  	workspacePackages := make(map[ImportPath]unit)
   277  	for _, inst := range meta.Packages {
   278  		if !isWorkspacePackageLocked(s, meta, inst) {
   279  			continue
   280  		}
   281  
   282  		workspacePackages[ImportPath(inst.ImportPath)] = unit{}
   283  	}
   284  	return immutable.MapOf(workspacePackages)
   285  }
   286  
   287  // allFilesHaveRealPackages reports whether all files referenced by inst are
   288  // contained in a "real" package (not command-line-arguments).
   289  //
   290  // If inst is valid but all "real" packages containing any file are invalid, this
   291  // function returns false.
   292  //
   293  // If inst is not a command-line-arguments package, this is trivially true.
   294  func allFilesHaveRealPackages(g *metadata.Graph, inst *build.Instance) bool {
   295  checkURIs:
   296  	for _, file := range inst.BuildFiles {
   297  		for _, pkgPath := range g.FilesToPackage[protocol.URIFromPath(file.Filename)] {
   298  			if !metadata.IsCommandLineArguments(pkgPath) {
   299  				continue checkURIs
   300  			}
   301  		}
   302  		return false
   303  	}
   304  	return true
   305  }