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