1
0
mirror of https://github.com/golang/go synced 2024-11-18 22:34:45 -07:00
go/internal/lsp/cache/gofile.go
Rebecca Stambler 4f238b926e internal/lsp: fix deadlock between f.mu and f.handleMu
This change propagates the file handle through the type-checking
process, ensuring that the same handle is used throughout. It also
removes the ordering constraint that f.mu needs to be acquired before
f.handleMu. To make this more correct, we should associate a cached
package only with a FileHandle, but this relies on correct cache
invalidation, so that will be addressed in future changes.

Updates golang/go#34052

Change-Id: I6645046bfd15c882619a7f01f9b48c838de42a30
Reviewed-on: https://go-review.googlesource.com/c/tools/+/193718
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
2019-09-05 22:17:16 +00:00

302 lines
7.6 KiB
Go

// 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"
"go/ast"
"go/token"
"sync"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/telemetry"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/log"
errors "golang.org/x/xerrors"
)
// goFile holds all of the information we know about a Go file.
type goFile struct {
fileBase
// mu protects all mutable state of the Go file,
// which can be modified during type-checking.
mu sync.Mutex
// missingImports is the set of unresolved imports for this package.
// It contains any packages with `go list` errors.
missingImports map[packagePath]struct{}
// justOpened indicates that the file has just been opened.
// We re-run go/packages.Load on just opened files to make sure
// that we know about all of their packages.
justOpened bool
imports []*ast.ImportSpec
pkgs map[packageID]source.CheckPackageHandle
meta map[packageID]*metadata
}
// metadata assumes that the caller holds the f.mu lock.
func (f *goFile) metadata() []*metadata {
result := make([]*metadata, 0, len(f.meta))
for _, m := range f.meta {
result = append(result, m)
}
return result
}
func (f *goFile) GetToken(ctx context.Context) (*token.File, error) {
file, err := f.GetAST(ctx, source.ParseFull)
if file == nil {
return nil, err
}
tok := f.view.session.cache.fset.File(file.Pos())
if tok == nil {
return nil, errors.Errorf("no token.File for %s", f.URI())
}
return tok, nil
}
func (f *goFile) GetAST(ctx context.Context, mode source.ParseMode) (*ast.File, error) {
ctx = telemetry.File.With(ctx, f.URI())
fh := f.Handle(ctx)
if f.isDirty(ctx, fh) || f.wrongParseMode(ctx, fh, mode) {
if err := f.view.loadParseTypecheck(ctx, f, fh); err != nil {
return nil, err
}
}
// Check for a cached AST first, in case getting a trimmed version would actually cause a re-parse.
cached, err := f.view.session.cache.cachedAST(fh, mode)
if cached != nil || err != nil {
return cached, err
}
ph := f.view.session.cache.ParseGoHandle(fh, mode)
return ph.Parse(ctx)
}
func (cache *cache) cachedAST(fh source.FileHandle, mode source.ParseMode) (*ast.File, error) {
for _, m := range []source.ParseMode{
source.ParseHeader,
source.ParseExported,
source.ParseFull,
} {
if m < mode {
continue
}
if v, ok := cache.store.Cached(parseKey{
file: fh.Identity(),
mode: m,
}).(*parseGoData); ok {
return v.ast, v.err
}
}
return nil, nil
}
func (f *goFile) GetPackages(ctx context.Context) ([]source.Package, error) {
cphs, err := f.GetCheckPackageHandles(ctx)
if err != nil {
return nil, err
}
var pkgs []source.Package
for _, cph := range cphs {
pkg, err := cph.Check(ctx)
if err != nil {
log.Error(ctx, "failed to check package", err)
}
pkgs = append(pkgs, pkg)
}
if len(pkgs) == 0 {
return nil, errors.Errorf("no packages for %s", f.URI())
}
return pkgs, nil
}
func (f *goFile) GetPackage(ctx context.Context) (source.Package, error) {
cph, err := f.GetCheckPackageHandle(ctx)
if err != nil {
return nil, err
}
return cph.Check(ctx)
}
func (f *goFile) GetCheckPackageHandles(ctx context.Context) ([]source.CheckPackageHandle, error) {
ctx = telemetry.File.With(ctx, f.URI())
fh := f.Handle(ctx)
if f.isDirty(ctx, fh) || f.wrongParseMode(ctx, fh, source.ParseFull) {
if err := f.view.loadParseTypecheck(ctx, f, fh); err != nil {
return nil, err
}
}
f.mu.Lock()
defer f.mu.Unlock()
var cphs []source.CheckPackageHandle
for _, cph := range f.pkgs {
cphs = append(cphs, cph)
}
if len(cphs) == 0 {
return nil, errors.Errorf("no CheckPackageHandles for %s", f.URI())
}
return cphs, nil
}
func (f *goFile) GetCheckPackageHandle(ctx context.Context) (source.CheckPackageHandle, error) {
cphs, err := f.GetCheckPackageHandles(ctx)
if err != nil {
return nil, err
}
return bestCheckPackageHandle(f.URI(), cphs)
}
func (f *goFile) GetCachedPackage(ctx context.Context) (source.Package, error) {
f.mu.Lock()
var cphs []source.CheckPackageHandle
for _, cph := range f.pkgs {
cphs = append(cphs, cph)
}
f.mu.Unlock()
if len(cphs) == 0 {
return nil, errors.Errorf("no CheckPackageHandles for %s", f.URI())
}
cph, err := bestCheckPackageHandle(f.URI(), cphs)
if err != nil {
return nil, err
}
return cph.Cached(ctx)
}
// bestCheckPackageHandle picks the "narrowest" package for a given file.
//
// By "narrowest" package, we mean the package with the fewest number of files
// that includes the given file. This solves the problem of test variants,
// as the test will have more files than the non-test package.
func bestCheckPackageHandle(uri span.URI, cphs []source.CheckPackageHandle) (source.CheckPackageHandle, error) {
var result source.CheckPackageHandle
for _, cph := range cphs {
if result == nil || len(cph.Files()) < len(result.Files()) {
result = cph
}
}
if result == nil {
return nil, errors.Errorf("no CheckPackageHandle for %s", uri)
}
return result, nil
}
func (f *goFile) wrongParseMode(ctx context.Context, fh source.FileHandle, mode source.ParseMode) bool {
f.mu.Lock()
defer f.mu.Unlock()
for _, cph := range f.pkgs {
for _, ph := range cph.Files() {
if fh.Identity() == ph.File().Identity() {
return ph.Mode() < mode
}
}
}
return true
}
func (f *goFile) Builtin() (*ast.File, bool) {
builtinPkg := f.View().BuiltinPackage()
for filename, file := range builtinPkg.Files {
if filename == f.URI().Filename() {
return file, true
}
}
return nil, false
}
// isDirty is true if the file needs to be type-checked.
// It assumes that the file's view's mutex is held by the caller.
func (f *goFile) isDirty(ctx context.Context, fh source.FileHandle) bool {
f.mu.Lock()
defer f.mu.Unlock()
// If the the file has just been opened,
// it may be part of more packages than we are aware of.
//
// Note: This must be the first case, otherwise we may not reset the value of f.justOpened.
if f.justOpened {
f.meta = make(map[packageID]*metadata)
f.pkgs = make(map[packageID]source.CheckPackageHandle)
f.justOpened = false
return true
}
if len(f.meta) == 0 || len(f.pkgs) == 0 {
return true
}
if len(f.missingImports) > 0 {
return true
}
for _, cph := range f.pkgs {
for _, file := range cph.Files() {
// There is a type-checked package for the current file handle.
if file.File().Identity() == fh.Identity() {
return false
}
}
}
return true
}
func (f *goFile) GetActiveReverseDeps(ctx context.Context) (files []source.GoFile) {
seen := make(map[packageID]struct{}) // visited packages
results := make(map[*goFile]struct{})
f.view.mu.Lock()
defer f.view.mu.Unlock()
f.view.mcache.mu.Lock()
defer f.view.mcache.mu.Unlock()
for _, m := range f.metadata() {
f.view.reverseDeps(ctx, seen, results, m.id)
for f := range results {
if f == nil {
continue
}
// Don't return any of the active files in this package.
f.mu.Lock()
_, ok := f.meta[m.id]
f.mu.Unlock()
if ok {
continue
}
files = append(files, f)
}
}
return files
}
func (v *view) reverseDeps(ctx context.Context, seen map[packageID]struct{}, results map[*goFile]struct{}, id packageID) {
if _, ok := seen[id]; ok {
return
}
seen[id] = struct{}{}
m, ok := v.mcache.packages[id]
if !ok {
return
}
for _, uri := range m.files {
// Call unlocked version of getFile since we hold the lock on the view.
if f, err := v.getFile(ctx, uri); err == nil && v.session.IsOpen(uri) {
results[f.(*goFile)] = struct{}{}
}
}
for parentID := range m.parents {
v.reverseDeps(ctx, seen, results, parentID)
}
}