1
0
mirror of https://github.com/golang/go synced 2024-10-01 07:38:32 -06:00
go/internal/lsp/cache/check.go
Muir Manders 1d0142ba47 internal/lsp: improve error handling while parsing
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>
2019-06-11 16:41:26 +00:00

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...)
}