mirror of
https://github.com/golang/go
synced 2024-11-19 01:14:39 -07:00
2208e1677e
This change eliminates the extra step of calling GetFile on the view and getting the FileHandle from the snapshot. It also eliminiates the redundant source.File type. Follow up changes will clean up the file kind handling, since it still exists on the fileBase type. Change-Id: I635ab8632821b36e062be5151eaab425a5698f60 Reviewed-on: https://go-review.googlesource.com/c/tools/+/211778 Run-TryBot: Rebecca Stambler <rstambler@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Heschi Kreinick <heschi@google.com>
616 lines
16 KiB
Go
616 lines
16 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 source
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/printer"
|
|
"go/token"
|
|
"go/types"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/internal/lsp/protocol"
|
|
"golang.org/x/tools/internal/span"
|
|
errors "golang.org/x/xerrors"
|
|
)
|
|
|
|
type mappedRange struct {
|
|
spanRange span.Range
|
|
m *protocol.ColumnMapper
|
|
|
|
// protocolRange is the result of converting the spanRange using the mapper.
|
|
// It is computed on-demand.
|
|
protocolRange *protocol.Range
|
|
}
|
|
|
|
func newMappedRange(fset *token.FileSet, m *protocol.ColumnMapper, start, end token.Pos) mappedRange {
|
|
return mappedRange{
|
|
spanRange: span.Range{
|
|
FileSet: fset,
|
|
Start: start,
|
|
End: end,
|
|
Converter: m.Converter,
|
|
},
|
|
m: m,
|
|
}
|
|
}
|
|
|
|
func (s mappedRange) Range() (protocol.Range, error) {
|
|
if s.protocolRange == nil {
|
|
spn, err := s.spanRange.Span()
|
|
if err != nil {
|
|
return protocol.Range{}, err
|
|
}
|
|
prng, err := s.m.Range(spn)
|
|
if err != nil {
|
|
return protocol.Range{}, err
|
|
}
|
|
s.protocolRange = &prng
|
|
}
|
|
return *s.protocolRange, nil
|
|
}
|
|
|
|
func (s mappedRange) Span() (span.Span, error) {
|
|
return s.spanRange.Span()
|
|
}
|
|
|
|
func (s mappedRange) URI() span.URI {
|
|
return s.m.URI
|
|
}
|
|
|
|
// getParsedFile is a convenience function that extracts the Package and ParseGoHandle for a File in a Snapshot.
|
|
// selectPackage is typically Narrowest/WidestCheckPackageHandle below.
|
|
func getParsedFile(ctx context.Context, snapshot Snapshot, fh FileHandle, selectPackage PackagePolicy) (Package, ParseGoHandle, error) {
|
|
phs, err := snapshot.PackageHandles(ctx, fh)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
ph, err := selectPackage(phs)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
pkg, err := ph.Check(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
pgh, err := pkg.File(fh.Identity().URI)
|
|
return pkg, pgh, err
|
|
}
|
|
|
|
type PackagePolicy func([]PackageHandle) (PackageHandle, error)
|
|
|
|
// NarrowestCheckPackageHandle picks the "narrowest" package for a given file.
|
|
//
|
|
// By "narrowest" package, we mean the package with the fewest number of files
|
|
// that includes the given file. This solves the problem of test variants,
|
|
// as the test will have more files than the non-test package.
|
|
func NarrowestCheckPackageHandle(handles []PackageHandle) (PackageHandle, error) {
|
|
if len(handles) < 1 {
|
|
return nil, errors.Errorf("no CheckPackageHandles")
|
|
}
|
|
result := handles[0]
|
|
for _, handle := range handles[1:] {
|
|
if result == nil || len(handle.CompiledGoFiles()) < len(result.CompiledGoFiles()) {
|
|
result = handle
|
|
}
|
|
}
|
|
if result == nil {
|
|
return nil, errors.Errorf("nil CheckPackageHandles have been returned")
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// WidestCheckPackageHandle returns the CheckPackageHandle containing the most files.
|
|
//
|
|
// This is useful for something like diagnostics, where we'd prefer to offer diagnostics
|
|
// for as many files as possible.
|
|
func WidestCheckPackageHandle(handles []PackageHandle) (PackageHandle, error) {
|
|
if len(handles) < 1 {
|
|
return nil, errors.Errorf("no CheckPackageHandles")
|
|
}
|
|
result := handles[0]
|
|
for _, handle := range handles[1:] {
|
|
if result == nil || len(handle.CompiledGoFiles()) > len(result.CompiledGoFiles()) {
|
|
result = handle
|
|
}
|
|
}
|
|
if result == nil {
|
|
return nil, errors.Errorf("nil CheckPackageHandles have been returned")
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// SpecificPackageHandle creates a PackagePolicy to select a
|
|
// particular PackageHandle when you alread know the one you want.
|
|
func SpecificPackageHandle(desiredID string) PackagePolicy {
|
|
return func(handles []PackageHandle) (PackageHandle, error) {
|
|
for _, h := range handles {
|
|
if h.ID() == desiredID {
|
|
return h, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("no package handle with expected id %q", desiredID)
|
|
}
|
|
}
|
|
|
|
func IsGenerated(ctx context.Context, view View, uri span.URI) bool {
|
|
fh, err := view.Snapshot().GetFile(ctx, uri)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
ph := view.Session().Cache().ParseGoHandle(fh, ParseHeader)
|
|
parsed, _, _, err := ph.Parse(ctx)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
tok := view.Session().Cache().FileSet().File(parsed.Pos())
|
|
if tok == nil {
|
|
return false
|
|
}
|
|
for _, commentGroup := range parsed.Comments {
|
|
for _, comment := range commentGroup.List {
|
|
if matched := generatedRx.MatchString(comment.Text); matched {
|
|
// Check if comment is at the beginning of the line in source.
|
|
if pos := tok.Position(comment.Slash); pos.Column == 1 {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func nodeToProtocolRange(ctx context.Context, view View, m *protocol.ColumnMapper, n ast.Node) (protocol.Range, error) {
|
|
mrng, err := nodeToMappedRange(view, m, n)
|
|
if err != nil {
|
|
return protocol.Range{}, err
|
|
}
|
|
return mrng.Range()
|
|
}
|
|
|
|
func objToMappedRange(v View, pkg Package, obj types.Object) (mappedRange, error) {
|
|
if pkgName, ok := obj.(*types.PkgName); ok {
|
|
// An imported Go package has a package-local, unqualified name.
|
|
// When the name matches the imported package name, there is no
|
|
// identifier in the import spec with the local package name.
|
|
//
|
|
// For example:
|
|
// import "go/ast" // name "ast" matches package name
|
|
// import a "go/ast" // name "a" does not match package name
|
|
//
|
|
// When the identifier does not appear in the source, have the range
|
|
// of the object be the import path, including quotes.
|
|
if pkgName.Imported().Name() == pkgName.Name() {
|
|
return posToMappedRange(v, pkg, obj.Pos(), obj.Pos()+token.Pos(len(pkgName.Imported().Path())+2))
|
|
}
|
|
}
|
|
return nameToMappedRange(v, pkg, obj.Pos(), obj.Name())
|
|
}
|
|
|
|
func nameToMappedRange(v View, pkg Package, pos token.Pos, name string) (mappedRange, error) {
|
|
return posToMappedRange(v, pkg, pos, pos+token.Pos(len(name)))
|
|
}
|
|
|
|
func nodeToMappedRange(view View, m *protocol.ColumnMapper, n ast.Node) (mappedRange, error) {
|
|
return posToRange(view, m, n.Pos(), n.End())
|
|
}
|
|
|
|
func posToMappedRange(v View, pkg Package, pos, end token.Pos) (mappedRange, error) {
|
|
logicalFilename := v.Session().Cache().FileSet().File(pos).Position(pos).Filename
|
|
m, err := v.FindMapperInPackage(pkg, span.FileURI(logicalFilename))
|
|
if err != nil {
|
|
return mappedRange{}, err
|
|
}
|
|
return posToRange(v, m, pos, end)
|
|
}
|
|
|
|
func posToRange(view View, m *protocol.ColumnMapper, pos, end token.Pos) (mappedRange, error) {
|
|
if !pos.IsValid() {
|
|
return mappedRange{}, errors.Errorf("invalid position for %v", pos)
|
|
}
|
|
if !end.IsValid() {
|
|
return mappedRange{}, errors.Errorf("invalid position for %v", end)
|
|
}
|
|
return newMappedRange(view.Session().Cache().FileSet(), m, pos, end), nil
|
|
}
|
|
|
|
// Matches cgo generated comment as well as the proposed standard:
|
|
// https://golang.org/s/generatedcode
|
|
var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`)
|
|
|
|
func DetectLanguage(langID, filename string) FileKind {
|
|
switch langID {
|
|
case "go":
|
|
return Go
|
|
case "go.mod":
|
|
return Mod
|
|
case "go.sum":
|
|
return Sum
|
|
}
|
|
// Fallback to detecting the language based on the file extension.
|
|
switch filepath.Ext(filename) {
|
|
case ".mod":
|
|
return Mod
|
|
case ".sum":
|
|
return Sum
|
|
default: // fallback to Go
|
|
return Go
|
|
}
|
|
}
|
|
|
|
func (k FileKind) String() string {
|
|
switch k {
|
|
case Mod:
|
|
return "go.mod"
|
|
case Sum:
|
|
return "go.sum"
|
|
default:
|
|
return "go"
|
|
}
|
|
}
|
|
|
|
// Returns the index and the node whose position is contained inside the node list.
|
|
func nodeAtPos(nodes []ast.Node, pos token.Pos) (ast.Node, int) {
|
|
if nodes == nil {
|
|
return nil, -1
|
|
}
|
|
for i, node := range nodes {
|
|
if node.Pos() <= pos && pos <= node.End() {
|
|
return node, i
|
|
}
|
|
}
|
|
return nil, -1
|
|
}
|
|
|
|
// indexExprAtPos returns the index of the expression containing pos.
|
|
func indexExprAtPos(pos token.Pos, args []ast.Expr) int {
|
|
for i, expr := range args {
|
|
if expr.Pos() <= pos && pos <= expr.End() {
|
|
return i
|
|
}
|
|
}
|
|
return len(args)
|
|
}
|
|
|
|
func exprAtPos(pos token.Pos, args []ast.Expr) ast.Expr {
|
|
for _, expr := range args {
|
|
if expr.Pos() <= pos && pos <= expr.End() {
|
|
return expr
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// fieldSelections returns the set of fields that can
|
|
// be selected from a value of type T.
|
|
func fieldSelections(T types.Type) (fields []*types.Var) {
|
|
// TODO(adonovan): this algorithm doesn't exclude ambiguous
|
|
// selections that match more than one field/method.
|
|
// types.NewSelectionSet should do that for us.
|
|
|
|
seen := make(map[*types.Var]bool) // for termination on recursive types
|
|
|
|
var visit func(T types.Type)
|
|
visit = func(T types.Type) {
|
|
if T, ok := deref(T).Underlying().(*types.Struct); ok {
|
|
for i := 0; i < T.NumFields(); i++ {
|
|
f := T.Field(i)
|
|
if seen[f] {
|
|
continue
|
|
}
|
|
seen[f] = true
|
|
fields = append(fields, f)
|
|
if f.Anonymous() {
|
|
visit(f.Type())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
visit(T)
|
|
|
|
return fields
|
|
}
|
|
|
|
// typeIsValid reports whether typ doesn't contain any Invalid types.
|
|
func typeIsValid(typ types.Type) bool {
|
|
switch typ := typ.Underlying().(type) {
|
|
case *types.Basic:
|
|
return typ.Kind() != types.Invalid
|
|
case *types.Array:
|
|
return typeIsValid(typ.Elem())
|
|
case *types.Slice:
|
|
return typeIsValid(typ.Elem())
|
|
case *types.Pointer:
|
|
return typeIsValid(typ.Elem())
|
|
case *types.Map:
|
|
return typeIsValid(typ.Key()) && typeIsValid(typ.Elem())
|
|
case *types.Chan:
|
|
return typeIsValid(typ.Elem())
|
|
case *types.Signature:
|
|
return typeIsValid(typ.Params()) && typeIsValid(typ.Results())
|
|
case *types.Tuple:
|
|
for i := 0; i < typ.Len(); i++ {
|
|
if !typeIsValid(typ.At(i).Type()) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
case *types.Struct, *types.Interface, *types.Named:
|
|
// Don't bother checking structs, interfaces, or named types for validity.
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// resolveInvalid traverses the node of the AST that defines the scope
|
|
// containing the declaration of obj, and attempts to find a user-friendly
|
|
// name for its invalid type. The resulting Object and its Type are fake.
|
|
func resolveInvalid(fset *token.FileSet, obj types.Object, node ast.Node, info *types.Info) types.Object {
|
|
var resultExpr ast.Expr
|
|
ast.Inspect(node, func(node ast.Node) bool {
|
|
switch n := node.(type) {
|
|
case *ast.ValueSpec:
|
|
for _, name := range n.Names {
|
|
if info.Defs[name] == obj {
|
|
resultExpr = n.Type
|
|
}
|
|
}
|
|
return false
|
|
case *ast.Field: // This case handles parameters and results of a FuncDecl or FuncLit.
|
|
for _, name := range n.Names {
|
|
if info.Defs[name] == obj {
|
|
resultExpr = n.Type
|
|
}
|
|
}
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
})
|
|
// Construct a fake type for the object and return a fake object with this type.
|
|
typename := formatNode(fset, resultExpr)
|
|
typ := types.NewNamed(types.NewTypeName(token.NoPos, obj.Pkg(), typename, nil), types.Typ[types.Invalid], nil)
|
|
return types.NewVar(obj.Pos(), obj.Pkg(), obj.Name(), typ)
|
|
}
|
|
|
|
func isPointer(T types.Type) bool {
|
|
_, ok := T.(*types.Pointer)
|
|
return ok
|
|
}
|
|
|
|
// deref returns a pointer's element type, traversing as many levels as needed.
|
|
// Otherwise it returns typ.
|
|
func deref(typ types.Type) types.Type {
|
|
for {
|
|
p, ok := typ.Underlying().(*types.Pointer)
|
|
if !ok {
|
|
return typ
|
|
}
|
|
typ = p.Elem()
|
|
}
|
|
}
|
|
|
|
func isTypeName(obj types.Object) bool {
|
|
_, ok := obj.(*types.TypeName)
|
|
return ok
|
|
}
|
|
|
|
func isFunc(obj types.Object) bool {
|
|
_, ok := obj.(*types.Func)
|
|
return ok
|
|
}
|
|
|
|
func isEmptyInterface(T types.Type) bool {
|
|
intf, _ := T.(*types.Interface)
|
|
return intf != nil && intf.NumMethods() == 0
|
|
}
|
|
|
|
func isUntyped(T types.Type) bool {
|
|
if basic, ok := T.(*types.Basic); ok {
|
|
return basic.Info()&types.IsUntyped > 0
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isSelector returns the enclosing *ast.SelectorExpr when pos is in the
|
|
// selector.
|
|
func enclosingSelector(path []ast.Node, pos token.Pos) *ast.SelectorExpr {
|
|
if len(path) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if sel, ok := path[0].(*ast.SelectorExpr); ok {
|
|
return sel
|
|
}
|
|
|
|
if _, ok := path[0].(*ast.Ident); ok && len(path) > 1 {
|
|
if sel, ok := path[1].(*ast.SelectorExpr); ok && pos >= sel.Sel.Pos() {
|
|
return sel
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// typeConversion returns the type being converted to if call is a type
|
|
// conversion expression.
|
|
func typeConversion(call *ast.CallExpr, info *types.Info) types.Type {
|
|
var ident *ast.Ident
|
|
switch expr := call.Fun.(type) {
|
|
case *ast.Ident:
|
|
ident = expr
|
|
case *ast.SelectorExpr:
|
|
ident = expr.Sel
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
// Type conversion (e.g. "float64(foo)").
|
|
if fun, _ := info.ObjectOf(ident).(*types.TypeName); fun != nil {
|
|
return fun.Type()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// fieldsAccessible returns whether s has at least one field accessible by p.
|
|
func fieldsAccessible(s *types.Struct, p *types.Package) bool {
|
|
for i := 0; i < s.NumFields(); i++ {
|
|
f := s.Field(i)
|
|
if f.Exported() || f.Pkg() == p {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func formatParams(s Snapshot, pkg Package, sig *types.Signature, qf types.Qualifier) []string {
|
|
params := make([]string, 0, sig.Params().Len())
|
|
for i := 0; i < sig.Params().Len(); i++ {
|
|
el := sig.Params().At(i)
|
|
typ, err := formatFieldType(s, pkg, el, qf)
|
|
if err != nil {
|
|
typ = types.TypeString(el.Type(), qf)
|
|
}
|
|
|
|
// Handle a variadic parameter (can only be the final parameter).
|
|
if sig.Variadic() && i == sig.Params().Len()-1 {
|
|
typ = strings.Replace(typ, "[]", "...", 1)
|
|
}
|
|
|
|
if el.Name() == "" {
|
|
params = append(params, typ)
|
|
} else {
|
|
params = append(params, el.Name()+" "+typ)
|
|
}
|
|
}
|
|
return params
|
|
}
|
|
|
|
func formatFieldType(s Snapshot, srcpkg Package, obj types.Object, qf types.Qualifier) (string, error) {
|
|
file, pkg, err := s.View().FindPosInPackage(srcpkg, obj.Pos())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
ident, err := findIdentifier(s, pkg, file, obj.Pos())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if i := ident.ident; i == nil || i.Obj == nil || i.Obj.Decl == nil {
|
|
return "", errors.Errorf("no object for ident %v", i.Name)
|
|
}
|
|
f, ok := ident.ident.Obj.Decl.(*ast.Field)
|
|
if !ok {
|
|
return "", errors.Errorf("ident %s is not a field type", ident.Name)
|
|
}
|
|
return formatNode(s.View().Session().Cache().FileSet(), f.Type), nil
|
|
}
|
|
|
|
func formatNode(fset *token.FileSet, n ast.Node) string {
|
|
var buf strings.Builder
|
|
if err := printer.Fprint(&buf, fset, n); err != nil {
|
|
return ""
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func formatResults(tup *types.Tuple, qf types.Qualifier) ([]string, bool) {
|
|
var writeResultParens bool
|
|
results := make([]string, 0, tup.Len())
|
|
for i := 0; i < tup.Len(); i++ {
|
|
if i >= 1 {
|
|
writeResultParens = true
|
|
}
|
|
el := tup.At(i)
|
|
typ := types.TypeString(el.Type(), qf)
|
|
|
|
if el.Name() == "" {
|
|
results = append(results, typ)
|
|
} else {
|
|
if i == 0 {
|
|
writeResultParens = true
|
|
}
|
|
results = append(results, el.Name()+" "+typ)
|
|
}
|
|
}
|
|
return results, writeResultParens
|
|
}
|
|
|
|
// formatType returns the detail and kind for an object of type *types.TypeName.
|
|
func formatType(typ types.Type, qf types.Qualifier) (detail string, kind protocol.CompletionItemKind) {
|
|
if types.IsInterface(typ) {
|
|
detail = "interface{...}"
|
|
kind = protocol.InterfaceCompletion
|
|
} else if _, ok := typ.(*types.Struct); ok {
|
|
detail = "struct{...}"
|
|
kind = protocol.StructCompletion
|
|
} else if typ != typ.Underlying() {
|
|
detail, kind = formatType(typ.Underlying(), qf)
|
|
} else {
|
|
detail = types.TypeString(typ, qf)
|
|
kind = protocol.ClassCompletion
|
|
}
|
|
return detail, kind
|
|
}
|
|
|
|
func formatFunction(params []string, results []string, writeResultParens bool) string {
|
|
var detail strings.Builder
|
|
|
|
detail.WriteByte('(')
|
|
for i, p := range params {
|
|
if i > 0 {
|
|
detail.WriteString(", ")
|
|
}
|
|
detail.WriteString(p)
|
|
}
|
|
detail.WriteByte(')')
|
|
|
|
// Add space between parameters and results.
|
|
if len(results) > 0 {
|
|
detail.WriteByte(' ')
|
|
}
|
|
|
|
if writeResultParens {
|
|
detail.WriteByte('(')
|
|
}
|
|
for i, p := range results {
|
|
if i > 0 {
|
|
detail.WriteString(", ")
|
|
}
|
|
detail.WriteString(p)
|
|
}
|
|
if writeResultParens {
|
|
detail.WriteByte(')')
|
|
}
|
|
|
|
return detail.String()
|
|
}
|
|
|
|
func SortDiagnostics(d []Diagnostic) {
|
|
sort.Slice(d, func(i int, j int) bool {
|
|
return CompareDiagnostic(d[i], d[j]) < 0
|
|
})
|
|
}
|
|
|
|
func CompareDiagnostic(a, b Diagnostic) int {
|
|
if r := protocol.CompareRange(a.Range, b.Range); r != 0 {
|
|
return r
|
|
}
|
|
if a.Message < b.Message {
|
|
return -1
|
|
}
|
|
if a.Message == b.Message {
|
|
return 0
|
|
}
|
|
return 1
|
|
}
|