blob: dee7083a1b3f00503e444617e0e989ae60ea83bd [file] [log] [blame]
// Copyright 2018 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 source
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"sync"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/protocol"
)
type View struct {
mu sync.Mutex // protects all mutable state of the view
Config *packages.Config
files map[protocol.DocumentURI]*File
}
func NewView() *View {
return &View{
Config: &packages.Config{
Mode: packages.LoadSyntax,
Fset: token.NewFileSet(),
Tests: true,
Overlay: make(map[string][]byte),
},
files: make(map[protocol.DocumentURI]*File),
}
}
// GetFile returns a File for the given uri.
// It will always succeed, adding the file to the managed set if needed.
func (v *View) GetFile(uri protocol.DocumentURI) *File {
v.mu.Lock()
f, found := v.files[uri]
if !found {
f = &File{
URI: uri,
view: v,
}
v.files[f.URI] = f
}
v.mu.Unlock()
return f
}
// TypeCheck type-checks the package for the given package path.
func (v *View) TypeCheck(uri protocol.DocumentURI) (*packages.Package, error) {
v.mu.Lock()
defer v.mu.Unlock()
path, err := FromURI(uri)
if err != nil {
return nil, err
}
pkgs, err := packages.Load(v.Config, fmt.Sprintf("file=%s", path))
if len(pkgs) == 0 {
if err == nil {
err = fmt.Errorf("no packages found for %s", path)
}
return nil, err
}
pkg := pkgs[0]
return pkg, nil
}
func (v *View) TypeCheckAtPosition(uri protocol.DocumentURI, position protocol.Position) (*packages.Package, *ast.File, token.Pos, error) {
v.mu.Lock()
defer v.mu.Unlock()
filename, err := FromURI(uri)
if err != nil {
return nil, nil, token.NoPos, err
}
var mu sync.Mutex
var qfileContent []byte
cfg := &packages.Config{
Mode: v.Config.Mode,
Dir: v.Config.Dir,
Env: v.Config.Env,
BuildFlags: v.Config.BuildFlags,
Fset: v.Config.Fset,
Tests: v.Config.Tests,
Overlay: v.Config.Overlay,
ParseFile: func(fset *token.FileSet, current string, data []byte) (*ast.File, error) {
// Save the file contents for use later in determining the query position.
if sameFile(current, filename) {
mu.Lock()
qfileContent = data
mu.Unlock()
}
return parser.ParseFile(fset, current, data, parser.AllErrors)
},
}
pkgs, err := packages.Load(cfg, fmt.Sprintf("file=%s", filename))
if len(pkgs) == 0 {
if err == nil {
err = fmt.Errorf("no package found for %s", filename)
}
return nil, nil, token.NoPos, err
}
pkg := pkgs[0]
var qpos token.Pos
var qfile *ast.File
for _, file := range pkg.Syntax {
tokfile := pkg.Fset.File(file.Pos())
if tokfile == nil || tokfile.Name() != filename {
continue
}
pos := positionToPos(tokfile, qfileContent, int(position.Line), int(position.Character))
if !pos.IsValid() {
return nil, nil, token.NoPos, fmt.Errorf("invalid position for %s", filename)
}
qfile = file
qpos = pos
break
}
if qfile == nil || qpos == token.NoPos {
return nil, nil, token.NoPos, fmt.Errorf("unable to find position %s:%v:%v", filename, position.Line, position.Character)
}
return pkg, qfile, qpos, nil
}
// trimAST clears any part of the AST not relevant to type checking
// expressions at pos.
func trimAST(file *ast.File, pos token.Pos) {
ast.Inspect(file, func(n ast.Node) bool {
if n == nil {
return false
}
if pos < n.Pos() || pos >= n.End() {
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
}
func sameFile(filename1, filename2 string) bool {
if filepath.Base(filename1) != filepath.Base(filename2) {
return false
}
finfo1, err := os.Stat(filename1)
if err != nil {
return false
}
finfo2, err := os.Stat(filename2)
if err != nil {
return false
}
return os.SameFile(finfo1, finfo2)
}
// positionToPos converts a 0-based line and column number in a file
// to a token.Pos. It returns NoPos if the file did not contain the position.
func positionToPos(file *token.File, content []byte, line, col int) token.Pos {
if file.Size() != len(content) {
return token.NoPos
}
if file.LineCount() < int(line) { // these can be equal if the last line is empty
return token.NoPos
}
start := 0
for i := 0; i < int(line); i++ {
if start >= len(content) {
return token.NoPos
}
index := bytes.IndexByte(content[start:], '\n')
if index == -1 {
return token.NoPos
}
start += (index + 1)
}
offset := start + int(col)
if offset > file.Size() {
return token.NoPos
}
return file.Pos(offset)
}