mirror of
https://github.com/golang/go
synced 2024-11-19 00:14:39 -07: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>
338 lines
8.2 KiB
Go
338 lines
8.2 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/parser"
|
|
"go/scanner"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"golang.org/x/tools/internal/span"
|
|
)
|
|
|
|
func parseFile(fset *token.FileSet, filename string, src []byte) (*ast.File, error) {
|
|
return parser.ParseFile(fset, filename, src, parser.AllErrors|parser.ParseComments)
|
|
}
|
|
|
|
// We use a counting semaphore to limit
|
|
// the number of parallel I/O calls per process.
|
|
var ioLimit = make(chan bool, 20)
|
|
|
|
// parseFiles reads and parses the Go source files and returns the ASTs
|
|
// of the ones that could be at least partially parsed, along with a list
|
|
// parse errors encountered, and a fatal error that prevented parsing.
|
|
//
|
|
// Because files are scanned in parallel, the token.Pos
|
|
// positions of the resulting ast.Files are not ordered.
|
|
//
|
|
func (imp *importer) parseFiles(filenames []string, ignoreFuncBodies bool) ([]*astFile, []error, error) {
|
|
var (
|
|
wg sync.WaitGroup
|
|
mu sync.Mutex
|
|
n = len(filenames)
|
|
parsed = make([]*astFile, n)
|
|
errors = make([]error, n)
|
|
fatalErr error
|
|
)
|
|
|
|
setFatalErr := func(err error) {
|
|
mu.Lock()
|
|
fatalErr = err
|
|
mu.Unlock()
|
|
}
|
|
|
|
for i, filename := range filenames {
|
|
if err := imp.ctx.Err(); err != nil {
|
|
setFatalErr(err)
|
|
break
|
|
}
|
|
// First, check if we have already cached an AST for this file.
|
|
f, err := imp.view.findFile(span.FileURI(filename))
|
|
if err != nil {
|
|
setFatalErr(err)
|
|
break
|
|
}
|
|
if f == nil {
|
|
setFatalErr(fmt.Errorf("could not find file %s", filename))
|
|
break
|
|
}
|
|
|
|
gof, ok := f.(*goFile)
|
|
if !ok {
|
|
setFatalErr(fmt.Errorf("non-Go file in parse call: %s", filename))
|
|
break
|
|
}
|
|
wg.Add(1)
|
|
go func(i int, filename string) {
|
|
ioLimit <- true // wait
|
|
defer func() {
|
|
<-ioLimit // signal done
|
|
wg.Done()
|
|
}()
|
|
|
|
// If we already have a cached AST, reuse it.
|
|
// If the AST is trimmed, only use it if we are ignoring function bodies.
|
|
if gof.ast != nil && gof.ast.isTrimmed == ignoreFuncBodies {
|
|
parsed[i], errors[i] = gof.ast, gof.ast.err
|
|
return
|
|
}
|
|
|
|
// We don't have a cached AST for this file, so we read its content and parse it.
|
|
src, _, err := gof.Handle(imp.ctx).Read(imp.ctx)
|
|
if err != nil {
|
|
setFatalErr(err)
|
|
return
|
|
}
|
|
if src == nil {
|
|
setFatalErr(fmt.Errorf("no source for %v", filename))
|
|
return
|
|
}
|
|
|
|
// ParseFile may return a partial AST and an error.
|
|
f, err := parseFile(imp.fset, filename, src)
|
|
parsed[i], errors[i] = &astFile{
|
|
file: f,
|
|
err: err,
|
|
isTrimmed: ignoreFuncBodies,
|
|
}, err
|
|
|
|
if ignoreFuncBodies {
|
|
trimAST(f)
|
|
}
|
|
// Fix any badly parsed parts of the AST.
|
|
if f != nil {
|
|
tok := imp.fset.File(f.Pos())
|
|
imp.view.fix(imp.ctx, f, tok, src)
|
|
}
|
|
}(i, filename)
|
|
}
|
|
wg.Wait()
|
|
|
|
if fatalErr != nil {
|
|
return nil, nil, fatalErr
|
|
}
|
|
|
|
// Eliminate nils, preserving order.
|
|
var o int
|
|
for _, f := range parsed {
|
|
if f != nil {
|
|
parsed[o] = f
|
|
o++
|
|
}
|
|
}
|
|
parsed = parsed[:o]
|
|
|
|
o = 0
|
|
for _, err := range errors {
|
|
if err != nil {
|
|
errors[o] = err
|
|
o++
|
|
}
|
|
}
|
|
errors = errors[:o]
|
|
|
|
return parsed, errors, nil
|
|
}
|
|
|
|
// sameFile returns true if x and y have the same basename and denote
|
|
// the same file.
|
|
//
|
|
func sameFile(x, y string) bool {
|
|
if x == y {
|
|
// It could be the case that y doesn't exist.
|
|
// For instance, it may be an overlay file that
|
|
// hasn't been written to disk. To handle that case
|
|
// let x == y through. (We added the exact absolute path
|
|
// string to the CompiledGoFiles list, so the unwritten
|
|
// overlay case implies x==y.)
|
|
return true
|
|
}
|
|
if strings.EqualFold(filepath.Base(x), filepath.Base(y)) { // (optimisation)
|
|
if xi, err := os.Stat(x); err == nil {
|
|
if yi, err := os.Stat(y); err == nil {
|
|
return os.SameFile(xi, yi)
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// trimAST clears any part of the AST not relevant to type checking
|
|
// expressions at pos.
|
|
func trimAST(file *ast.File) {
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
|
if n == nil {
|
|
return false
|
|
}
|
|
switch n := n.(type) {
|
|
case *ast.FuncDecl:
|
|
n.Body = nil
|
|
case *ast.BlockStmt:
|
|
n.List = nil
|
|
case *ast.CaseClause:
|
|
n.Body = nil
|
|
case *ast.CommClause:
|
|
n.Body = nil
|
|
case *ast.CompositeLit:
|
|
// Leave elts in place for [...]T
|
|
// array literals, because they can
|
|
// affect the expression's type.
|
|
if !isEllipsisArray(n.Type) {
|
|
n.Elts = nil
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
func isEllipsisArray(n ast.Expr) bool {
|
|
at, ok := n.(*ast.ArrayType)
|
|
if !ok {
|
|
return false
|
|
}
|
|
_, ok = at.Len.(*ast.Ellipsis)
|
|
return ok
|
|
}
|
|
|
|
// fix inspects and potentially modifies any *ast.BadStmts or *ast.BadExprs in the AST.
|
|
// We attempt to modify the AST such that we can type-check it more effectively.
|
|
func (v *view) fix(ctx context.Context, file *ast.File, tok *token.File, src []byte) {
|
|
var parent ast.Node
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
|
if n == nil {
|
|
return false
|
|
}
|
|
switch n := n.(type) {
|
|
case *ast.BadStmt:
|
|
if err := v.parseDeferOrGoStmt(n, parent, tok, src); err != nil {
|
|
v.Session().Logger().Debugf(ctx, "unable to parse defer or go from *ast.BadStmt: %v", err)
|
|
}
|
|
return false
|
|
default:
|
|
parent = n
|
|
return true
|
|
}
|
|
})
|
|
}
|
|
|
|
// parseDeferOrGoStmt tries to parse an *ast.BadStmt into a defer or a go statement.
|
|
//
|
|
// go/parser packages a statement of the form "defer x." as an *ast.BadStmt because
|
|
// it does not include a call expression. This means that go/types skips type-checking
|
|
// this statement entirely, and we can't use the type information when completing.
|
|
// Here, we try to generate a fake *ast.DeferStmt or *ast.GoStmt to put into the AST,
|
|
// instead of the *ast.BadStmt.
|
|
func (v *view) parseDeferOrGoStmt(bad *ast.BadStmt, parent ast.Node, tok *token.File, src []byte) error {
|
|
// Check if we have a bad statement containing either a "go" or "defer".
|
|
s := &scanner.Scanner{}
|
|
s.Init(tok, src, nil, 0)
|
|
|
|
var pos token.Pos
|
|
var tkn token.Token
|
|
var lit string
|
|
for {
|
|
if tkn == token.EOF {
|
|
return fmt.Errorf("reached the end of the file")
|
|
}
|
|
if pos >= bad.From {
|
|
break
|
|
}
|
|
pos, tkn, lit = s.Scan()
|
|
}
|
|
var stmt ast.Stmt
|
|
switch lit {
|
|
case "defer":
|
|
stmt = &ast.DeferStmt{
|
|
Defer: pos,
|
|
}
|
|
case "go":
|
|
stmt = &ast.GoStmt{
|
|
Go: pos,
|
|
}
|
|
default:
|
|
return fmt.Errorf("no defer or go statement found")
|
|
}
|
|
|
|
// The expression after the "defer" or "go" starts at this position.
|
|
from, _, _ := s.Scan()
|
|
var to, curr token.Pos
|
|
FindTo:
|
|
for {
|
|
curr, tkn, lit = s.Scan()
|
|
// TODO(rstambler): This still needs more handling to work correctly.
|
|
// We encounter a specific issue with code that looks like this:
|
|
//
|
|
// defer x.<>
|
|
// y := 1
|
|
//
|
|
// In this scenario, we parse it as "defer x.y", which then fails to
|
|
// type-check, and we don't get completions as expected.
|
|
switch tkn {
|
|
case token.COMMENT, token.EOF, token.SEMICOLON, token.DEFINE:
|
|
break FindTo
|
|
}
|
|
// to is the end of expression that should become the Fun part of the call.
|
|
to = curr
|
|
}
|
|
if !from.IsValid() || tok.Offset(from) >= len(src) {
|
|
return fmt.Errorf("invalid from position")
|
|
}
|
|
if !to.IsValid() || tok.Offset(to)+1 >= len(src) {
|
|
return fmt.Errorf("invalid to position")
|
|
}
|
|
exprstr := string(src[tok.Offset(from) : tok.Offset(to)+1])
|
|
expr, err := parser.ParseExpr(exprstr)
|
|
if expr == nil {
|
|
return fmt.Errorf("no expr in %s: %v", exprstr, err)
|
|
}
|
|
// parser.ParseExpr returns undefined positions.
|
|
// Adjust them for the current file.
|
|
v.offsetPositions(expr, from-1)
|
|
|
|
// Package the expression into a fake *ast.CallExpr and re-insert into the function.
|
|
call := &ast.CallExpr{
|
|
Fun: expr,
|
|
Lparen: to,
|
|
Rparen: to,
|
|
}
|
|
switch stmt := stmt.(type) {
|
|
case *ast.DeferStmt:
|
|
stmt.Call = call
|
|
case *ast.GoStmt:
|
|
stmt.Call = call
|
|
}
|
|
switch parent := parent.(type) {
|
|
case *ast.BlockStmt:
|
|
for i, s := range parent.List {
|
|
if s == bad {
|
|
parent.List[i] = stmt
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// offsetPositions applies an offset to the positions in an ast.Node.
|
|
// TODO(rstambler): Add more cases here as they become necessary.
|
|
func (v *view) offsetPositions(expr ast.Expr, offset token.Pos) {
|
|
ast.Inspect(expr, func(n ast.Node) bool {
|
|
switch n := n.(type) {
|
|
case *ast.Ident:
|
|
n.NamePos += offset
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
})
|
|
}
|