mirror of
https://github.com/golang/go
synced 2024-10-01 07:38:32 -06:00
1d0142ba47
If the context is canceled (or times out) during parsing, we were previously caching the package with no *ast.Files. Any further LSP queries against that package would fail because the package is already loaded, but none of the files are mapped to the package. Fix by propagating non-parse errors as "fatal" errors in parseFiles. typeCheck will propagate these errors and not cache the package. I also fixed the package cache to not cache errors loading packages. If you get an error like "context canceled" then none of the package's files are mapped to the package. This prevents the package from ever getting unmapped when its files are updated. I also added a retry mechanism where if the current request is not canceled but the package failed to load due to a previous request being canceled, this request can try loading the package again. Updates golang/go#32354, golang/go#32360 Change-Id: I466ddb8d336aeecf6e50f9f6d040787a86a60ca0 GitHub-Last-Rev: 5f1e7ef9c883b76a9c1b3636936d91ec0821d922 GitHub-Pull-Request: golang/tools#110 Reviewed-on: https://go-review.googlesource.com/c/tools/+/181317 Run-TryBot: Rebecca Stambler <rstambler@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
233 lines
6.0 KiB
Go
233 lines
6.0 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"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
"golang.org/x/tools/go/packages"
|
|
"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[string]struct{}
|
|
|
|
// topLevelPkgID is the ID of the package from which type-checking began.
|
|
topLevelPkgID string
|
|
|
|
ctx context.Context
|
|
fset *token.FileSet
|
|
}
|
|
|
|
func (imp *importer) Import(pkgPath string) (*types.Package, error) {
|
|
pkg, err := imp.getPkg(pkgPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pkg.types, nil
|
|
}
|
|
|
|
func (imp *importer) getPkg(pkgPath string) (*pkg, error) {
|
|
if _, ok := imp.seen[pkgPath]; ok {
|
|
return nil, fmt.Errorf("circular import detected")
|
|
}
|
|
imp.view.pcache.mu.Lock()
|
|
e, ok := imp.view.pcache.packages[pkgPath]
|
|
|
|
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[pkgPath] = e
|
|
imp.view.pcache.mu.Unlock()
|
|
|
|
// This goroutine becomes responsible for populating
|
|
// the entry and broadcasting its readiness.
|
|
e.pkg, e.err = imp.typeCheck(pkgPath)
|
|
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 && imp.ctx.Err() == nil {
|
|
imp.view.pcache.mu.Lock()
|
|
// Clear out canceled cache entry if it is still there.
|
|
if imp.view.pcache.packages[pkgPath] == e {
|
|
delete(imp.view.pcache.packages, pkgPath)
|
|
}
|
|
imp.view.pcache.mu.Unlock()
|
|
return imp.getPkg(pkgPath)
|
|
}
|
|
return nil, e.err
|
|
}
|
|
|
|
return e.pkg, nil
|
|
}
|
|
|
|
func (imp *importer) typeCheck(pkgPath string) (*pkg, error) {
|
|
meta, ok := imp.view.mcache.packages[pkgPath]
|
|
if !ok {
|
|
return nil, fmt.Errorf("no metadata for %v", pkgPath)
|
|
}
|
|
// Use the default type information for the unsafe package.
|
|
var typ *types.Package
|
|
if meta.pkgPath == "unsafe" {
|
|
typ = types.Unsafe
|
|
} else {
|
|
typ = types.NewPackage(meta.pkgPath, meta.name)
|
|
}
|
|
pkg := &pkg{
|
|
id: meta.id,
|
|
pkgPath: meta.pkgPath,
|
|
files: meta.files,
|
|
imports: make(map[string]*pkg),
|
|
types: typ,
|
|
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),
|
|
}
|
|
appendError := func(err error) {
|
|
imp.view.appendPkgError(pkg, err)
|
|
}
|
|
|
|
// Ignore function bodies for any dependency packages.
|
|
ignoreFuncBodies := imp.topLevelPkgID != pkg.id
|
|
|
|
// Don't type-check function bodies if we are not in the top-level package.
|
|
files, parseErrs, err := imp.parseFiles(meta.files, ignoreFuncBodies)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, err := range parseErrs {
|
|
appendError(err)
|
|
}
|
|
|
|
// If something unexpected happens, don't cache a package with 0 parsed files.
|
|
if len(files) == 0 {
|
|
return nil, fmt.Errorf("no parsed files for package %s", pkg.pkgPath)
|
|
}
|
|
|
|
pkg.syntax = files
|
|
|
|
// Handle circular imports by copying previously seen imports.
|
|
seen := make(map[string]struct{})
|
|
for k, v := range imp.seen {
|
|
seen[k] = v
|
|
}
|
|
seen[pkgPath] = struct{}{}
|
|
|
|
cfg := &types.Config{
|
|
Error: appendError,
|
|
IgnoreFuncBodies: ignoreFuncBodies,
|
|
Importer: &importer{
|
|
view: imp.view,
|
|
ctx: imp.ctx,
|
|
fset: imp.fset,
|
|
topLevelPkgID: imp.topLevelPkgID,
|
|
seen: seen,
|
|
},
|
|
}
|
|
check := types.NewChecker(cfg, imp.fset, pkg.types, pkg.typesInfo)
|
|
check.Files(pkg.GetSyntax())
|
|
|
|
// Add every file in this package to our cache.
|
|
imp.cachePackage(imp.ctx, pkg, meta)
|
|
|
|
return pkg, nil
|
|
}
|
|
|
|
func (imp *importer) cachePackage(ctx context.Context, pkg *pkg, meta *metadata) {
|
|
for _, fAST := range pkg.syntax {
|
|
// TODO: If a file is in multiple packages, which package do we store?
|
|
if !fAST.file.Pos().IsValid() {
|
|
imp.view.Session().Logger().Errorf(ctx, "invalid position for file %v", fAST.file.Name)
|
|
continue
|
|
}
|
|
tok := imp.view.Session().Cache().FileSet().File(fAST.file.Pos())
|
|
if tok == nil {
|
|
imp.view.Session().Logger().Errorf(ctx, "no token.File for %v", fAST.file.Name)
|
|
continue
|
|
}
|
|
fURI := span.FileURI(tok.Name())
|
|
f, err := imp.view.getFile(fURI)
|
|
if err != nil {
|
|
imp.view.Session().Logger().Errorf(ctx, "no file: %v", err)
|
|
continue
|
|
}
|
|
gof, ok := f.(*goFile)
|
|
if !ok {
|
|
imp.view.Session().Logger().Errorf(ctx, "%v is not a Go file", f.URI())
|
|
continue
|
|
}
|
|
gof.token = tok
|
|
gof.ast = fAST
|
|
gof.imports = fAST.file.Imports
|
|
gof.pkg = pkg
|
|
}
|
|
|
|
// 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(importPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
pkg.imports[importPath] = importPkg
|
|
}
|
|
}
|
|
|
|
func (v *view) 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: v.Session().Cache().FileSet().Position(err.Pos).String(),
|
|
Msg: err.Msg,
|
|
Kind: packages.TypeError,
|
|
})
|
|
}
|
|
pkg.errors = append(pkg.errors, errs...)
|
|
}
|