mirror of
https://github.com/golang/go
synced 2024-11-05 18:36:10 -07:00
9176cd3008
While looking at Kubernetes I noticed that golang.org/x/text packages were some of the largest. The problem is the large code-generated tables, which use ellipsis array literals. Teach gopls to trim the cases that matter there. While silly, this trims ~60MB off the live heap, so I think it might be worth it. Change-Id: I0cfd80bd5fbc8703ac628312982af9c6ed871758 Reviewed-on: https://go-review.googlesource.com/c/tools/+/248180 Run-TryBot: Heschi Kreinick <heschi@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
1155 lines
28 KiB
Go
1155 lines
28 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 (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/scanner"
|
|
"go/token"
|
|
"reflect"
|
|
"strconv"
|
|
|
|
"golang.org/x/tools/internal/event"
|
|
"golang.org/x/tools/internal/lsp/debug/tag"
|
|
"golang.org/x/tools/internal/lsp/protocol"
|
|
"golang.org/x/tools/internal/lsp/source"
|
|
"golang.org/x/tools/internal/memoize"
|
|
"golang.org/x/tools/internal/span"
|
|
errors "golang.org/x/xerrors"
|
|
)
|
|
|
|
// parseKey uniquely identifies a parsed Go file.
|
|
type parseKey struct {
|
|
file source.FileIdentity
|
|
mode source.ParseMode
|
|
}
|
|
|
|
// astCacheKey is similar to parseKey, but is a distinct type because
|
|
// it is used to key a different value within the same map.
|
|
type astCacheKey parseKey
|
|
|
|
type parseGoHandle struct {
|
|
handle *memoize.Handle
|
|
file source.FileHandle
|
|
mode source.ParseMode
|
|
astCacheHandle *memoize.Handle
|
|
}
|
|
|
|
type parseGoData struct {
|
|
parsed *source.ParsedGoFile
|
|
|
|
// If true, we adjusted the AST to make it type check better, and
|
|
// it may not match the source code.
|
|
fixed bool
|
|
err error // any other errors
|
|
}
|
|
|
|
func (s *snapshot) parseGoHandle(ctx context.Context, fh source.FileHandle, mode source.ParseMode) *parseGoHandle {
|
|
key := parseKey{
|
|
file: fh.FileIdentity(),
|
|
mode: mode,
|
|
}
|
|
if pgh := s.getGoFile(key); pgh != nil {
|
|
return pgh
|
|
}
|
|
parseHandle := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
|
|
snapshot := arg.(*snapshot)
|
|
return parseGo(ctx, snapshot.view.session.cache.fset, fh, mode)
|
|
})
|
|
|
|
astHandle := s.generation.Bind(astCacheKey(key), func(ctx context.Context, arg memoize.Arg) interface{} {
|
|
snapshot := arg.(*snapshot)
|
|
return buildASTCache(ctx, snapshot, parseHandle)
|
|
})
|
|
|
|
pgh := &parseGoHandle{
|
|
handle: parseHandle,
|
|
file: fh,
|
|
mode: mode,
|
|
astCacheHandle: astHandle,
|
|
}
|
|
return s.addGoFile(key, pgh)
|
|
}
|
|
|
|
func (pgh *parseGoHandle) String() string {
|
|
return pgh.File().URI().Filename()
|
|
}
|
|
|
|
func (pgh *parseGoHandle) File() source.FileHandle {
|
|
return pgh.file
|
|
}
|
|
|
|
func (pgh *parseGoHandle) Mode() source.ParseMode {
|
|
return pgh.mode
|
|
}
|
|
|
|
func (s *snapshot) ParseGo(ctx context.Context, fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, error) {
|
|
pgh := s.parseGoHandle(ctx, fh, mode)
|
|
pgf, _, err := s.parseGo(ctx, pgh)
|
|
return pgf, err
|
|
}
|
|
|
|
func (s *snapshot) parseGo(ctx context.Context, pgh *parseGoHandle) (*source.ParsedGoFile, bool, error) {
|
|
d, err := pgh.handle.Get(ctx, s.generation, s)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
data := d.(*parseGoData)
|
|
return data.parsed, data.fixed, data.err
|
|
}
|
|
|
|
func (s *snapshot) PosToDecl(ctx context.Context, pgf *source.ParsedGoFile) (map[token.Pos]ast.Decl, error) {
|
|
fh, err := s.GetFile(ctx, pgf.URI)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pgh := s.parseGoHandle(ctx, fh, pgf.Mode)
|
|
d, err := pgh.astCacheHandle.Get(ctx, s.generation, s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data := d.(*astCacheData)
|
|
return data.posToDecl, data.err
|
|
}
|
|
|
|
func (s *snapshot) PosToField(ctx context.Context, pgf *source.ParsedGoFile) (map[token.Pos]*ast.Field, error) {
|
|
fh, err := s.GetFile(ctx, pgf.URI)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pgh := s.parseGoHandle(ctx, fh, pgf.Mode)
|
|
d, err := pgh.astCacheHandle.Get(ctx, s.generation, s)
|
|
if err != nil || d == nil {
|
|
return nil, err
|
|
}
|
|
|
|
data := d.(*astCacheData)
|
|
return data.posToField, data.err
|
|
}
|
|
|
|
type astCacheData struct {
|
|
err error
|
|
|
|
posToDecl map[token.Pos]ast.Decl
|
|
posToField map[token.Pos]*ast.Field
|
|
}
|
|
|
|
// buildASTCache builds caches to aid in quickly going from the typed
|
|
// world to the syntactic world.
|
|
func buildASTCache(ctx context.Context, snapshot *snapshot, parseHandle *memoize.Handle) *astCacheData {
|
|
var (
|
|
// path contains all ancestors, including n.
|
|
path []ast.Node
|
|
// decls contains all ancestors that are decls.
|
|
decls []ast.Decl
|
|
)
|
|
|
|
v, err := parseHandle.Get(ctx, snapshot.generation, snapshot)
|
|
if err != nil {
|
|
return &astCacheData{err: err}
|
|
}
|
|
file := v.(*parseGoData).parsed.File
|
|
if err != nil {
|
|
return &astCacheData{err: fmt.Errorf("nil file")}
|
|
}
|
|
|
|
data := &astCacheData{
|
|
posToDecl: make(map[token.Pos]ast.Decl),
|
|
posToField: make(map[token.Pos]*ast.Field),
|
|
}
|
|
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
|
if n == nil {
|
|
lastP := path[len(path)-1]
|
|
path = path[:len(path)-1]
|
|
if len(decls) > 0 && decls[len(decls)-1] == lastP {
|
|
decls = decls[:len(decls)-1]
|
|
}
|
|
return false
|
|
}
|
|
|
|
path = append(path, n)
|
|
|
|
switch n := n.(type) {
|
|
case *ast.Field:
|
|
addField := func(f ast.Node) {
|
|
if f.Pos().IsValid() {
|
|
data.posToField[f.Pos()] = n
|
|
if len(decls) > 0 {
|
|
data.posToDecl[f.Pos()] = decls[len(decls)-1]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add mapping for *ast.Field itself. This handles embedded
|
|
// fields which have no associated *ast.Ident name.
|
|
addField(n)
|
|
|
|
// Add mapping for each field name since you can have
|
|
// multiple names for the same type expression.
|
|
for _, name := range n.Names {
|
|
addField(name)
|
|
}
|
|
|
|
// Also map "X" in "...X" to the containing *ast.Field. This
|
|
// makes it easy to format variadic signature params
|
|
// properly.
|
|
if elips, ok := n.Type.(*ast.Ellipsis); ok && elips.Elt != nil {
|
|
addField(elips.Elt)
|
|
}
|
|
case *ast.FuncDecl:
|
|
decls = append(decls, n)
|
|
|
|
if n.Name != nil && n.Name.Pos().IsValid() {
|
|
data.posToDecl[n.Name.Pos()] = n
|
|
}
|
|
case *ast.GenDecl:
|
|
decls = append(decls, n)
|
|
|
|
for _, spec := range n.Specs {
|
|
switch spec := spec.(type) {
|
|
case *ast.TypeSpec:
|
|
if spec.Name != nil && spec.Name.Pos().IsValid() {
|
|
data.posToDecl[spec.Name.Pos()] = n
|
|
}
|
|
case *ast.ValueSpec:
|
|
for _, id := range spec.Names {
|
|
if id != nil && id.Pos().IsValid() {
|
|
data.posToDecl[id.Pos()] = n
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
return data
|
|
}
|
|
|
|
func parseGo(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mode source.ParseMode) *parseGoData {
|
|
ctx, done := event.Start(ctx, "cache.parseGo", tag.File.Of(fh.URI().Filename()))
|
|
defer done()
|
|
|
|
if fh.Kind() != source.Go {
|
|
return &parseGoData{err: errors.Errorf("cannot parse non-Go file %s", fh.URI())}
|
|
}
|
|
buf, err := fh.Read()
|
|
if err != nil {
|
|
return &parseGoData{err: err}
|
|
}
|
|
|
|
parserMode := parser.AllErrors | parser.ParseComments
|
|
if mode == source.ParseHeader {
|
|
parserMode = parser.ImportsOnly | parser.ParseComments
|
|
}
|
|
file, parseError := parser.ParseFile(fset, fh.URI().Filename(), buf, parserMode)
|
|
var tok *token.File
|
|
var fixed bool
|
|
if file != nil {
|
|
tok = fset.File(file.Pos())
|
|
if tok == nil {
|
|
tok = fset.AddFile(fh.URI().Filename(), -1, len(buf))
|
|
tok.SetLinesForContent(buf)
|
|
return &parseGoData{
|
|
parsed: &source.ParsedGoFile{
|
|
URI: fh.URI(),
|
|
Mode: mode,
|
|
Src: buf,
|
|
File: file,
|
|
Tok: tok,
|
|
Mapper: &protocol.ColumnMapper{
|
|
URI: fh.URI(),
|
|
Content: buf,
|
|
Converter: span.NewTokenConverter(fset, tok),
|
|
},
|
|
ParseErr: parseError,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Fix any badly parsed parts of the AST.
|
|
fixed = fixAST(ctx, file, tok, buf)
|
|
|
|
// Fix certain syntax errors that render the file unparseable.
|
|
newSrc := fixSrc(file, tok, buf)
|
|
if newSrc != nil {
|
|
newFile, _ := parser.ParseFile(fset, fh.URI().Filename(), newSrc, parserMode)
|
|
if newFile != nil {
|
|
// Maintain the original parseError so we don't try formatting the doctored file.
|
|
file = newFile
|
|
buf = newSrc
|
|
tok = fset.File(file.Pos())
|
|
|
|
fixed = fixAST(ctx, file, tok, buf)
|
|
}
|
|
}
|
|
|
|
if mode == source.ParseExported {
|
|
trimAST(file)
|
|
}
|
|
}
|
|
// A missing file is always an error; use the parse error or make one up if there isn't one.
|
|
if file == nil {
|
|
if parseError == nil {
|
|
parseError = errors.Errorf("parsing %s failed with no parse error reported", fh.URI())
|
|
}
|
|
err = parseError
|
|
}
|
|
m := &protocol.ColumnMapper{
|
|
URI: fh.URI(),
|
|
Converter: span.NewTokenConverter(fset, tok),
|
|
Content: buf,
|
|
}
|
|
|
|
return &parseGoData{
|
|
parsed: &source.ParsedGoFile{
|
|
URI: fh.URI(),
|
|
Mode: mode,
|
|
Src: buf,
|
|
File: file,
|
|
Tok: tok,
|
|
Mapper: m,
|
|
ParseErr: parseError,
|
|
},
|
|
fixed: fixed,
|
|
err: err,
|
|
}
|
|
}
|
|
|
|
// 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:
|
|
// types.Info.Types for long slice/array literals are particularly
|
|
// expensive. Try to clear them out.
|
|
at, ok := n.Type.(*ast.ArrayType)
|
|
if !ok {
|
|
break
|
|
}
|
|
// Removing the elements from an ellipsis array changes its type.
|
|
// Try to set the length explicitly so we can continue.
|
|
if _, ok := at.Len.(*ast.Ellipsis); ok {
|
|
length, ok := arrayLength(n)
|
|
if !ok {
|
|
break
|
|
}
|
|
at.Len = &ast.BasicLit{
|
|
Kind: token.INT,
|
|
Value: fmt.Sprint(length),
|
|
ValuePos: at.Len.Pos(),
|
|
}
|
|
}
|
|
n.Elts = nil
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
// arrayLength returns the length of some simple forms of ellipsis array literal.
|
|
// Notably, it handles the tables in golang.org/x/text.
|
|
func arrayLength(array *ast.CompositeLit) (int, bool) {
|
|
litVal := func(expr ast.Expr) (int, bool) {
|
|
lit, ok := expr.(*ast.BasicLit)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
val, err := strconv.ParseInt(lit.Value, 10, 64)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return int(val), true
|
|
}
|
|
largestKey := -1
|
|
for _, elt := range array.Elts {
|
|
kve, ok := elt.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch key := kve.Key.(type) {
|
|
case *ast.BasicLit:
|
|
if val, ok := litVal(key); ok && largestKey < val {
|
|
largestKey = val
|
|
}
|
|
case *ast.BinaryExpr:
|
|
// golang.org/x/text uses subtraction (and only subtraction) in its indices.
|
|
if key.Op != token.SUB {
|
|
break
|
|
}
|
|
x, ok := litVal(key.X)
|
|
if !ok {
|
|
break
|
|
}
|
|
y, ok := litVal(key.Y)
|
|
if !ok {
|
|
break
|
|
}
|
|
if val := x - y; largestKey < val {
|
|
largestKey = val
|
|
}
|
|
}
|
|
}
|
|
if largestKey != -1 {
|
|
return largestKey + 1, true
|
|
}
|
|
return len(array.Elts), true
|
|
}
|
|
|
|
// fixAST inspects the AST and potentially modifies any *ast.BadStmts so that it can be
|
|
// type-checked more effectively.
|
|
func fixAST(ctx context.Context, n ast.Node, tok *token.File, src []byte) (fixed bool) {
|
|
var err error
|
|
walkASTWithParent(n, func(n, parent ast.Node) bool {
|
|
switch n := n.(type) {
|
|
case *ast.BadStmt:
|
|
if fixed = fixDeferOrGoStmt(n, parent, tok, src); fixed {
|
|
// Recursively fix in our fixed node.
|
|
_ = fixAST(ctx, parent, tok, src)
|
|
} else {
|
|
err = errors.Errorf("unable to parse defer or go from *ast.BadStmt: %v", err)
|
|
}
|
|
return false
|
|
case *ast.BadExpr:
|
|
if fixed = fixArrayType(n, parent, tok, src); fixed {
|
|
// Recursively fix in our fixed node.
|
|
_ = fixAST(ctx, parent, tok, src)
|
|
return false
|
|
}
|
|
|
|
// Fix cases where parser interprets if/for/switch "init"
|
|
// statement as "cond" expression, e.g.:
|
|
//
|
|
// // "i := foo" is init statement, not condition.
|
|
// for i := foo
|
|
//
|
|
fixInitStmt(n, parent, tok, src)
|
|
|
|
return false
|
|
case *ast.SelectorExpr:
|
|
// Fix cases where a keyword prefix results in a phantom "_" selector, e.g.:
|
|
//
|
|
// foo.var<> // want to complete to "foo.variance"
|
|
//
|
|
fixPhantomSelector(n, tok, src)
|
|
return true
|
|
|
|
case *ast.BlockStmt:
|
|
switch parent.(type) {
|
|
case *ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.SelectStmt:
|
|
// Adjust closing curly brace of empty switch/select
|
|
// statements so we can complete inside them.
|
|
fixEmptySwitch(n, tok, src)
|
|
}
|
|
|
|
return true
|
|
default:
|
|
return true
|
|
}
|
|
})
|
|
return fixed
|
|
}
|
|
|
|
// walkASTWithParent walks the AST rooted at n. The semantics are
|
|
// similar to ast.Inspect except it does not call f(nil).
|
|
func walkASTWithParent(n ast.Node, f func(n ast.Node, parent ast.Node) bool) {
|
|
var ancestors []ast.Node
|
|
ast.Inspect(n, func(n ast.Node) (recurse bool) {
|
|
defer func() {
|
|
if recurse {
|
|
ancestors = append(ancestors, n)
|
|
}
|
|
}()
|
|
|
|
if n == nil {
|
|
ancestors = ancestors[:len(ancestors)-1]
|
|
return false
|
|
}
|
|
|
|
var parent ast.Node
|
|
if len(ancestors) > 0 {
|
|
parent = ancestors[len(ancestors)-1]
|
|
}
|
|
|
|
return f(n, parent)
|
|
})
|
|
}
|
|
|
|
// fixSrc attempts to modify the file's source code to fix certain
|
|
// syntax errors that leave the rest of the file unparsed.
|
|
func fixSrc(f *ast.File, tok *token.File, src []byte) (newSrc []byte) {
|
|
walkASTWithParent(f, func(n, parent ast.Node) bool {
|
|
if newSrc != nil {
|
|
return false
|
|
}
|
|
|
|
switch n := n.(type) {
|
|
case *ast.BlockStmt:
|
|
newSrc = fixMissingCurlies(f, n, parent, tok, src)
|
|
case *ast.SelectorExpr:
|
|
newSrc = fixDanglingSelector(n, tok, src)
|
|
}
|
|
|
|
return newSrc == nil
|
|
})
|
|
|
|
return newSrc
|
|
}
|
|
|
|
// fixMissingCurlies adds in curly braces for block statements that
|
|
// are missing curly braces. For example:
|
|
//
|
|
// if foo
|
|
//
|
|
// becomes
|
|
//
|
|
// if foo {}
|
|
func fixMissingCurlies(f *ast.File, b *ast.BlockStmt, parent ast.Node, tok *token.File, src []byte) []byte {
|
|
// If the "{" is already in the source code, there isn't anything to
|
|
// fix since we aren't missing curlies.
|
|
if b.Lbrace.IsValid() {
|
|
braceOffset := tok.Offset(b.Lbrace)
|
|
if braceOffset < len(src) && src[braceOffset] == '{' {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
parentLine := tok.Line(parent.Pos())
|
|
|
|
if parentLine >= tok.LineCount() {
|
|
// If we are the last line in the file, no need to fix anything.
|
|
return nil
|
|
}
|
|
|
|
// Insert curlies at the end of parent's starting line. The parent
|
|
// is the statement that contains the block, e.g. *ast.IfStmt. The
|
|
// block's Pos()/End() can't be relied upon because they are based
|
|
// on the (missing) curly braces. We assume the statement is a
|
|
// single line for now and try sticking the curly braces at the end.
|
|
insertPos := tok.LineStart(parentLine+1) - 1
|
|
|
|
// Scootch position backwards until it's not in a comment. For example:
|
|
//
|
|
// if foo<> // some amazing comment |
|
|
// someOtherCode()
|
|
//
|
|
// insertPos will be located at "|", so we back it out of the comment.
|
|
didSomething := true
|
|
for didSomething {
|
|
didSomething = false
|
|
for _, c := range f.Comments {
|
|
if c.Pos() < insertPos && insertPos <= c.End() {
|
|
insertPos = c.Pos()
|
|
didSomething = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bail out if line doesn't end in an ident or ".". This is to avoid
|
|
// cases like below where we end up making things worse by adding
|
|
// curlies:
|
|
//
|
|
// if foo &&
|
|
// bar<>
|
|
switch precedingToken(insertPos, tok, src) {
|
|
case token.IDENT, token.PERIOD:
|
|
// ok
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
buf.Grow(len(src) + 3)
|
|
buf.Write(src[:tok.Offset(insertPos)])
|
|
|
|
// Detect if we need to insert a semicolon to fix "for" loop situations like:
|
|
//
|
|
// for i := foo(); foo<>
|
|
//
|
|
// Just adding curlies is not sufficient to make things parse well.
|
|
if fs, ok := parent.(*ast.ForStmt); ok {
|
|
if _, ok := fs.Cond.(*ast.BadExpr); !ok {
|
|
if xs, ok := fs.Post.(*ast.ExprStmt); ok {
|
|
if _, ok := xs.X.(*ast.BadExpr); ok {
|
|
buf.WriteByte(';')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Insert "{}" at insertPos.
|
|
buf.WriteByte('{')
|
|
buf.WriteByte('}')
|
|
buf.Write(src[tok.Offset(insertPos):])
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// fixEmptySwitch moves empty switch/select statements' closing curly
|
|
// brace down one line. This allows us to properly detect incomplete
|
|
// "case" and "default" keywords as inside the switch statement. For
|
|
// example:
|
|
//
|
|
// switch {
|
|
// def<>
|
|
// }
|
|
//
|
|
// gets parsed like:
|
|
//
|
|
// switch {
|
|
// }
|
|
//
|
|
// Later we manually pull out the "def" token, but we need to detect
|
|
// that our "<>" position is inside the switch block. To do that we
|
|
// move the curly brace so it looks like:
|
|
//
|
|
// switch {
|
|
//
|
|
// }
|
|
//
|
|
func fixEmptySwitch(body *ast.BlockStmt, tok *token.File, src []byte) {
|
|
// We only care about empty switch statements.
|
|
if len(body.List) > 0 || !body.Rbrace.IsValid() {
|
|
return
|
|
}
|
|
|
|
// If the right brace is actually in the source code at the
|
|
// specified position, don't mess with it.
|
|
braceOffset := tok.Offset(body.Rbrace)
|
|
if braceOffset < len(src) && src[braceOffset] == '}' {
|
|
return
|
|
}
|
|
|
|
braceLine := tok.Line(body.Rbrace)
|
|
if braceLine >= tok.LineCount() {
|
|
// If we are the last line in the file, no need to fix anything.
|
|
return
|
|
}
|
|
|
|
// Move the right brace down one line.
|
|
body.Rbrace = tok.LineStart(braceLine + 1)
|
|
}
|
|
|
|
// fixDanglingSelector inserts real "_" selector expressions in place
|
|
// of phantom "_" selectors. For example:
|
|
//
|
|
// func _() {
|
|
// x.<>
|
|
// }
|
|
// var x struct { i int }
|
|
//
|
|
// To fix completion at "<>", we insert a real "_" after the "." so the
|
|
// following declaration of "x" can be parsed and type checked
|
|
// normally.
|
|
func fixDanglingSelector(s *ast.SelectorExpr, tok *token.File, src []byte) []byte {
|
|
if !isPhantomUnderscore(s.Sel, tok, src) {
|
|
return nil
|
|
}
|
|
|
|
if !s.X.End().IsValid() {
|
|
return nil
|
|
}
|
|
|
|
// Insert directly after the selector's ".".
|
|
insertOffset := tok.Offset(s.X.End()) + 1
|
|
if src[insertOffset-1] != '.' {
|
|
return nil
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
buf.Grow(len(src) + 1)
|
|
buf.Write(src[:insertOffset])
|
|
buf.WriteByte('_')
|
|
buf.Write(src[insertOffset:])
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// fixPhantomSelector tries to fix selector expressions with phantom
|
|
// "_" selectors. In particular, we check if the selector is a
|
|
// keyword, and if so we swap in an *ast.Ident with the keyword text. For example:
|
|
//
|
|
// foo.var
|
|
//
|
|
// yields a "_" selector instead of "var" since "var" is a keyword.
|
|
func fixPhantomSelector(sel *ast.SelectorExpr, tok *token.File, src []byte) {
|
|
if !isPhantomUnderscore(sel.Sel, tok, src) {
|
|
return
|
|
}
|
|
|
|
// Only consider selectors directly abutting the selector ".". This
|
|
// avoids false positives in cases like:
|
|
//
|
|
// foo. // don't think "var" is our selector
|
|
// var bar = 123
|
|
//
|
|
if sel.Sel.Pos() != sel.X.End()+1 {
|
|
return
|
|
}
|
|
|
|
maybeKeyword := readKeyword(sel.Sel.Pos(), tok, src)
|
|
if maybeKeyword == "" {
|
|
return
|
|
}
|
|
|
|
replaceNode(sel, sel.Sel, &ast.Ident{
|
|
Name: maybeKeyword,
|
|
NamePos: sel.Sel.Pos(),
|
|
})
|
|
}
|
|
|
|
// isPhantomUnderscore reports whether the given ident is a phantom
|
|
// underscore. The parser sometimes inserts phantom underscores when
|
|
// it encounters otherwise unparseable situations.
|
|
func isPhantomUnderscore(id *ast.Ident, tok *token.File, src []byte) bool {
|
|
if id == nil || id.Name != "_" {
|
|
return false
|
|
}
|
|
|
|
// Phantom underscore means the underscore is not actually in the
|
|
// program text.
|
|
offset := tok.Offset(id.Pos())
|
|
return len(src) <= offset || src[offset] != '_'
|
|
}
|
|
|
|
// fixInitStmt fixes cases where the parser misinterprets an
|
|
// if/for/switch "init" statement as the "cond" conditional. In cases
|
|
// like "if i := 0" the user hasn't typed the semicolon yet so the
|
|
// parser is looking for the conditional expression. However, "i := 0"
|
|
// are not valid expressions, so we get a BadExpr.
|
|
func fixInitStmt(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte) {
|
|
if !bad.Pos().IsValid() || !bad.End().IsValid() {
|
|
return
|
|
}
|
|
|
|
// Try to extract a statement from the BadExpr.
|
|
stmtBytes := src[tok.Offset(bad.Pos()) : tok.Offset(bad.End()-1)+1]
|
|
stmt, err := parseStmt(bad.Pos(), stmtBytes)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// If the parent statement doesn't already have an "init" statement,
|
|
// move the extracted statement into the "init" field and insert a
|
|
// dummy expression into the required "cond" field.
|
|
switch p := parent.(type) {
|
|
case *ast.IfStmt:
|
|
if p.Init != nil {
|
|
return
|
|
}
|
|
p.Init = stmt
|
|
p.Cond = &ast.Ident{
|
|
Name: "_",
|
|
NamePos: stmt.End(),
|
|
}
|
|
case *ast.ForStmt:
|
|
if p.Init != nil {
|
|
return
|
|
}
|
|
p.Init = stmt
|
|
p.Cond = &ast.Ident{
|
|
Name: "_",
|
|
NamePos: stmt.End(),
|
|
}
|
|
case *ast.SwitchStmt:
|
|
if p.Init != nil {
|
|
return
|
|
}
|
|
p.Init = stmt
|
|
p.Tag = nil
|
|
}
|
|
}
|
|
|
|
// readKeyword reads the keyword starting at pos, if any.
|
|
func readKeyword(pos token.Pos, tok *token.File, src []byte) string {
|
|
var kwBytes []byte
|
|
for i := tok.Offset(pos); i < len(src); i++ {
|
|
// Use a simplified identifier check since keywords are always lowercase ASCII.
|
|
if src[i] < 'a' || src[i] > 'z' {
|
|
break
|
|
}
|
|
kwBytes = append(kwBytes, src[i])
|
|
|
|
// Stop search at arbitrarily chosen too-long-for-a-keyword length.
|
|
if len(kwBytes) > 15 {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
if kw := string(kwBytes); token.Lookup(kw).IsKeyword() {
|
|
return kw
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// fixArrayType tries to parse an *ast.BadExpr into an *ast.ArrayType.
|
|
// go/parser often turns lone array types like "[]int" into BadExprs
|
|
// if it isn't expecting a type.
|
|
func fixArrayType(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte) bool {
|
|
// Our expected input is a bad expression that looks like "[]someExpr".
|
|
|
|
from := bad.Pos()
|
|
to := bad.End()
|
|
|
|
if !from.IsValid() || !to.IsValid() {
|
|
return false
|
|
}
|
|
|
|
exprBytes := make([]byte, 0, int(to-from)+3)
|
|
// Avoid doing tok.Offset(to) since that panics if badExpr ends at EOF.
|
|
exprBytes = append(exprBytes, src[tok.Offset(from):tok.Offset(to-1)+1]...)
|
|
exprBytes = bytes.TrimSpace(exprBytes)
|
|
|
|
// If our expression ends in "]" (e.g. "[]"), add a phantom selector
|
|
// so we can complete directly after the "[]".
|
|
if len(exprBytes) > 0 && exprBytes[len(exprBytes)-1] == ']' {
|
|
exprBytes = append(exprBytes, '_')
|
|
}
|
|
|
|
// Add "{}" to turn our ArrayType into a CompositeLit. This is to
|
|
// handle the case of "[...]int" where we must make it a composite
|
|
// literal to be parseable.
|
|
exprBytes = append(exprBytes, '{', '}')
|
|
|
|
expr, err := parseExpr(from, exprBytes)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
cl, _ := expr.(*ast.CompositeLit)
|
|
if cl == nil {
|
|
return false
|
|
}
|
|
|
|
at, _ := cl.Type.(*ast.ArrayType)
|
|
if at == nil {
|
|
return false
|
|
}
|
|
|
|
return replaceNode(parent, bad, at)
|
|
}
|
|
|
|
// precedingToken scans src to find the token preceding pos.
|
|
func precedingToken(pos token.Pos, tok *token.File, src []byte) token.Token {
|
|
s := &scanner.Scanner{}
|
|
s.Init(tok, src, nil, 0)
|
|
|
|
var lastTok token.Token
|
|
for {
|
|
p, t, _ := s.Scan()
|
|
if t == token.EOF || p >= pos {
|
|
break
|
|
}
|
|
|
|
lastTok = t
|
|
}
|
|
return lastTok
|
|
}
|
|
|
|
// fixDeferOrGoStmt 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 fixDeferOrGoStmt(bad *ast.BadStmt, parent ast.Node, tok *token.File, src []byte) bool {
|
|
// 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
|
|
tkn token.Token
|
|
)
|
|
for {
|
|
if tkn == token.EOF {
|
|
return false
|
|
}
|
|
if pos >= bad.From {
|
|
break
|
|
}
|
|
pos, tkn, _ = s.Scan()
|
|
}
|
|
|
|
var stmt ast.Stmt
|
|
switch tkn {
|
|
case token.DEFER:
|
|
stmt = &ast.DeferStmt{
|
|
Defer: pos,
|
|
}
|
|
case token.GO:
|
|
stmt = &ast.GoStmt{
|
|
Go: pos,
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
|
|
var (
|
|
from, to, last token.Pos
|
|
lastToken token.Token
|
|
braceDepth int
|
|
phantomSelectors []token.Pos
|
|
)
|
|
FindTo:
|
|
for {
|
|
to, tkn, _ = s.Scan()
|
|
|
|
if from == token.NoPos {
|
|
from = to
|
|
}
|
|
|
|
switch tkn {
|
|
case token.EOF:
|
|
break FindTo
|
|
case token.SEMICOLON:
|
|
// If we aren't in nested braces, end of statement means
|
|
// end of expression.
|
|
if braceDepth == 0 {
|
|
break FindTo
|
|
}
|
|
case token.LBRACE:
|
|
braceDepth++
|
|
}
|
|
|
|
// This handles the common dangling selector case. For example in
|
|
//
|
|
// defer fmt.
|
|
// y := 1
|
|
//
|
|
// we notice the dangling period and end our expression.
|
|
//
|
|
// If the previous token was a "." and we are looking at a "}",
|
|
// the period is likely a dangling selector and needs a phantom
|
|
// "_". Likewise if the current token is on a different line than
|
|
// the period, the period is likely a dangling selector.
|
|
if lastToken == token.PERIOD && (tkn == token.RBRACE || tok.Line(to) > tok.Line(last)) {
|
|
// Insert phantom "_" selector after the dangling ".".
|
|
phantomSelectors = append(phantomSelectors, last+1)
|
|
// If we aren't in a block then end the expression after the ".".
|
|
if braceDepth == 0 {
|
|
to = last + 1
|
|
break
|
|
}
|
|
}
|
|
|
|
lastToken = tkn
|
|
last = to
|
|
|
|
switch tkn {
|
|
case token.RBRACE:
|
|
braceDepth--
|
|
if braceDepth <= 0 {
|
|
if braceDepth == 0 {
|
|
// +1 to include the "}" itself.
|
|
to += 1
|
|
}
|
|
break FindTo
|
|
}
|
|
}
|
|
}
|
|
|
|
if !from.IsValid() || tok.Offset(from) >= len(src) {
|
|
return false
|
|
}
|
|
|
|
if !to.IsValid() || tok.Offset(to) >= len(src) {
|
|
return false
|
|
}
|
|
|
|
// Insert any phantom selectors needed to prevent dangling "." from messing
|
|
// up the AST.
|
|
exprBytes := make([]byte, 0, int(to-from)+len(phantomSelectors))
|
|
for i, b := range src[tok.Offset(from):tok.Offset(to)] {
|
|
if len(phantomSelectors) > 0 && from+token.Pos(i) == phantomSelectors[0] {
|
|
exprBytes = append(exprBytes, '_')
|
|
phantomSelectors = phantomSelectors[1:]
|
|
}
|
|
exprBytes = append(exprBytes, b)
|
|
}
|
|
|
|
if len(phantomSelectors) > 0 {
|
|
exprBytes = append(exprBytes, '_')
|
|
}
|
|
|
|
expr, err := parseExpr(from, exprBytes)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
return replaceNode(parent, bad, stmt)
|
|
}
|
|
|
|
// parseStmt parses the statement in src and updates its position to
|
|
// start at pos.
|
|
func parseStmt(pos token.Pos, src []byte) (ast.Stmt, error) {
|
|
// Wrap our expression to make it a valid Go file we can pass to ParseFile.
|
|
fileSrc := bytes.Join([][]byte{
|
|
[]byte("package fake;func _(){"),
|
|
src,
|
|
[]byte("}"),
|
|
}, nil)
|
|
|
|
// Use ParseFile instead of ParseExpr because ParseFile has
|
|
// best-effort behavior, whereas ParseExpr fails hard on any error.
|
|
fakeFile, err := parser.ParseFile(token.NewFileSet(), "", fileSrc, 0)
|
|
if fakeFile == nil {
|
|
return nil, errors.Errorf("error reading fake file source: %v", err)
|
|
}
|
|
|
|
// Extract our expression node from inside the fake file.
|
|
if len(fakeFile.Decls) == 0 {
|
|
return nil, errors.Errorf("error parsing fake file: %v", err)
|
|
}
|
|
|
|
fakeDecl, _ := fakeFile.Decls[0].(*ast.FuncDecl)
|
|
if fakeDecl == nil || len(fakeDecl.Body.List) == 0 {
|
|
return nil, errors.Errorf("no statement in %s: %v", src, err)
|
|
}
|
|
|
|
stmt := fakeDecl.Body.List[0]
|
|
|
|
// parser.ParseFile returns undefined positions.
|
|
// Adjust them for the current file.
|
|
offsetPositions(stmt, pos-1-(stmt.Pos()-1))
|
|
|
|
return stmt, nil
|
|
}
|
|
|
|
// parseExpr parses the expression in src and updates its position to
|
|
// start at pos.
|
|
func parseExpr(pos token.Pos, src []byte) (ast.Expr, error) {
|
|
stmt, err := parseStmt(pos, src)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
exprStmt, ok := stmt.(*ast.ExprStmt)
|
|
if !ok {
|
|
return nil, errors.Errorf("no expr in %s: %v", src, err)
|
|
}
|
|
|
|
return exprStmt.X, nil
|
|
}
|
|
|
|
var tokenPosType = reflect.TypeOf(token.NoPos)
|
|
|
|
// offsetPositions applies an offset to the positions in an ast.Node.
|
|
func offsetPositions(n ast.Node, offset token.Pos) {
|
|
ast.Inspect(n, func(n ast.Node) bool {
|
|
if n == nil {
|
|
return false
|
|
}
|
|
|
|
v := reflect.ValueOf(n).Elem()
|
|
|
|
switch v.Kind() {
|
|
case reflect.Struct:
|
|
for i := 0; i < v.NumField(); i++ {
|
|
f := v.Field(i)
|
|
if f.Type() != tokenPosType {
|
|
continue
|
|
}
|
|
|
|
if !f.CanSet() {
|
|
continue
|
|
}
|
|
|
|
f.SetInt(f.Int() + int64(offset))
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|
|
|
|
// replaceNode updates parent's child oldChild to be newChild. It
|
|
// returns whether it replaced successfully.
|
|
func replaceNode(parent, oldChild, newChild ast.Node) bool {
|
|
if parent == nil || oldChild == nil || newChild == nil {
|
|
return false
|
|
}
|
|
|
|
parentVal := reflect.ValueOf(parent).Elem()
|
|
if parentVal.Kind() != reflect.Struct {
|
|
return false
|
|
}
|
|
|
|
newChildVal := reflect.ValueOf(newChild)
|
|
|
|
tryReplace := func(v reflect.Value) bool {
|
|
if !v.CanSet() || !v.CanInterface() {
|
|
return false
|
|
}
|
|
|
|
// If the existing value is oldChild, we found our child. Make
|
|
// sure our newChild is assignable and then make the swap.
|
|
if v.Interface() == oldChild && newChildVal.Type().AssignableTo(v.Type()) {
|
|
v.Set(newChildVal)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Loop over parent's struct fields.
|
|
for i := 0; i < parentVal.NumField(); i++ {
|
|
f := parentVal.Field(i)
|
|
|
|
switch f.Kind() {
|
|
// Check interface and pointer fields.
|
|
case reflect.Interface, reflect.Ptr:
|
|
if tryReplace(f) {
|
|
return true
|
|
}
|
|
|
|
// Search through any slice fields.
|
|
case reflect.Slice:
|
|
for i := 0; i < f.Len(); i++ {
|
|
if tryReplace(f.Index(i)) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|