internal/lsp: match files by identity

Instead of using a simple path map we now attempt to match files with
os.SameFile with fast paths for exact path matches. This should fix issues both
with symlinked directories (the mac tmp folder) and with case sensitivity
(windows)

Change-Id: I014dd01f89d08a348e7de7697cbc3a2512a6e5b3
Reviewed-on: https://go-review.googlesource.com/c/tools/+/169699
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Ian Cottrell 2019-03-27 09:25:30 -04:00
parent dbeab5af4b
commit c70d86f8b7
4 changed files with 96 additions and 52 deletions

View File

@ -19,7 +19,7 @@ import (
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
) )
func (v *View) parse(ctx context.Context, uri span.URI) ([]packages.Error, error) { func (v *View) parse(ctx context.Context, f *File) ([]packages.Error, error) {
v.mcache.mu.Lock() v.mcache.mu.Lock()
defer v.mcache.mu.Unlock() defer v.mcache.mu.Unlock()
@ -28,12 +28,6 @@ func (v *View) parse(ctx context.Context, uri span.URI) ([]packages.Error, error
return nil, err return nil, err
} }
f := v.files[uri]
// This should never happen.
if f == nil {
return nil, fmt.Errorf("no file for %v", uri)
}
// If the package for the file has not been invalidated by the application // If the package for the file has not been invalidated by the application
// of the pending changes, there is no need to continue. // of the pending changes, there is no need to continue.
if f.isPopulated() { if f.isPopulated() {
@ -45,7 +39,7 @@ func (v *View) parse(ctx context.Context, uri span.URI) ([]packages.Error, error
return errs, err return errs, err
} }
if f.meta == nil { if f.meta == nil {
return nil, fmt.Errorf("no metadata found for %v", uri) return nil, fmt.Errorf("no metadata found for %v", f.filename)
} }
imp := &importer{ imp := &importer{
view: v, view: v,
@ -65,7 +59,7 @@ func (v *View) parse(ctx context.Context, uri span.URI) ([]packages.Error, error
// If we still have not found the package for the file, something is wrong. // If we still have not found the package for the file, something is wrong.
if f.pkg == nil { if f.pkg == nil {
return nil, fmt.Errorf("no package found for %v", uri) return nil, fmt.Errorf("parse: no package found for %v", f.filename)
} }
return nil, nil return nil, nil
} }
@ -83,7 +77,11 @@ func (v *View) cachePackage(pkg *Package) {
continue continue
} }
fURI := span.FileURI(tok.Name()) fURI := span.FileURI(tok.Name())
f := v.getFile(fURI) f, err := v.getFile(fURI)
if err != nil {
log.Printf("no file: %v", err)
continue
}
f.token = tok f.token = tok
f.ast = file f.ast = file
f.imports = f.ast.Imports f.imports = f.ast.Imports
@ -92,17 +90,13 @@ func (v *View) cachePackage(pkg *Package) {
} }
func (v *View) checkMetadata(ctx context.Context, f *File) ([]packages.Error, error) { func (v *View) checkMetadata(ctx context.Context, f *File) ([]packages.Error, error) {
filename, err := f.uri.Filename() if v.reparseImports(ctx, f, f.filename) {
if err != nil {
return nil, err
}
if v.reparseImports(ctx, f, filename) {
cfg := v.Config cfg := v.Config
cfg.Mode = packages.LoadImports cfg.Mode = packages.LoadImports
pkgs, err := packages.Load(&cfg, fmt.Sprintf("file=%s", filename)) pkgs, err := packages.Load(&cfg, fmt.Sprintf("file=%s", f.filename))
if len(pkgs) == 0 { if len(pkgs) == 0 {
if err == nil { if err == nil {
err = fmt.Errorf("no packages found for %s", filename) err = fmt.Errorf("no packages found for %s", f.filename)
} }
return nil, err return nil, err
} }
@ -156,7 +150,7 @@ func (v *View) link(pkgPath string, pkg *packages.Package, parent *metadata) *me
m.name = pkg.Name m.name = pkg.Name
m.files = pkg.CompiledGoFiles m.files = pkg.CompiledGoFiles
for _, filename := range m.files { for _, filename := range m.files {
if f, ok := v.files[span.FileURI(filename)]; ok { if f := v.findFile(span.FileURI(filename)); f != nil {
f.meta = m f.meta = m
} }
} }
@ -347,7 +341,7 @@ func (v *View) parseFiles(filenames []string) ([]*ast.File, []error) {
} }
// First, check if we have already cached an AST for this file. // First, check if we have already cached an AST for this file.
f := v.files[span.FileURI(filename)] f := v.findFile(span.FileURI(filename))
var fAST *ast.File var fAST *ast.File
if f != nil { if f != nil {
fAST = f.ast fAST = f.ast

View File

@ -9,6 +9,8 @@ import (
"go/ast" "go/ast"
"go/token" "go/token"
"io/ioutil" "io/ioutil"
"path/filepath"
"strings"
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
@ -16,7 +18,10 @@ import (
// File holds all the information we know about a file. // File holds all the information we know about a file.
type File struct { type File struct {
uri span.URI uris []span.URI
filename string
basename string
view *View view *View
active bool active bool
content []byte content []byte
@ -27,8 +32,12 @@ type File struct {
imports []*ast.ImportSpec imports []*ast.ImportSpec
} }
func basename(filename string) string {
return strings.ToLower(filepath.Base(filename))
}
func (f *File) URI() span.URI { func (f *File) URI() span.URI {
return f.uri return f.uris[0]
} }
// GetContent returns the contents of the file, reading it from file system if needed. // GetContent returns the contents of the file, reading it from file system if needed.
@ -52,7 +61,7 @@ func (f *File) GetToken(ctx context.Context) *token.File {
defer f.view.mu.Unlock() defer f.view.mu.Unlock()
if f.token == nil || len(f.view.contentChanges) > 0 { if f.token == nil || len(f.view.contentChanges) > 0 {
if _, err := f.view.parse(ctx, f.uri); err != nil { if _, err := f.view.parse(ctx, f); err != nil {
return nil return nil
} }
} }
@ -64,7 +73,7 @@ func (f *File) GetAST(ctx context.Context) *ast.File {
defer f.view.mu.Unlock() defer f.view.mu.Unlock()
if f.ast == nil || len(f.view.contentChanges) > 0 { if f.ast == nil || len(f.view.contentChanges) > 0 {
if _, err := f.view.parse(ctx, f.uri); err != nil { if _, err := f.view.parse(ctx, f); err != nil {
return nil return nil
} }
} }
@ -74,9 +83,8 @@ func (f *File) GetAST(ctx context.Context) *ast.File {
func (f *File) GetPackage(ctx context.Context) source.Package { func (f *File) GetPackage(ctx context.Context) source.Package {
f.view.mu.Lock() f.view.mu.Lock()
defer f.view.mu.Unlock() defer f.view.mu.Unlock()
if f.pkg == nil || len(f.view.contentChanges) > 0 { if f.pkg == nil || len(f.view.contentChanges) > 0 {
errs, err := f.view.parse(ctx, f.uri) errs, err := f.view.parse(ctx, f)
if err != nil { if err != nil {
// Create diagnostics for errors if we are able to. // Create diagnostics for errors if we are able to.
if len(errs) > 0 { if len(errs) > 0 {
@ -105,11 +113,7 @@ func (f *File) read(ctx context.Context) {
} }
} }
// We don't know the content yet, so read it. // We don't know the content yet, so read it.
filename, err := f.uri.Filename() content, err := ioutil.ReadFile(f.filename)
if err != nil {
return
}
content, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return return
} }

View File

@ -7,6 +7,7 @@ package cache
import ( import (
"context" "context"
"go/token" "go/token"
"os"
"sync" "sync"
"golang.org/x/tools/go/packages" "golang.org/x/tools/go/packages"
@ -30,8 +31,10 @@ type View struct {
// go/packages API. It is shared across all views. // go/packages API. It is shared across all views.
Config packages.Config Config packages.Config
// files caches information for opened files in a view. // keep track of files by uri and by basename, a single file may be mapped
files map[span.URI]*File // to multiple uris, and the same basename may map to multiple files
filesByURI map[span.URI]*File
filesByBase map[string][]*File
// contentChanges saves the content changes for a given state of the view. // contentChanges saves the content changes for a given state of the view.
// When type information is requested by the view, all of the dirty changes // When type information is requested by the view, all of the dirty changes
@ -76,7 +79,8 @@ func NewView(config *packages.Config) *View {
backgroundCtx: ctx, backgroundCtx: ctx,
cancel: cancel, cancel: cancel,
Config: *config, Config: *config,
files: make(map[span.URI]*File), filesByURI: make(map[span.URI]*File),
filesByBase: make(map[string][]*File),
contentChanges: make(map[span.URI]func()), contentChanges: make(map[span.URI]func()),
mcache: &metadataCache{ mcache: &metadataCache{
packages: make(map[string]*metadata), packages: make(map[string]*metadata),
@ -137,7 +141,10 @@ func (v *View) applyContentChanges(ctx context.Context) error {
// setContent applies a content update for a given file. It assumes that the // setContent applies a content update for a given file. It assumes that the
// caller is holding the view's mutex. // caller is holding the view's mutex.
func (v *View) applyContentChange(uri span.URI, content []byte) { func (v *View) applyContentChange(uri span.URI, content []byte) {
f := v.getFile(uri) f, err := v.getFile(uri)
if err != nil {
return
}
f.content = content f.content = content
// TODO(rstambler): Should we recompute these here? // TODO(rstambler): Should we recompute these here?
@ -153,16 +160,12 @@ func (v *View) applyContentChange(uri span.URI, content []byte) {
case f.active && content == nil: case f.active && content == nil:
// The file was active, so we need to forget its content. // The file was active, so we need to forget its content.
f.active = false f.active = false
if filename, err := f.uri.Filename(); err == nil { delete(f.view.Config.Overlay, f.filename)
delete(f.view.Config.Overlay, filename)
}
f.content = nil f.content = nil
case content != nil: case content != nil:
// This is an active overlay, so we update the map. // This is an active overlay, so we update the map.
f.active = true f.active = true
if filename, err := f.uri.Filename(); err == nil { f.view.Config.Overlay[f.filename] = f.content
f.view.Config.Overlay[filename] = f.content
}
} }
} }
@ -180,7 +183,7 @@ func (v *View) remove(pkgPath string) {
// All of the files in the package may also be holding a pointer to the // All of the files in the package may also be holding a pointer to the
// invalidated package. // invalidated package.
for _, filename := range m.files { for _, filename := range m.files {
if f, ok := v.files[span.FileURI(filename)]; ok { if f := v.findFile(span.FileURI(filename)); f != nil {
f.pkg = nil f.pkg = nil
} }
} }
@ -197,18 +200,61 @@ func (v *View) GetFile(ctx context.Context, uri span.URI) (source.File, error) {
return nil, ctx.Err() return nil, ctx.Err()
} }
return v.getFile(uri), nil return v.getFile(uri)
} }
// getFile is the unlocked internal implementation of GetFile. // getFile is the unlocked internal implementation of GetFile.
func (v *View) getFile(uri span.URI) *File { func (v *View) getFile(uri span.URI) (*File, error) {
f, found := v.files[uri] if f := v.findFile(uri); f != nil {
if !found { return f, nil
f = &File{ }
uri: uri, filename, err := uri.Filename()
view: v, if err != nil {
} return nil, err
v.files[uri] = f }
f := &File{
view: v,
filename: filename,
}
v.mapFile(uri, f)
return f, nil
}
func (v *View) findFile(uri span.URI) *File {
if f := v.filesByURI[uri]; f != nil {
// a perfect match
return f
}
// no exact match stored, time to do some real work
// check for any files with the same basename
fname, err := uri.Filename()
if err != nil {
return nil
}
basename := basename(fname)
if candidates := v.filesByBase[basename]; candidates != nil {
pathStat, err := os.Stat(fname)
if err == nil {
return nil
}
for _, c := range candidates {
if cStat, err := os.Stat(c.filename); err == nil {
if os.SameFile(pathStat, cStat) {
// same file, map it
v.mapFile(uri, c)
return c
}
}
}
}
return nil
}
func (v *View) mapFile(uri span.URI, f *File) {
v.filesByURI[uri] = f
f.uris = append(f.uris, uri)
if f.basename == "" {
f.basename = basename(f.filename)
v.filesByBase[f.basename] = append(v.filesByBase[f.basename], f)
} }
return f
} }

View File

@ -58,7 +58,7 @@ func Diagnostics(ctx context.Context, v View, uri span.URI) (map[span.URI][]Diag
} }
pkg := f.GetPackage(ctx) pkg := f.GetPackage(ctx)
if pkg == nil { if pkg == nil {
return nil, fmt.Errorf("no package found for %v", f.URI()) return nil, fmt.Errorf("diagnostics: no package found for %v", f.URI())
} }
// Prepare the reports we will send for this package. // Prepare the reports we will send for this package.
reports := make(map[span.URI][]Diagnostic) reports := make(map[span.URI][]Diagnostic)