blob: 05ed06941489b51dd8779220ddbad9cb7d33a038 [file] [log] [blame] [edit]
// 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"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"golang.org/x/tools/gopls/internal/cache/metadata"
"golang.org/x/tools/gopls/internal/cache/typerefs"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/persistent"
"golang.org/x/tools/gopls/internal/util/slices"
"golang.org/x/tools/gopls/internal/vulncheck"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/memoize"
"golang.org/x/tools/internal/xcontext"
)
// NewSession creates a new gopls session with the given cache.
func NewSession(ctx context.Context, c *Cache) *Session {
index := atomic.AddInt64(&sessionIndex, 1)
s := &Session{
id: strconv.FormatInt(index, 10),
cache: c,
gocmdRunner: &gocommand.Runner{},
overlayFS: newOverlayFS(c),
parseCache: newParseCache(1 * time.Minute), // keep recently parsed files for a minute, to optimize typing CPU
viewMap: make(map[protocol.DocumentURI]*View),
}
event.Log(ctx, "New session", KeyCreateSession.Of(s))
return s
}
// A Session holds the state (views, file contents, parse cache,
// memoized computations) of a gopls server process.
//
// It implements the file.Source interface.
type Session struct {
// Unique identifier for this session.
id string
// Immutable attributes shared across views.
cache *Cache // shared cache
gocmdRunner *gocommand.Runner // limits go command concurrency
viewMu sync.Mutex
views []*View
viewMap map[protocol.DocumentURI]*View // file->best view; nil after shutdown
// snapshots is a counting semaphore that records the number
// of unreleased snapshots associated with this session.
// Shutdown waits for it to fall to zero.
snapshotWG sync.WaitGroup
parseCache *parseCache
*overlayFS
}
// ID returns the unique identifier for this session on this server.
func (s *Session) ID() string { return s.id }
func (s *Session) String() string { return s.id }
// GoCommandRunner returns the gocommand Runner for this session.
func (s *Session) GoCommandRunner() *gocommand.Runner {
return s.gocmdRunner
}
// Shutdown the session and all views it has created.
func (s *Session) Shutdown(ctx context.Context) {
var views []*View
s.viewMu.Lock()
views = append(views, s.views...)
s.views = nil
s.viewMap = nil
s.viewMu.Unlock()
for _, view := range views {
view.shutdown()
}
s.parseCache.stop()
s.snapshotWG.Wait() // wait for all work on associated snapshots to finish
event.Log(ctx, "Shutdown session", KeyShutdownSession.Of(s))
}
// Cache returns the cache that created this session, for debugging only.
func (s *Session) Cache() *Cache {
return s.cache
}
// TODO(rfindley): is the logic surrounding this error actually necessary?
var ErrViewExists = errors.New("view already exists for session")
// NewView creates a new View, returning it and its first snapshot. If a
// non-empty tempWorkspace directory is provided, the View will record a copy
// of its gopls workspace module in that directory, so that client tooling
// can execute in the same main module. On success it also returns a release
// function that must be called when the Snapshot is no longer needed.
func (s *Session) NewView(ctx context.Context, folder *Folder) (*View, *Snapshot, func(), error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
// Querying the file system to check whether
// two folders denote the same existing directory.
if inode1, err := os.Stat(filepath.FromSlash(folder.Dir.Path())); err == nil {
for _, view := range s.views {
inode2, err := os.Stat(filepath.FromSlash(view.folder.Dir.Path()))
if err == nil && os.SameFile(inode1, inode2) {
return nil, nil, nil, ErrViewExists
}
}
}
def, err := defineView(ctx, s, folder, nil)
if err != nil {
return nil, nil, nil, err
}
view, snapshot, release := s.createView(ctx, def)
s.views = append(s.views, view)
// we always need to drop the view map
s.viewMap = make(map[protocol.DocumentURI]*View)
return view, snapshot, release, nil
}
// createView creates a new view, with an initial snapshot that retains the
// supplied context, detached from events and cancelation.
//
// The caller is responsible for calling the release function once.
func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, *Snapshot, func()) {
index := atomic.AddInt64(&viewIndex, 1)
// We want a true background context and not a detached context here
// the spans need to be unrelated and no tag values should pollute it.
baseCtx := event.Detach(xcontext.Detach(ctx))
backgroundCtx, cancel := context.WithCancel(baseCtx)
// Compute a skip function to use for module cache scanning.
//
// Note that unlike other filtering operations, we definitely don't want to
// exclude the gomodcache here, even if it is contained in the workspace
// folder.
//
// TODO(rfindley): consolidate with relPathExcludedByFilter(Func), Filterer,
// View.filterFunc.
var skipPath func(string) bool
{
// Compute a prefix match, respecting segment boundaries, by ensuring
// the pattern (dir) has a trailing slash.
dirPrefix := strings.TrimSuffix(string(def.folder.Dir), "/") + "/"
filterer := NewFilterer(def.folder.Options.DirectoryFilters)
skipPath = func(dir string) bool {
uri := strings.TrimSuffix(string(protocol.URIFromPath(dir)), "/")
// Note that the logic below doesn't handle the case where uri ==
// v.folder.Dir, because there is no point in excluding the entire
// workspace folder!
if rel := strings.TrimPrefix(uri, dirPrefix); rel != uri {
return filterer.Disallow(rel)
}
return false
}
}
var ignoreFilter *ignoreFilter
{
var dirs []string
if len(def.workspaceModFiles) == 0 {
for _, entry := range filepath.SplitList(def.folder.Env.GOPATH) {
dirs = append(dirs, filepath.Join(entry, "src"))
}
} else {
dirs = append(dirs, def.folder.Env.GOMODCACHE)
for m := range def.workspaceModFiles {
dirs = append(dirs, filepath.Dir(m.Path()))
}
}
ignoreFilter = newIgnoreFilter(dirs)
}
var pe *imports.ProcessEnv
{
env := make(map[string]string)
envSlice := slices.Concat(os.Environ(), def.folder.Options.EnvSlice(), []string{"GO111MODULE=" + def.adjustedGO111MODULE()})
for _, kv := range envSlice {
if k, v, ok := strings.Cut(kv, "="); ok {
env[k] = v
}
}
pe = &imports.ProcessEnv{
GocmdRunner: s.gocmdRunner,
BuildFlags: slices.Clone(def.folder.Options.BuildFlags),
// TODO(rfindley): an old comment said "processEnv operations should not mutate the modfile"
// But shouldn't we honor the default behavior of mod vendoring?
ModFlag: "readonly",
SkipPathInScan: skipPath,
Env: env,
WorkingDir: def.root.Path(),
ModCache: s.cache.modCache.dirCache(def.folder.Env.GOMODCACHE),
}
if def.folder.Options.VerboseOutput {
pe.Logf = func(format string, args ...interface{}) {
event.Log(ctx, fmt.Sprintf(format, args...))
}
}
}
v := &View{
id: strconv.FormatInt(index, 10),
gocmdRunner: s.gocmdRunner,
initialWorkspaceLoad: make(chan struct{}),
initializationSema: make(chan struct{}, 1),
baseCtx: baseCtx,
parseCache: s.parseCache,
ignoreFilter: ignoreFilter,
fs: s.overlayFS,
viewDefinition: def,
importsState: newImportsState(backgroundCtx, s.cache.modCache, pe),
}
s.snapshotWG.Add(1)
v.snapshot = &Snapshot{
view: v,
backgroundCtx: backgroundCtx,
cancel: cancel,
store: s.cache.store,
refcount: 1, // Snapshots are born referenced.
done: s.snapshotWG.Done,
packages: new(persistent.Map[PackageID, *packageHandle]),
meta: new(metadata.Graph),
files: newFileMap(),
activePackages: new(persistent.Map[PackageID, *Package]),
symbolizeHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
shouldLoad: new(persistent.Map[PackageID, []PackagePath]),
unloadableFiles: new(persistent.Set[protocol.DocumentURI]),
parseModHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
parseWorkHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
modTidyHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
modVulnHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
modWhyHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
pkgIndex: typerefs.NewPackageIndex(),
moduleUpgrades: new(persistent.Map[protocol.DocumentURI, map[string]string]),
vulns: new(persistent.Map[protocol.DocumentURI, *vulncheck.Result]),
}
// Snapshots must observe all open files, as there are some caching
// heuristics that change behavior depending on open files.
for _, o := range s.overlayFS.Overlays() {
_, _ = v.snapshot.ReadFile(ctx, o.URI())
}
// Record the environment of the newly created view in the log.
event.Log(ctx, viewEnv(v))
// Initialize the view without blocking.
initCtx, initCancel := context.WithCancel(xcontext.Detach(ctx))
v.cancelInitialWorkspaceLoad = initCancel
snapshot := v.snapshot
// Pass a second reference to the background goroutine.
bgRelease := snapshot.Acquire()
go func() {
defer bgRelease()
snapshot.initialize(initCtx, true)
}()
// Return a third reference to the caller.
return v, snapshot, snapshot.Acquire()
}
// RemoveView removes from the session the view rooted at the specified directory.
// It reports whether a view of that directory was removed.
func (s *Session) RemoveView(dir protocol.DocumentURI) bool {
s.viewMu.Lock()
defer s.viewMu.Unlock()
for _, view := range s.views {
if view.folder.Dir == dir {
i := s.dropView(view)
if i == -1 {
return false // can't happen
}
// delete this view... we don't care about order but we do want to make
// sure we can garbage collect the view
s.views = removeElement(s.views, i)
return true
}
}
return false
}
// View returns the view with a matching id, if present.
func (s *Session) View(id string) (*View, error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
for _, view := range s.views {
if view.ID() == id {
return view, nil
}
}
return nil, fmt.Errorf("no view with ID %q", id)
}
// SnapshotOf returns a Snapshot corresponding to the given URI.
//
// In the case where the file can be can be associated with a View by
// bestViewForURI (based on directory information alone, without package
// metadata), SnapshotOf returns the current Snapshot for that View. Otherwise,
// it awaits loading package metadata and returns a Snapshot for the first View
// containing a real (=not command-line-arguments) package for the file.
//
// If that also fails to find a View, SnapshotOf returns a Snapshot for the
// first view in s.views that is not shut down (i.e. s.views[0] unless we lose
// a race), for determinism in tests and so that we tend to aggregate the
// resulting command-line-arguments packages into a single view.
//
// SnapshotOf returns an error if a failure occurs along the way (most likely due
// to context cancellation), or if there are no Views in the Session.
//
// On success, the caller must call the returned function to release the snapshot.
func (s *Session) SnapshotOf(ctx context.Context, uri protocol.DocumentURI) (*Snapshot, func(), error) {
// Fast path: if the uri has a static association with a view, return it.
s.viewMu.Lock()
v, err := s.viewOfLocked(ctx, uri)
s.viewMu.Unlock()
if err != nil {
return nil, nil, err
}
if v != nil {
snapshot, release, err := v.Snapshot()
if err == nil {
return snapshot, release, nil
}
// View is shut down. Forget this association.
s.viewMu.Lock()
if s.viewMap[uri] == v {
delete(s.viewMap, uri)
}
s.viewMu.Unlock()
}
// Fall-back: none of the views could be associated with uri based on
// directory information alone.
//
// Don't memoize the view association in viewMap, as it is not static: Views
// may change as metadata changes.
//
// TODO(rfindley): we could perhaps optimize this case by peeking at existing
// metadata before awaiting the load (after all, a load only adds metadata).
// But that seems potentially tricky, when in the common case no loading
// should be required.
views := s.Views()
for _, v := range views {
snapshot, release, err := v.Snapshot()
if err != nil {
continue // view was shut down
}
_ = snapshot.awaitLoaded(ctx) // ignore error
g := snapshot.MetadataGraph()
// We don't check the error from awaitLoaded, because a load failure (that
// doesn't result from context cancelation) should not prevent us from
// continuing to search for the best view.
if ctx.Err() != nil {
release()
return nil, nil, ctx.Err()
}
// Special handling for the builtin file, since it doesn't have packages.
if snapshot.IsBuiltin(uri) {
return snapshot, release, nil
}
// Only match this view if it loaded a real package for the file.
//
// Any view can load a command-line-arguments package; aggregate those into
// views[0] below.
for _, id := range g.IDs[uri] {
if !metadata.IsCommandLineArguments(id) || g.Packages[id].Standalone {
return snapshot, release, nil
}
}
release()
}
for _, v := range views {
snapshot, release, err := v.Snapshot()
if err == nil {
return snapshot, release, nil // first valid snapshot
}
}
return nil, nil, errNoViews
}
// errNoViews is sought by orphaned file diagnostics, to detect the case where
// we have no view containing a file.
var errNoViews = errors.New("no views")
// viewOfLocked wraps bestViewForURI, memoizing its result.
//
// Precondition: caller holds s.viewMu lock.
//
// May return (nil, nil).
func (s *Session) viewOfLocked(ctx context.Context, uri protocol.DocumentURI) (*View, error) {
v, hit := s.viewMap[uri]
if !hit {
// Cache miss: compute (and memoize) the best view.
fh, err := s.ReadFile(ctx, uri)
if err != nil {
return nil, err
}
v, err = bestView(ctx, s, fh, s.views)
if err != nil {
return nil, err
}
if s.viewMap == nil {
return nil, errors.New("session is shut down")
}
s.viewMap[uri] = v
}
return v, nil
}
func (s *Session) Views() []*View {
s.viewMu.Lock()
defer s.viewMu.Unlock()
result := make([]*View, len(s.views))
copy(result, s.views)
return result
}
// selectViewDefs constructs the best set of views covering the provided workspace
// folders and open files.
//
// This implements the zero-config algorithm of golang/go#57979.
func selectViewDefs(ctx context.Context, fs file.Source, folders []*Folder, openFiles []protocol.DocumentURI) ([]*viewDefinition, error) {
var defs []*viewDefinition
// First, compute a default view for each workspace folder.
// TODO(golang/go#57979): technically, this is path dependent, since
// DidChangeWorkspaceFolders could introduce a path-dependent ordering on
// folders. We should keep folders sorted, or sort them here.
for _, folder := range folders {
def, err := defineView(ctx, fs, folder, nil)
if err != nil {
return nil, err
}
defs = append(defs, def)
}
// Next, ensure that the set of views covers all open files contained in a
// workspace folder.
//
// We only do this for files contained in a workspace folder, because other
// open files are most likely the result of jumping to a definition from a
// workspace file; we don't want to create additional views in those cases:
// they should be resolved after initialization.
folderForFile := func(uri protocol.DocumentURI) *Folder {
var longest *Folder
for _, folder := range folders {
// Check that this is a better match than longest, but not through a
// vendor directory. Count occurrences of "/vendor/" as a quick check
// that the vendor directory is between the folder and the file. Note the
// addition of a trailing "/" to handle the odd case where the folder is named
// vendor (which I hope is exceedingly rare in any case).
//
// Vendored packages are, by definition, part of an existing view.
if (longest == nil || len(folder.Dir) > len(longest.Dir)) &&
folder.Dir.Encloses(uri) &&
strings.Count(string(uri), "/vendor/") == strings.Count(string(folder.Dir)+"/", "/vendor/") {
longest = folder
}
}
return longest
}
checkFiles:
for _, uri := range openFiles {
folder := folderForFile(uri)
if folder == nil || !folder.Options.ZeroConfig {
continue // only guess views for open files
}
fh, err := fs.ReadFile(ctx, uri)
if err != nil {
return nil, err
}
def, err := bestView(ctx, fs, fh, defs)
if err != nil {
// We should never call selectViewDefs with a cancellable context, so
// this should never fail.
return nil, bug.Errorf("failed to find best view for open file: %v", err)
}
if def != nil {
continue // file covered by an existing view
}
def, err = defineView(ctx, fs, folder, fh)
if err != nil {
// We should never call selectViewDefs with a cancellable context, so
// this should never fail.
return nil, bug.Errorf("failed to define view for open file: %v", err)
}
// It need not strictly be the case that the best view for a file is
// distinct from other views, as the logic of getViewDefinition and
// bestViewForURI does not align perfectly. This is not necessarily a bug:
// there may be files for which we can't construct a valid view.
//
// Nevertheless, we should not create redundant views.
for _, alt := range defs {
if viewDefinitionsEqual(alt, def) {
continue checkFiles
}
}
defs = append(defs, def)
}
return defs, nil
}
// The viewDefiner interface allows the bestView algorithm to operate on both
// Views and viewDefinitions.
type viewDefiner interface{ definition() *viewDefinition }
// BestViews returns the most relevant subset of views for a given uri.
//
// This may be used to filter diagnostics to the most relevant builds.
func BestViews[V viewDefiner](ctx context.Context, fs file.Source, uri protocol.DocumentURI, views []V) ([]V, error) {
if len(views) == 0 {
return nil, nil // avoid the call to findRootPattern
}
dir := uri.Dir()
modURI, err := findRootPattern(ctx, dir, "go.mod", fs)
if err != nil {
return nil, err
}
// Prefer GoWork > GoMod > GOPATH > GoPackages > AdHoc.
var (
goPackagesViews []V // prefer longest
workViews []V // prefer longest
modViews []V // exact match
gopathViews []V // prefer longest
adHocViews []V // exact match
)
// pushView updates the views slice with the matching view v, using the
// heuristic that views with a longer root are preferable. Accordingly,
// pushView may be a no op if v's root is shorter than the roots in the views
// slice.
//
// Invariant: the length of all roots in views is the same.
pushView := func(views *[]V, v V) {
if len(*views) == 0 {
*views = []V{v}
return
}
better := func(l, r V) bool {
return len(l.definition().root) > len(r.definition().root)
}
existing := (*views)[0]
switch {
case better(existing, v):
case better(v, existing):
*views = []V{v}
default:
*views = append(*views, v)
}
}
for _, view := range views {
switch def := view.definition(); def.Type() {
case GoPackagesDriverView:
if def.root.Encloses(dir) {
pushView(&goPackagesViews, view)
}
case GoWorkView:
if _, ok := def.workspaceModFiles[modURI]; ok || uri == def.gowork {
pushView(&workViews, view)
}
case GoModView:
if _, ok := def.workspaceModFiles[modURI]; ok {
modViews = append(modViews, view)
}
case GOPATHView:
if def.root.Encloses(dir) {
pushView(&gopathViews, view)
}
case AdHocView:
if def.root == dir {
adHocViews = append(adHocViews, view)
}
}
}
// Now that we've collected matching views, choose the best match,
// considering ports.
//
// We only consider one type of view, since the matching view created by
// defineView should be of the best type.
var bestViews []V
switch {
case len(workViews) > 0:
bestViews = workViews
case len(modViews) > 0:
bestViews = modViews
case len(gopathViews) > 0:
bestViews = gopathViews
case len(goPackagesViews) > 0:
bestViews = goPackagesViews
case len(adHocViews) > 0:
bestViews = adHocViews
}
return bestViews, nil
}
// bestView returns the best View or viewDefinition that contains the
// given file, or (nil, nil) if no matching view is found.
//
// bestView only returns an error in the event of context cancellation.
//
// Making this function generic is convenient so that we can avoid mapping view
// definitions back to views inside Session.DidModifyFiles, where performance
// matters. It is, however, not the cleanest application of generics.
//
// Note: keep this function in sync with defineView.
func bestView[V viewDefiner](ctx context.Context, fs file.Source, fh file.Handle, views []V) (V, error) {
var zero V
bestViews, err := BestViews(ctx, fs, fh.URI(), views)
if err != nil || len(bestViews) == 0 {
return zero, err
}
content, err := fh.Content()
// Port matching doesn't apply to non-go files, or files that no longer exist.
// Note that the behavior here on non-existent files shouldn't matter much,
// since there will be a subsequent failure. But it is simpler to preserve
// the invariant that bestView only fails on context cancellation.
if fileKind(fh) != file.Go || err != nil {
return bestViews[0], nil
}
// Find the first view that matches constraints.
// Content trimming is nontrivial, so do this outside of the loop below.
path := fh.URI().Path()
content = trimContentForPortMatch(content)
for _, v := range bestViews {
def := v.definition()
viewPort := port{def.GOOS(), def.GOARCH()}
if viewPort.matches(path, content) {
return v, nil
}
}
return zero, nil // no view found
}
// updateViewLocked recreates the view with the given options.
//
// If the resulting error is non-nil, the view may or may not have already been
// dropped from the session.
func (s *Session) updateViewLocked(ctx context.Context, view *View, def *viewDefinition) (*View, error) {
i := s.dropView(view)
if i == -1 {
return nil, fmt.Errorf("view %q not found", view.id)
}
view, _, release := s.createView(ctx, def)
defer release()
// substitute the new view into the array where the old view was
s.views[i] = view
s.viewMap = make(map[protocol.DocumentURI]*View)
return view, nil
}
// removeElement removes the ith element from the slice replacing it with the last element.
// TODO(adonovan): generics, someday.
func removeElement(slice []*View, index int) []*View {
last := len(slice) - 1
slice[index] = slice[last]
slice[last] = nil // aid GC
return slice[:last]
}
// dropView removes v from the set of views for the receiver s and calls
// v.shutdown, returning the index of v in s.views (if found), or -1 if v was
// not found. s.viewMu must be held while calling this function.
func (s *Session) dropView(v *View) int {
// we always need to drop the view map
s.viewMap = make(map[protocol.DocumentURI]*View)
for i := range s.views {
if v == s.views[i] {
// we found the view, drop it and return the index it was found at
s.views[i] = nil
v.shutdown()
return i
}
}
// TODO(rfindley): it looks wrong that we don't shutdown v in this codepath.
// We should never get here.
bug.Reportf("tried to drop nonexistent view %q", v.id)
return -1
}
// ResetView resets the best view for the given URI.
func (s *Session) ResetView(ctx context.Context, uri protocol.DocumentURI) (*View, error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
v, err := s.viewOfLocked(ctx, uri)
if err != nil {
return nil, err
}
return s.updateViewLocked(ctx, v, v.viewDefinition)
}
// DidModifyFiles reports a file modification to the session. It returns
// the new snapshots after the modifications have been applied, paired with
// the affected file URIs for those snapshots.
// On success, it returns a release function that
// must be called when the snapshots are no longer needed.
//
// TODO(rfindley): what happens if this function fails? It must leave us in a
// broken state, which we should surface to the user, probably as a request to
// restart gopls.
func (s *Session) DidModifyFiles(ctx context.Context, modifications []file.Modification) (map[*View][]protocol.DocumentURI, error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
// Update overlays.
//
// This is done while holding viewMu because the set of open files affects
// the set of views, and to prevent views from seeing updated file content
// before they have processed invalidations.
replaced, err := s.updateOverlays(ctx, modifications)
if err != nil {
return nil, err
}
// checkViews controls whether the set of views needs to be recomputed, for
// example because a go.mod file was created or deleted, or a go.work file
// changed on disk.
checkViews := false
changed := make(map[protocol.DocumentURI]file.Handle)
for _, c := range modifications {
fh := mustReadFile(ctx, s, c.URI)
changed[c.URI] = fh
// Any change to the set of open files causes views to be recomputed.
if c.Action == file.Open || c.Action == file.Close {
checkViews = true
}
// Any on-disk change to a go.work or go.mod file causes recomputing views.
//
// TODO(rfindley): go.work files need not be named "go.work" -- we need to
// check each view's source to handle the case of an explicit GOWORK value.
// Write a test that fails, and fix this.
if (isGoWork(c.URI) || isGoMod(c.URI)) && (c.Action == file.Save || c.OnDisk) {
checkViews = true
}
// Any change to the set of supported ports in a file may affect view
// selection. This is perhaps more subtle than it first seems: since the
// algorithm for selecting views considers open files in a deterministic
// order, a change in supported ports may cause a different port to be
// chosen, even if all open files still match an existing View!
//
// We endeavor to avoid that sort of path dependence, so must re-run the
// view selection algorithm whenever any input changes.
//
// However, extracting the build comment is nontrivial, so we don't want to
// pay this cost when e.g. processing a bunch of on-disk changes due to a
// branch change. Be careful to only do this if both files are open Go
// files.
if old, ok := replaced[c.URI]; ok && !checkViews && fileKind(fh) == file.Go {
if new, ok := fh.(*overlay); ok {
if buildComment(old.content) != buildComment(new.content) {
checkViews = true
}
}
}
}
if checkViews {
// Hack: collect folders from existing views.
// TODO(golang/go#57979): we really should track folders independent of
// Views, but since we always have a default View for each folder, this
// works for now.
var folders []*Folder // preserve folder order
seen := make(map[*Folder]unit)
for _, v := range s.views {
if _, ok := seen[v.folder]; ok {
continue
}
seen[v.folder] = unit{}
folders = append(folders, v.folder)
}
var openFiles []protocol.DocumentURI
for _, o := range s.Overlays() {
openFiles = append(openFiles, o.URI())
}
// Sort for determinism.
sort.Slice(openFiles, func(i, j int) bool {
return openFiles[i] < openFiles[j]
})
// TODO(rfindley): can we avoid running the go command (go env)
// synchronously to change processing? Can we assume that the env did not
// change, and derive go.work using a combination of the configured
// GOWORK value and filesystem?
defs, err := selectViewDefs(ctx, s, folders, openFiles)
if err != nil {
// Catastrophic failure, equivalent to a failure of session
// initialization and therefore should almost never happen. One
// scenario where this failure mode could occur is if some file
// permissions have changed preventing us from reading go.mod
// files.
//
// TODO(rfindley): consider surfacing this error more loudly. We
// could report a bug, but it's not really a bug.
event.Error(ctx, "selecting new views", err)
} else {
kept := make(map[*View]unit)
var newViews []*View
for _, def := range defs {
var newView *View
// Reuse existing view?
for _, v := range s.views {
if viewDefinitionsEqual(def, v.viewDefinition) {
newView = v
kept[v] = unit{}
break
}
}
if newView == nil {
v, _, release := s.createView(ctx, def)
release()
newView = v
}
newViews = append(newViews, newView)
}
for _, v := range s.views {
if _, ok := kept[v]; !ok {
v.shutdown()
}
}
s.views = newViews
s.viewMap = make(map[protocol.DocumentURI]*View)
}
}
// We only want to run fast-path diagnostics (i.e. diagnoseChangedFiles) once
// for each changed file, in its best view.
viewsToDiagnose := map[*View][]protocol.DocumentURI{}
for _, mod := range modifications {
v, err := s.viewOfLocked(ctx, mod.URI)
if err != nil {
// bestViewForURI only returns an error in the event of context
// cancellation. Since state changes should occur on an uncancellable
// context, an error here is a bug.
bug.Reportf("finding best view for change: %v", err)
continue
}
if v != nil {
viewsToDiagnose[v] = append(viewsToDiagnose[v], mod.URI)
}
}
// ...but changes may be relevant to other views, for example if they are
// changes to a shared package.
for _, v := range s.views {
_, release, needsDiagnosis := s.invalidateViewLocked(ctx, v, StateChange{Modifications: modifications, Files: changed})
release()
if needsDiagnosis || checkViews {
if _, ok := viewsToDiagnose[v]; !ok {
viewsToDiagnose[v] = nil
}
}
}
return viewsToDiagnose, nil
}
// ExpandModificationsToDirectories returns the set of changes with the
// directory changes removed and expanded to include all of the files in
// the directory.
func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes []file.Modification) []file.Modification {
var snapshots []*Snapshot
s.viewMu.Lock()
for _, v := range s.views {
snapshot, release, err := v.Snapshot()
if err != nil {
continue // view is shut down; continue with others
}
defer release()
snapshots = append(snapshots, snapshot)
}
s.viewMu.Unlock()
// Expand the modification to any file we could care about, which we define
// to be any file observed by any of the snapshots.
//
// There may be other files in the directory, but if we haven't read them yet
// we don't need to invalidate them.
var result []file.Modification
for _, c := range changes {
expanded := make(map[protocol.DocumentURI]bool)
for _, snapshot := range snapshots {
for _, uri := range snapshot.filesInDir(c.URI) {
expanded[uri] = true
}
}
if len(expanded) == 0 {
result = append(result, c)
} else {
for uri := range expanded {
result = append(result, file.Modification{
URI: uri,
Action: c.Action,
LanguageID: "",
OnDisk: c.OnDisk,
// changes to directories cannot include text or versions
})
}
}
}
return result
}
// updateOverlays updates the set of overlays and returns a map of any existing
// overlay values that were replaced.
//
// Precondition: caller holds s.viewMu lock.
// TODO(rfindley): move this to fs_overlay.go.
func (fs *overlayFS) updateOverlays(ctx context.Context, changes []file.Modification) (map[protocol.DocumentURI]*overlay, error) {
fs.mu.Lock()
defer fs.mu.Unlock()
replaced := make(map[protocol.DocumentURI]*overlay)
for _, c := range changes {
o, ok := fs.overlays[c.URI]
if ok {
replaced[c.URI] = o
}
// If the file is not opened in an overlay and the change is on disk,
// there's no need to update an overlay. If there is an overlay, we
// may need to update the overlay's saved value.
if !ok && c.OnDisk {
continue
}
// Determine the file kind on open, otherwise, assume it has been cached.
var kind file.Kind
switch c.Action {
case file.Open:
kind = file.KindForLang(c.LanguageID)
default:
if !ok {
return nil, fmt.Errorf("updateOverlays: modifying unopened overlay %v", c.URI)
}
kind = o.kind
}
// Closing a file just deletes its overlay.
if c.Action == file.Close {
delete(fs.overlays, c.URI)
continue
}
// If the file is on disk, check if its content is the same as in the
// overlay. Saves and on-disk file changes don't come with the file's
// content.
text := c.Text
if text == nil && (c.Action == file.Save || c.OnDisk) {
if !ok {
return nil, fmt.Errorf("no known content for overlay for %s", c.Action)
}
text = o.content
}
// On-disk changes don't come with versions.
version := c.Version
if c.OnDisk || c.Action == file.Save {
version = o.version
}
hash := file.HashOf(text)
var sameContentOnDisk bool
switch c.Action {
case file.Delete:
// Do nothing. sameContentOnDisk should be false.
case file.Save:
// Make sure the version and content (if present) is the same.
if false && o.version != version { // Client no longer sends the version
return nil, fmt.Errorf("updateOverlays: saving %s at version %v, currently at %v", c.URI, c.Version, o.version)
}
if c.Text != nil && o.hash != hash {
return nil, fmt.Errorf("updateOverlays: overlay %s changed on save", c.URI)
}
sameContentOnDisk = true
default:
fh := mustReadFile(ctx, fs.delegate, c.URI)
_, readErr := fh.Content()
sameContentOnDisk = (readErr == nil && fh.Identity().Hash == hash)
}
o = &overlay{
uri: c.URI,
version: version,
content: text,
kind: kind,
hash: hash,
saved: sameContentOnDisk,
}
// NOTE: previous versions of this code checked here that the overlay had a
// view and file kind (but we don't know why).
fs.overlays[c.URI] = o
}
return replaced, nil
}
func mustReadFile(ctx context.Context, fs file.Source, uri protocol.DocumentURI) file.Handle {
ctx = xcontext.Detach(ctx)
fh, err := fs.ReadFile(ctx, uri)
if err != nil {
// ReadFile cannot fail with an uncancellable context.
bug.Reportf("reading file failed unexpectedly: %v", err)
return brokenFile{uri, err}
}
return fh
}
// A brokenFile represents an unexpected failure to read a file.
type brokenFile struct {
uri protocol.DocumentURI
err error
}
func (b brokenFile) URI() protocol.DocumentURI { return b.uri }
func (b brokenFile) Identity() file.Identity { return file.Identity{URI: b.uri} }
func (b brokenFile) SameContentsOnDisk() bool { return false }
func (b brokenFile) Version() int32 { return 0 }
func (b brokenFile) Content() ([]byte, error) { return nil, b.err }
// FileWatchingGlobPatterns returns a set of glob patterns that the client is
// required to watch for changes, and notify the server of them, in order to
// keep the server's state up to date.
//
// This set includes
// 1. all go.mod and go.work files in the workspace; and
// 2. for each Snapshot, its modules (or directory for ad-hoc views). In
// module mode, this is the set of active modules (and for VS Code, all
// workspace directories within them, due to golang/go#42348).
//
// The watch for workspace go.work and go.mod files in (1) is sufficient to
// capture changes to the repo structure that may affect the set of views.
// Whenever this set changes, we reload the workspace and invalidate memoized
// files.
//
// The watch for workspace directories in (2) should keep each View up to date,
// as it should capture any newly added/modified/deleted Go files.
//
// Patterns are returned as a set of protocol.RelativePatterns, since they can
// always be later translated to glob patterns (i.e. strings) if the client
// lacks relative pattern support. By convention, any pattern returned with
// empty baseURI should be served as a glob pattern.
//
// In general, we prefer to serve relative patterns, as they work better on
// most clients that support both, and do not have issues with Windows driver
// letter casing:
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#relativePattern
//
// TODO(golang/go#57979): we need to reset the memoizedFS when a view changes.
// Consider the case where we incidentally read a file, then it moved outside
// of an active module, and subsequently changed: we would still observe the
// original file state.
func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[protocol.RelativePattern]unit {
s.viewMu.Lock()
defer s.viewMu.Unlock()
// Always watch files that may change the set of views.
patterns := map[protocol.RelativePattern]unit{
{Pattern: "**/*.{mod,work}"}: {},
}
for _, view := range s.views {
snapshot, release, err := view.Snapshot()
if err != nil {
continue // view is shut down; continue with others
}
for k, v := range snapshot.fileWatchingGlobPatterns() {
patterns[k] = v
}
release()
}
return patterns
}
// OrphanedFileDiagnostics reports diagnostics describing why open files have
// no packages or have only command-line-arguments packages.
//
// If the resulting diagnostic is nil, the file is either not orphaned or we
// can't produce a good diagnostic.
//
// The caller must not mutate the result.
func (s *Session) OrphanedFileDiagnostics(ctx context.Context) (map[protocol.DocumentURI][]*Diagnostic, error) {
// Note: diagnostics holds a slice for consistency with other diagnostic
// funcs.
diagnostics := make(map[protocol.DocumentURI][]*Diagnostic)
byView := make(map[*View][]*overlay)
for _, o := range s.Overlays() {
uri := o.URI()
snapshot, release, err := s.SnapshotOf(ctx, uri)
if err != nil {
// TODO(golang/go#57979): we have to use the .go suffix as an approximation for
// file kind here, because we don't have access to Options if no View was
// matched.
//
// But Options are really a property of Folder, not View, and we could
// match a folder here.
//
// Refactor so that Folders are tracked independently of Views, and use
// the correct options here to get the most accurate file kind.
//
// TODO(golang/go#57979): once we switch entirely to the zeroconfig
// logic, we should use this diagnostic for the fallback case of
// s.views[0] in the ViewOf logic.
if errors.Is(err, errNoViews) {
if strings.HasSuffix(string(uri), ".go") {
if _, rng, ok := orphanedFileDiagnosticRange(ctx, s.parseCache, o); ok {
diagnostics[uri] = []*Diagnostic{{
URI: uri,
Range: rng,
Severity: protocol.SeverityWarning,
Source: ListError,
Message: fmt.Sprintf("No active builds contain %s: consider opening a new workspace folder containing it", uri.Path()),
}}
}
}
continue
}
return nil, err
}
v := snapshot.View()
release()
byView[v] = append(byView[v], o)
}
for view, overlays := range byView {
snapshot, release, err := view.Snapshot()
if err != nil {
continue // view is shutting down
}
defer release()
diags, err := snapshot.orphanedFileDiagnostics(ctx, overlays)
if err != nil {
return nil, err
}
for _, d := range diags {
diagnostics[d.URI] = append(diagnostics[d.URI], d)
}
}
return diagnostics, nil
}