mirror of
https://github.com/golang/go
synced 2024-11-18 20:44:45 -07:00
63f37bb4d3
If typeCheck() returned an error, we could get into a state where a package had an entry in the pcache, but the package's files had an empty "pkgs" map. When we got a DidChange event for one of the files, no packages would get invalidated since the file's "pkgs" was empty. This resulted in the cached typeCheck() error persisting indefinitely. Fix by never caching pcache entries on error. An easy way to reproduce the problem was to delete the package name from a file. For example, edit "package foo" to be just "package". This caused the package to get stuck with an "AST for %s has an invalid position" error. Change-Id: I330bf9e419852dffa0f2dee94b56226367488dd1 GitHub-Last-Rev: 18be7078521b942694c76f799a2d520eee47167d GitHub-Pull-Request: golang/tools#135 Reviewed-on: https://go-review.googlesource.com/c/tools/+/185839 Reviewed-by: Rebecca Stambler <rstambler@golang.org> Run-TryBot: Rebecca Stambler <rstambler@golang.org>
296 lines
7.5 KiB
Go
296 lines
7.5 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"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/scanner"
|
|
"go/token"
|
|
"go/types"
|
|
"sync"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
"golang.org/x/tools/go/packages"
|
|
"golang.org/x/tools/internal/lsp/source"
|
|
"golang.org/x/tools/internal/lsp/telemetry/trace"
|
|
"golang.org/x/tools/internal/span"
|
|
)
|
|
|
|
type importer struct {
|
|
view *view
|
|
|
|
// seen maintains the set of previously imported packages.
|
|
// If we have seen a package that is already in this map, we have a circular import.
|
|
seen map[packageID]struct{}
|
|
|
|
// topLevelPkgID is the ID of the package from which type-checking began.
|
|
topLevelPkgID packageID
|
|
|
|
ctx context.Context
|
|
fset *token.FileSet
|
|
}
|
|
|
|
func (imp *importer) Import(pkgPath string) (*types.Package, error) {
|
|
ctx := imp.ctx
|
|
id, ok := imp.view.mcache.ids[packagePath(pkgPath)]
|
|
if !ok {
|
|
return nil, fmt.Errorf("no known ID for %s", pkgPath)
|
|
}
|
|
pkg, err := imp.getPkg(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pkg.types, nil
|
|
}
|
|
|
|
func (imp *importer) getPkg(ctx context.Context, id packageID) (*pkg, error) {
|
|
if _, ok := imp.seen[id]; ok {
|
|
return nil, fmt.Errorf("circular import detected")
|
|
}
|
|
imp.view.pcache.mu.Lock()
|
|
e, ok := imp.view.pcache.packages[id]
|
|
|
|
if ok {
|
|
// cache hit
|
|
imp.view.pcache.mu.Unlock()
|
|
// wait for entry to become ready
|
|
<-e.ready
|
|
} else {
|
|
// cache miss
|
|
e = &entry{ready: make(chan struct{})}
|
|
imp.view.pcache.packages[id] = e
|
|
imp.view.pcache.mu.Unlock()
|
|
|
|
// This goroutine becomes responsible for populating
|
|
// the entry and broadcasting its readiness.
|
|
e.pkg, e.err = imp.typeCheck(ctx, id)
|
|
if e.err != nil {
|
|
// Don't cache failed packages. If we didn't successfully cache the package
|
|
// in each file, then this pcache entry won't get invalidated as those files
|
|
// change.
|
|
imp.view.pcache.mu.Lock()
|
|
if imp.view.pcache.packages[id] == e {
|
|
delete(imp.view.pcache.packages, id)
|
|
}
|
|
imp.view.pcache.mu.Unlock()
|
|
}
|
|
close(e.ready)
|
|
}
|
|
|
|
if e.err != nil {
|
|
// If the import had been previously canceled, and that error cached, try again.
|
|
if e.err == context.Canceled && ctx.Err() == nil {
|
|
return imp.getPkg(ctx, id)
|
|
}
|
|
return nil, e.err
|
|
}
|
|
|
|
return e.pkg, nil
|
|
}
|
|
|
|
func (imp *importer) typeCheck(ctx context.Context, id packageID) (*pkg, error) {
|
|
ctx, done := trace.StartSpan(ctx, "cache.importer.typeCheck")
|
|
defer done()
|
|
meta, ok := imp.view.mcache.packages[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("no metadata for %v", id)
|
|
}
|
|
pkg := &pkg{
|
|
id: meta.id,
|
|
pkgPath: meta.pkgPath,
|
|
imports: make(map[packagePath]*pkg),
|
|
typesSizes: meta.typesSizes,
|
|
typesInfo: &types.Info{
|
|
Types: make(map[ast.Expr]types.TypeAndValue),
|
|
Defs: make(map[*ast.Ident]types.Object),
|
|
Uses: make(map[*ast.Ident]types.Object),
|
|
Implicits: make(map[ast.Node]types.Object),
|
|
Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
|
Scopes: make(map[ast.Node]*types.Scope),
|
|
},
|
|
analyses: make(map[*analysis.Analyzer]*analysisEntry),
|
|
}
|
|
|
|
// Ignore function bodies for any dependency packages.
|
|
mode := source.ParseFull
|
|
if imp.topLevelPkgID != pkg.id {
|
|
mode = source.ParseExported
|
|
}
|
|
var (
|
|
files []*astFile
|
|
phs []source.ParseGoHandle
|
|
wg sync.WaitGroup
|
|
)
|
|
for _, filename := range meta.files {
|
|
uri := span.FileURI(filename)
|
|
f, err := imp.view.getFile(ctx, uri)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
ph := imp.view.session.cache.ParseGoHandle(f.Handle(ctx), mode)
|
|
phs = append(phs, ph)
|
|
files = append(files, &astFile{
|
|
uri: ph.File().Identity().URI,
|
|
isTrimmed: mode == source.ParseExported,
|
|
ph: ph,
|
|
})
|
|
}
|
|
for i, ph := range phs {
|
|
wg.Add(1)
|
|
go func(i int, ph source.ParseGoHandle) {
|
|
defer wg.Done()
|
|
|
|
files[i].file, files[i].err = ph.Parse(ctx)
|
|
}(i, ph)
|
|
}
|
|
wg.Wait()
|
|
|
|
for _, f := range files {
|
|
pkg.files = append(pkg.files, f)
|
|
|
|
if f.err != nil {
|
|
if f.err == context.Canceled {
|
|
return nil, f.err
|
|
}
|
|
imp.view.session.cache.appendPkgError(pkg, f.err)
|
|
}
|
|
}
|
|
|
|
// Use the default type information for the unsafe package.
|
|
if meta.pkgPath == "unsafe" {
|
|
pkg.types = types.Unsafe
|
|
} else if len(files) == 0 { // not the unsafe package, no parsed files
|
|
return nil, fmt.Errorf("no parsed files for package %s", pkg.pkgPath)
|
|
} else {
|
|
pkg.types = types.NewPackage(string(meta.pkgPath), meta.name)
|
|
}
|
|
|
|
// Handle circular imports by copying previously seen imports.
|
|
seen := make(map[packageID]struct{})
|
|
for k, v := range imp.seen {
|
|
seen[k] = v
|
|
}
|
|
seen[id] = struct{}{}
|
|
|
|
cfg := &types.Config{
|
|
Error: func(err error) {
|
|
imp.view.session.cache.appendPkgError(pkg, err)
|
|
},
|
|
IgnoreFuncBodies: mode == source.ParseExported,
|
|
Importer: &importer{
|
|
view: imp.view,
|
|
ctx: ctx,
|
|
fset: imp.fset,
|
|
topLevelPkgID: imp.topLevelPkgID,
|
|
seen: seen,
|
|
},
|
|
}
|
|
check := types.NewChecker(cfg, imp.fset, pkg.types, pkg.typesInfo)
|
|
|
|
// Ignore type-checking errors.
|
|
check.Files(pkg.GetSyntax())
|
|
|
|
// Add every file in this package to our cache.
|
|
if err := imp.cachePackage(ctx, pkg, meta, mode); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return pkg, nil
|
|
}
|
|
|
|
func (imp *importer) cachePackage(ctx context.Context, pkg *pkg, meta *metadata, mode source.ParseMode) error {
|
|
for _, file := range pkg.files {
|
|
f, err := imp.view.getFile(ctx, file.uri)
|
|
if err != nil {
|
|
return fmt.Errorf("no such file %s: %v", file.uri, err)
|
|
}
|
|
gof, ok := f.(*goFile)
|
|
if !ok {
|
|
return fmt.Errorf("non Go file %s", file.uri)
|
|
}
|
|
if err := imp.cachePerFile(gof, file, pkg); err != nil {
|
|
return fmt.Errorf("failed to cache file %s: %v", gof.URI(), err)
|
|
}
|
|
}
|
|
|
|
// Set imports of package to correspond to cached packages.
|
|
// We lock the package cache, but we shouldn't get any inconsistencies
|
|
// because we are still holding the lock on the view.
|
|
for importPath := range meta.children {
|
|
importPkg, err := imp.getPkg(ctx, importPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
pkg.imports[importPkg.pkgPath] = importPkg
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (imp *importer) cachePerFile(gof *goFile, file *astFile, p *pkg) error {
|
|
gof.mu.Lock()
|
|
defer gof.mu.Unlock()
|
|
|
|
// Set the package even if we failed to parse the file.
|
|
if gof.pkgs == nil {
|
|
gof.pkgs = make(map[packageID]*pkg)
|
|
}
|
|
gof.pkgs[p.id] = p
|
|
|
|
// Get the AST for the file.
|
|
gof.ast = file
|
|
if gof.ast == nil {
|
|
return fmt.Errorf("no AST information for %s", file.uri)
|
|
}
|
|
if gof.ast.file == nil {
|
|
return fmt.Errorf("no AST for %s", file.uri)
|
|
}
|
|
// Get the *token.File directly from the AST.
|
|
pos := gof.ast.file.Pos()
|
|
if !pos.IsValid() {
|
|
return fmt.Errorf("AST for %s has an invalid position", file.uri)
|
|
}
|
|
tok := imp.view.session.cache.FileSet().File(pos)
|
|
if tok == nil {
|
|
return fmt.Errorf("no *token.File for %s", file.uri)
|
|
}
|
|
gof.token = tok
|
|
gof.imports = gof.ast.file.Imports
|
|
return nil
|
|
}
|
|
|
|
func (c *cache) appendPkgError(pkg *pkg, err error) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
var errs []packages.Error
|
|
switch err := err.(type) {
|
|
case *scanner.Error:
|
|
errs = append(errs, packages.Error{
|
|
Pos: err.Pos.String(),
|
|
Msg: err.Msg,
|
|
Kind: packages.ParseError,
|
|
})
|
|
case scanner.ErrorList:
|
|
// The first parser error is likely the root cause of the problem.
|
|
if err.Len() > 0 {
|
|
errs = append(errs, packages.Error{
|
|
Pos: err[0].Pos.String(),
|
|
Msg: err[0].Msg,
|
|
Kind: packages.ParseError,
|
|
})
|
|
}
|
|
case types.Error:
|
|
errs = append(errs, packages.Error{
|
|
Pos: c.FileSet().Position(err.Pos).String(),
|
|
Msg: err.Msg,
|
|
Kind: packages.TypeError,
|
|
})
|
|
}
|
|
pkg.errors = append(pkg.errors, errs...)
|
|
}
|