mirror of https://github.com/golang/go.git
884 lines
25 KiB
Go
884 lines
25 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"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/constant"
|
|
"go/doc"
|
|
"go/format"
|
|
"go/token"
|
|
"go/types"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"golang.org/x/text/unicode/runenames"
|
|
"golang.org/x/tools/internal/event"
|
|
"golang.org/x/tools/internal/lsp/bug"
|
|
"golang.org/x/tools/internal/lsp/protocol"
|
|
"golang.org/x/tools/internal/lsp/safetoken"
|
|
"golang.org/x/tools/internal/typeparams"
|
|
)
|
|
|
|
// HoverContext contains context extracted from the syntax and type information
|
|
// of a given node, for use in various summaries (hover, autocomplete,
|
|
// signature help).
|
|
type HoverContext struct {
|
|
// signatureSource is the object or node use to derive the hover signature.
|
|
//
|
|
// It may also hold a precomputed string.
|
|
// TODO(rfindley): pre-compute all signatures to avoid this indirection.
|
|
signatureSource interface{}
|
|
|
|
// comment is the most relevant comment group associated with the hovered object.
|
|
Comment *ast.CommentGroup
|
|
}
|
|
|
|
// HoverJSON contains information used by hover. It is also the JSON returned
|
|
// for the "structured" hover format
|
|
type HoverJSON struct {
|
|
// Synopsis is a single sentence synopsis of the symbol's documentation.
|
|
Synopsis string `json:"synopsis"`
|
|
|
|
// FullDocumentation is the symbol's full documentation.
|
|
FullDocumentation string `json:"fullDocumentation"`
|
|
|
|
// Signature is the symbol's signature.
|
|
Signature string `json:"signature"`
|
|
|
|
// SingleLine is a single line describing the symbol.
|
|
// This is recommended only for use in clients that show a single line for hover.
|
|
SingleLine string `json:"singleLine"`
|
|
|
|
// SymbolName is the types.Object.Name for the given symbol.
|
|
SymbolName string `json:"symbolName"`
|
|
|
|
// LinkPath is the pkg.go.dev link for the given symbol.
|
|
// For example, the "go/ast" part of "pkg.go.dev/go/ast#Node".
|
|
LinkPath string `json:"linkPath"`
|
|
|
|
// LinkAnchor is the pkg.go.dev link anchor for the given symbol.
|
|
// For example, the "Node" part of "pkg.go.dev/go/ast#Node".
|
|
LinkAnchor string `json:"linkAnchor"`
|
|
}
|
|
|
|
func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) {
|
|
ident, err := Identifier(ctx, snapshot, fh, position)
|
|
if err != nil {
|
|
if hover, innerErr := hoverRune(ctx, snapshot, fh, position); innerErr == nil {
|
|
return hover, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
h, err := HoverIdentifier(ctx, ident)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rng, err := ident.Range()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hover, err := FormatHover(h, snapshot.View().Options())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &protocol.Hover{
|
|
Contents: protocol.MarkupContent{
|
|
Kind: snapshot.View().Options().PreferredContentFormat,
|
|
Value: hover,
|
|
},
|
|
Range: rng,
|
|
}, nil
|
|
}
|
|
|
|
func hoverRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) {
|
|
ctx, done := event.Start(ctx, "source.hoverRune")
|
|
defer done()
|
|
|
|
r, mrng, err := findRune(ctx, snapshot, fh, position)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rng, err := mrng.Range()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var desc string
|
|
runeName := runenames.Name(r)
|
|
if len(runeName) > 0 && runeName[0] == '<' {
|
|
// Check if the rune looks like an HTML tag. If so, trim the surrounding <>
|
|
// characters to work around https://github.com/microsoft/vscode/issues/124042.
|
|
runeName = strings.TrimRight(runeName[1:], ">")
|
|
}
|
|
if strconv.IsPrint(r) {
|
|
desc = fmt.Sprintf("'%s', U+%04X, %s", string(r), uint32(r), runeName)
|
|
} else {
|
|
desc = fmt.Sprintf("U+%04X, %s", uint32(r), runeName)
|
|
}
|
|
return &protocol.Hover{
|
|
Contents: protocol.MarkupContent{
|
|
Kind: snapshot.View().Options().PreferredContentFormat,
|
|
Value: desc,
|
|
},
|
|
Range: rng,
|
|
}, nil
|
|
}
|
|
|
|
// ErrNoRuneFound is the error returned when no rune is found at a particular position.
|
|
var ErrNoRuneFound = errors.New("no rune found")
|
|
|
|
// findRune returns rune information for a position in a file.
|
|
func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (rune, MappedRange, error) {
|
|
pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
|
|
if err != nil {
|
|
return 0, MappedRange{}, err
|
|
}
|
|
spn, err := pgf.Mapper.PointSpan(position)
|
|
if err != nil {
|
|
return 0, MappedRange{}, err
|
|
}
|
|
rng, err := spn.Range(pgf.Mapper.Converter)
|
|
if err != nil {
|
|
return 0, MappedRange{}, err
|
|
}
|
|
pos := rng.Start
|
|
|
|
// Find the basic literal enclosing the given position, if there is one.
|
|
var lit *ast.BasicLit
|
|
var found bool
|
|
ast.Inspect(pgf.File, func(n ast.Node) bool {
|
|
if found {
|
|
return false
|
|
}
|
|
if n, ok := n.(*ast.BasicLit); ok && pos >= n.Pos() && pos <= n.End() {
|
|
lit = n
|
|
found = true
|
|
}
|
|
return !found
|
|
})
|
|
if !found {
|
|
return 0, MappedRange{}, ErrNoRuneFound
|
|
}
|
|
|
|
var r rune
|
|
var start, end token.Pos
|
|
switch lit.Kind {
|
|
case token.CHAR:
|
|
s, err := strconv.Unquote(lit.Value)
|
|
if err != nil {
|
|
// If the conversion fails, it's because of an invalid syntax, therefore
|
|
// there is no rune to be found.
|
|
return 0, MappedRange{}, ErrNoRuneFound
|
|
}
|
|
r, _ = utf8.DecodeRuneInString(s)
|
|
if r == utf8.RuneError {
|
|
return 0, MappedRange{}, fmt.Errorf("rune error")
|
|
}
|
|
start, end = lit.Pos(), lit.End()
|
|
case token.INT:
|
|
// It's an integer, scan only if it is a hex litteral whose bitsize in
|
|
// ranging from 8 to 32.
|
|
if !(strings.HasPrefix(lit.Value, "0x") && len(lit.Value[2:]) >= 2 && len(lit.Value[2:]) <= 8) {
|
|
return 0, MappedRange{}, ErrNoRuneFound
|
|
}
|
|
v, err := strconv.ParseUint(lit.Value[2:], 16, 32)
|
|
if err != nil {
|
|
return 0, MappedRange{}, err
|
|
}
|
|
r = rune(v)
|
|
if r == utf8.RuneError {
|
|
return 0, MappedRange{}, fmt.Errorf("rune error")
|
|
}
|
|
start, end = lit.Pos(), lit.End()
|
|
case token.STRING:
|
|
// It's a string, scan only if it contains a unicode escape sequence under or before the
|
|
// current cursor position.
|
|
var found bool
|
|
litOffset, err := safetoken.Offset(pgf.Tok, lit.Pos())
|
|
if err != nil {
|
|
return 0, MappedRange{}, err
|
|
}
|
|
offset, err := safetoken.Offset(pgf.Tok, pos)
|
|
if err != nil {
|
|
return 0, MappedRange{}, err
|
|
}
|
|
for i := offset - litOffset; i > 0; i-- {
|
|
// Start at the cursor position and search backward for the beginning of a rune escape sequence.
|
|
rr, _ := utf8.DecodeRuneInString(lit.Value[i:])
|
|
if rr == utf8.RuneError {
|
|
return 0, MappedRange{}, fmt.Errorf("rune error")
|
|
}
|
|
if rr == '\\' {
|
|
// Got the beginning, decode it.
|
|
var tail string
|
|
r, _, tail, err = strconv.UnquoteChar(lit.Value[i:], '"')
|
|
if err != nil {
|
|
// If the conversion fails, it's because of an invalid syntax, therefore is no rune to be found.
|
|
return 0, MappedRange{}, ErrNoRuneFound
|
|
}
|
|
// Only the rune escape sequence part of the string has to be highlighted, recompute the range.
|
|
runeLen := len(lit.Value) - (int(i) + len(tail))
|
|
start = token.Pos(int(lit.Pos()) + int(i))
|
|
end = token.Pos(int(start) + runeLen)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
// No escape sequence found
|
|
return 0, MappedRange{}, ErrNoRuneFound
|
|
}
|
|
default:
|
|
return 0, MappedRange{}, ErrNoRuneFound
|
|
}
|
|
|
|
mappedRange, err := posToMappedRange(snapshot, pkg, start, end)
|
|
if err != nil {
|
|
return 0, MappedRange{}, err
|
|
}
|
|
return r, mappedRange, nil
|
|
}
|
|
|
|
func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) {
|
|
ctx, done := event.Start(ctx, "source.Hover")
|
|
defer done()
|
|
|
|
hoverCtx, err := FindHoverContext(ctx, i.Snapshot, i.pkg, i.Declaration.obj, i.Declaration.node, i.Declaration.fullDecl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
h := &HoverJSON{
|
|
FullDocumentation: hoverCtx.Comment.Text(),
|
|
Synopsis: doc.Synopsis(hoverCtx.Comment.Text()),
|
|
}
|
|
|
|
fset := i.Snapshot.FileSet()
|
|
// Determine the symbol's signature.
|
|
switch x := hoverCtx.signatureSource.(type) {
|
|
case string:
|
|
h.Signature = x // a pre-computed signature
|
|
|
|
case *ast.TypeSpec:
|
|
x2 := *x
|
|
// Don't duplicate comments when formatting type specs.
|
|
x2.Doc = nil
|
|
x2.Comment = nil
|
|
var b strings.Builder
|
|
b.WriteString("type ")
|
|
if err := format.Node(&b, fset, &x2); err != nil {
|
|
return nil, err
|
|
}
|
|
h.Signature = b.String()
|
|
|
|
case ast.Node:
|
|
var b strings.Builder
|
|
if err := format.Node(&b, fset, x); err != nil {
|
|
return nil, err
|
|
}
|
|
h.Signature = b.String()
|
|
|
|
// Check if the variable is an integer whose value we can present in a more
|
|
// user-friendly way, i.e. `var hex = 0xe34e` becomes `var hex = 58190`
|
|
if spec, ok := x.(*ast.ValueSpec); ok && len(spec.Values) > 0 {
|
|
if lit, ok := spec.Values[0].(*ast.BasicLit); ok && len(spec.Names) > 0 {
|
|
val := constant.MakeFromLiteral(types.ExprString(lit), lit.Kind, 0)
|
|
h.Signature = fmt.Sprintf("var %s = %s", spec.Names[0], val)
|
|
}
|
|
}
|
|
|
|
case types.Object:
|
|
// If the variable is implicitly declared in a type switch, we need to
|
|
// manually generate its object string.
|
|
if typ := i.Declaration.typeSwitchImplicit; typ != nil {
|
|
if v, ok := x.(*types.Var); ok {
|
|
h.Signature = fmt.Sprintf("var %s %s", v.Name(), types.TypeString(typ, i.qf))
|
|
break
|
|
}
|
|
}
|
|
h.Signature = objectString(x, i.qf, i.Inferred)
|
|
}
|
|
if obj := i.Declaration.obj; obj != nil {
|
|
h.SingleLine = objectString(obj, i.qf, nil)
|
|
}
|
|
obj := i.Declaration.obj
|
|
if obj == nil {
|
|
return h, nil
|
|
}
|
|
|
|
// Check if the identifier is test-only (and is therefore not part of a
|
|
// package's API). This is true if the request originated in a test package,
|
|
// and if the declaration is also found in the same test package.
|
|
if i.pkg != nil && obj.Pkg() != nil && i.pkg.ForTest() != "" {
|
|
if _, err := i.pkg.File(i.Declaration.MappedRange[0].URI()); err == nil {
|
|
return h, nil
|
|
}
|
|
}
|
|
|
|
h.SymbolName, h.LinkPath, h.LinkAnchor = linkData(obj, i.enclosing)
|
|
|
|
// See golang/go#36998: don't link to modules matching GOPRIVATE.
|
|
//
|
|
// The path returned by linkData is an import path.
|
|
if i.Snapshot.View().IsGoPrivatePath(h.LinkPath) {
|
|
h.LinkPath = ""
|
|
} else if mod, version, ok := moduleAtVersion(h.LinkPath, i); ok {
|
|
h.LinkPath = strings.Replace(h.LinkPath, mod, mod+"@"+version, 1)
|
|
}
|
|
|
|
return h, nil
|
|
}
|
|
|
|
// linkData returns the name, import path, and anchor to use in building links
|
|
// to obj.
|
|
//
|
|
// If obj is not visible in documentation, the returned name will be empty.
|
|
func linkData(obj types.Object, enclosing *types.TypeName) (name, importPath, anchor string) {
|
|
// Package names simply link to the package.
|
|
if obj, ok := obj.(*types.PkgName); ok {
|
|
return obj.Name(), obj.Imported().Path(), ""
|
|
}
|
|
|
|
// Builtins link to the special builtin package.
|
|
if obj.Parent() == types.Universe {
|
|
return obj.Name(), "builtin", obj.Name()
|
|
}
|
|
|
|
// In all other cases, the object must be exported.
|
|
if !obj.Exported() {
|
|
return "", "", ""
|
|
}
|
|
|
|
var recv types.Object // If non-nil, the field or method receiver base.
|
|
|
|
switch obj := obj.(type) {
|
|
case *types.Var:
|
|
// If the object is a field, and we have an associated selector
|
|
// composite literal, or struct, we can determine the link.
|
|
if obj.IsField() && enclosing != nil {
|
|
recv = enclosing
|
|
}
|
|
case *types.Func:
|
|
typ, ok := obj.Type().(*types.Signature)
|
|
if !ok {
|
|
// Note: this should never happen. go/types guarantees that the type of
|
|
// *Funcs are Signatures.
|
|
//
|
|
// TODO(rfindley): given a 'debug' mode, we should panic here.
|
|
return "", "", ""
|
|
}
|
|
if r := typ.Recv(); r != nil {
|
|
if rtyp, _ := Deref(r.Type()).(*types.Named); rtyp != nil {
|
|
// If we have an unexported type, see if the enclosing type is
|
|
// exported (we may have an interface or struct we can link
|
|
// to). If not, don't show any link.
|
|
if !rtyp.Obj().Exported() {
|
|
if enclosing != nil {
|
|
recv = enclosing
|
|
} else {
|
|
return "", "", ""
|
|
}
|
|
} else {
|
|
recv = rtyp.Obj()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if recv != nil && !recv.Exported() {
|
|
return "", "", ""
|
|
}
|
|
|
|
// Either the object or its receiver must be in the package scope.
|
|
scopeObj := obj
|
|
if recv != nil {
|
|
scopeObj = recv
|
|
}
|
|
if scopeObj.Pkg() == nil || scopeObj.Pkg().Scope().Lookup(scopeObj.Name()) != scopeObj {
|
|
return "", "", ""
|
|
}
|
|
|
|
// golang/go#52211: somehow we get here with a nil obj.Pkg
|
|
if obj.Pkg() == nil {
|
|
bug.Report("object with nil pkg", bug.Data{
|
|
"name": obj.Name(),
|
|
"type": fmt.Sprintf("%T", obj),
|
|
})
|
|
return "", "", ""
|
|
}
|
|
|
|
importPath = obj.Pkg().Path()
|
|
if recv != nil {
|
|
anchor = fmt.Sprintf("%s.%s", recv.Name(), obj.Name())
|
|
name = fmt.Sprintf("(%s.%s).%s", obj.Pkg().Name(), recv.Name(), obj.Name())
|
|
} else {
|
|
// For most cases, the link is "package/path#symbol".
|
|
anchor = obj.Name()
|
|
name = fmt.Sprintf("%s.%s", obj.Pkg().Name(), obj.Name())
|
|
}
|
|
return name, importPath, anchor
|
|
}
|
|
|
|
func moduleAtVersion(path string, i *IdentifierInfo) (string, string, bool) {
|
|
// TODO(rfindley): moduleAtVersion should not be responsible for deciding
|
|
// whether or not the link target supports module version links.
|
|
if strings.ToLower(i.Snapshot.View().Options().LinkTarget) != "pkg.go.dev" {
|
|
return "", "", false
|
|
}
|
|
impPkg, err := i.pkg.GetImport(path)
|
|
if err != nil {
|
|
return "", "", false
|
|
}
|
|
if impPkg.Version() == nil {
|
|
return "", "", false
|
|
}
|
|
version, modpath := impPkg.Version().Version, impPkg.Version().Path
|
|
if modpath == "" || version == "" {
|
|
return "", "", false
|
|
}
|
|
return modpath, version, true
|
|
}
|
|
|
|
// objectString is a wrapper around the types.ObjectString function.
|
|
// It handles adding more information to the object string.
|
|
func objectString(obj types.Object, qf types.Qualifier, inferred *types.Signature) string {
|
|
// If the signature type was inferred, prefer the preferred signature with a
|
|
// comment showing the generic signature.
|
|
if sig, _ := obj.Type().(*types.Signature); sig != nil && typeparams.ForSignature(sig).Len() > 0 && inferred != nil {
|
|
obj2 := types.NewFunc(obj.Pos(), obj.Pkg(), obj.Name(), inferred)
|
|
str := types.ObjectString(obj2, qf)
|
|
// Try to avoid overly long lines.
|
|
if len(str) > 60 {
|
|
str += "\n"
|
|
} else {
|
|
str += " "
|
|
}
|
|
str += "// " + types.TypeString(sig, qf)
|
|
return str
|
|
}
|
|
str := types.ObjectString(obj, qf)
|
|
switch obj := obj.(type) {
|
|
case *types.Const:
|
|
str = fmt.Sprintf("%s = %s", str, obj.Val())
|
|
|
|
// Try to add a formatted duration as an inline comment
|
|
typ, ok := obj.Type().(*types.Named)
|
|
if !ok {
|
|
break
|
|
}
|
|
pkg := typ.Obj().Pkg()
|
|
if pkg.Path() == "time" && typ.Obj().Name() == "Duration" {
|
|
if d, ok := constant.Int64Val(obj.Val()); ok {
|
|
str += " // " + time.Duration(d).String()
|
|
}
|
|
}
|
|
}
|
|
return str
|
|
}
|
|
|
|
// FindHoverContext returns a HoverContext struct for an AST node and its
|
|
// declaration object. node should be the actual node used in type checking,
|
|
// while fullNode could be a separate node with more complete syntactic
|
|
// information.
|
|
func FindHoverContext(ctx context.Context, s Snapshot, pkg Package, obj types.Object, pkgNode ast.Node, fullDecl ast.Decl) (*HoverContext, error) {
|
|
var info *HoverContext
|
|
|
|
// Type parameters get their signature from their declaration object.
|
|
if _, isTypeName := obj.(*types.TypeName); isTypeName {
|
|
if _, isTypeParam := obj.Type().(*typeparams.TypeParam); isTypeParam {
|
|
return &HoverContext{signatureSource: obj}, nil
|
|
}
|
|
}
|
|
|
|
// This is problematic for a number of reasons. We really need to have a more
|
|
// general mechanism to validate the coherency of AST with type information,
|
|
// but absent that we must do our best to ensure that we don't use fullNode
|
|
// when we actually need the node that was type checked.
|
|
//
|
|
// pkgNode may be nil, if it was eliminated from the type-checked syntax. In
|
|
// that case, use fullDecl if available.
|
|
node := pkgNode
|
|
if node == nil && fullDecl != nil {
|
|
node = fullDecl
|
|
}
|
|
|
|
switch node := node.(type) {
|
|
case *ast.Ident:
|
|
// The package declaration.
|
|
for _, f := range pkg.GetSyntax() {
|
|
if f.Name == pkgNode {
|
|
info = &HoverContext{Comment: f.Doc}
|
|
}
|
|
}
|
|
case *ast.ImportSpec:
|
|
// Try to find the package documentation for an imported package.
|
|
pkgPath, err := strconv.Unquote(node.Path.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
imp, err := pkg.GetImport(pkgPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Assume that only one file will contain package documentation,
|
|
// so pick the first file that has a doc comment.
|
|
for _, file := range imp.GetSyntax() {
|
|
if file.Doc != nil {
|
|
info = &HoverContext{Comment: file.Doc}
|
|
if file.Name != nil {
|
|
info.signatureSource = "package " + file.Name.Name
|
|
}
|
|
break
|
|
}
|
|
}
|
|
case *ast.GenDecl:
|
|
switch obj := obj.(type) {
|
|
case *types.TypeName, *types.Var, *types.Const, *types.Func:
|
|
// Always use the full declaration here if we have it, because the
|
|
// dependent code doesn't rely on pointer identity. This is fragile.
|
|
if d, _ := fullDecl.(*ast.GenDecl); d != nil {
|
|
node = d
|
|
}
|
|
// obj may not have been produced by type checking the AST containing
|
|
// node, so we need to be careful about using token.Pos.
|
|
tok := s.FileSet().File(obj.Pos())
|
|
offset, err := safetoken.Offset(tok, obj.Pos())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// fullTok and fullPos are the *token.File and object position in for the
|
|
// full AST.
|
|
fullTok := s.FileSet().File(node.Pos())
|
|
fullPos, err := safetoken.Pos(fullTok, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var spec ast.Spec
|
|
for _, s := range node.Specs {
|
|
// Avoid panics by guarding the calls to token.Offset (golang/go#48249).
|
|
start, err := safetoken.Offset(fullTok, s.Pos())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
end, err := safetoken.Offset(fullTok, s.End())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if start <= offset && offset <= end {
|
|
spec = s
|
|
break
|
|
}
|
|
}
|
|
|
|
info, err = hoverGenDecl(node, spec, fullPos, obj)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
case *ast.TypeSpec:
|
|
if obj.Parent() == types.Universe {
|
|
if genDecl, ok := fullDecl.(*ast.GenDecl); ok {
|
|
info = hoverTypeSpec(node, genDecl)
|
|
}
|
|
}
|
|
case *ast.FuncDecl:
|
|
switch obj.(type) {
|
|
case *types.Func:
|
|
info = &HoverContext{signatureSource: obj, Comment: node.Doc}
|
|
case *types.Builtin:
|
|
info = &HoverContext{Comment: node.Doc}
|
|
if sig, err := NewBuiltinSignature(ctx, s, obj.Name()); err == nil {
|
|
info.signatureSource = "func " + sig.name + sig.Format()
|
|
} else {
|
|
// Fall back on the object as a signature source.
|
|
bug.Report("invalid builtin hover", bug.Data{
|
|
"err": err.Error(),
|
|
})
|
|
info.signatureSource = obj
|
|
}
|
|
case *types.Var:
|
|
// Object is a function param or the field of an anonymous struct
|
|
// declared with ':='. Skip the first one because only fields
|
|
// can have docs.
|
|
if isFunctionParam(obj, node) {
|
|
break
|
|
}
|
|
|
|
field, err := s.PosToField(ctx, pkg, obj.Pos())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if field != nil {
|
|
comment := field.Doc
|
|
if comment.Text() == "" {
|
|
comment = field.Comment
|
|
}
|
|
info = &HoverContext{signatureSource: obj, Comment: comment}
|
|
}
|
|
}
|
|
}
|
|
|
|
if info == nil {
|
|
info = &HoverContext{signatureSource: obj}
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
// isFunctionParam returns true if the passed object is either an incoming
|
|
// or an outgoing function param
|
|
func isFunctionParam(obj types.Object, node *ast.FuncDecl) bool {
|
|
for _, f := range node.Type.Params.List {
|
|
if f.Pos() == obj.Pos() {
|
|
return true
|
|
}
|
|
}
|
|
if node.Type.Results != nil {
|
|
for _, f := range node.Type.Results.List {
|
|
if f.Pos() == obj.Pos() {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hoverGenDecl returns hover information an object declared via spec inside
|
|
// of the GenDecl node. obj is the type-checked object corresponding to the
|
|
// declaration, but may have been type-checked using a different AST than the
|
|
// given nodes; fullPos is the position of obj in node's AST.
|
|
func hoverGenDecl(node *ast.GenDecl, spec ast.Spec, fullPos token.Pos, obj types.Object) (*HoverContext, error) {
|
|
if spec == nil {
|
|
return nil, fmt.Errorf("no spec for node %v at position %v", node, fullPos)
|
|
}
|
|
|
|
// If we have a field or method.
|
|
switch obj.(type) {
|
|
case *types.Var, *types.Const, *types.Func:
|
|
return hoverVar(spec, fullPos, obj, node), nil
|
|
}
|
|
// Handle types.
|
|
switch spec := spec.(type) {
|
|
case *ast.TypeSpec:
|
|
return hoverTypeSpec(spec, node), nil
|
|
case *ast.ValueSpec:
|
|
return &HoverContext{signatureSource: spec, Comment: spec.Doc}, nil
|
|
case *ast.ImportSpec:
|
|
return &HoverContext{signatureSource: spec, Comment: spec.Doc}, nil
|
|
}
|
|
return nil, fmt.Errorf("unable to format spec %v (%T)", spec, spec)
|
|
}
|
|
|
|
// TODO(rfindley): rename this function.
|
|
func hoverTypeSpec(spec *ast.TypeSpec, decl *ast.GenDecl) *HoverContext {
|
|
comment := spec.Doc
|
|
if comment == nil && decl != nil {
|
|
comment = decl.Doc
|
|
}
|
|
if comment == nil {
|
|
comment = spec.Comment
|
|
}
|
|
return &HoverContext{
|
|
signatureSource: spec,
|
|
Comment: comment,
|
|
}
|
|
}
|
|
|
|
func hoverVar(node ast.Spec, fullPos token.Pos, obj types.Object, decl *ast.GenDecl) *HoverContext {
|
|
var fieldList *ast.FieldList
|
|
switch spec := node.(type) {
|
|
case *ast.TypeSpec:
|
|
switch t := spec.Type.(type) {
|
|
case *ast.StructType:
|
|
fieldList = t.Fields
|
|
case *ast.InterfaceType:
|
|
fieldList = t.Methods
|
|
}
|
|
case *ast.ValueSpec:
|
|
// Try to extract the field list of an anonymous struct
|
|
if fieldList = extractFieldList(spec.Type); fieldList != nil {
|
|
break
|
|
}
|
|
|
|
comment := spec.Doc
|
|
if comment == nil {
|
|
comment = decl.Doc
|
|
}
|
|
if comment == nil {
|
|
comment = spec.Comment
|
|
}
|
|
|
|
// We need the AST nodes for variable declarations of basic literals with
|
|
// associated values so that we can augment their hover with more information.
|
|
if _, ok := obj.(*types.Var); ok && spec.Type == nil && len(spec.Values) > 0 {
|
|
if _, ok := spec.Values[0].(*ast.BasicLit); ok {
|
|
return &HoverContext{signatureSource: spec, Comment: comment}
|
|
}
|
|
}
|
|
|
|
return &HoverContext{signatureSource: obj, Comment: comment}
|
|
}
|
|
|
|
if fieldList != nil {
|
|
comment := findFieldComment(fullPos, fieldList)
|
|
return &HoverContext{signatureSource: obj, Comment: comment}
|
|
}
|
|
return &HoverContext{signatureSource: obj, Comment: decl.Doc}
|
|
}
|
|
|
|
// extractFieldList recursively tries to extract a field list.
|
|
// If it is not found, nil is returned.
|
|
func extractFieldList(specType ast.Expr) *ast.FieldList {
|
|
switch t := specType.(type) {
|
|
case *ast.StructType:
|
|
return t.Fields
|
|
case *ast.InterfaceType:
|
|
return t.Methods
|
|
case *ast.ArrayType:
|
|
return extractFieldList(t.Elt)
|
|
case *ast.MapType:
|
|
// Map value has a greater chance to be a struct
|
|
if fields := extractFieldList(t.Value); fields != nil {
|
|
return fields
|
|
}
|
|
return extractFieldList(t.Key)
|
|
case *ast.ChanType:
|
|
return extractFieldList(t.Value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// findFieldComment visits all fields in depth-first order and returns
|
|
// the comment of a field with passed position. If no comment is found,
|
|
// nil is returned.
|
|
func findFieldComment(pos token.Pos, fieldList *ast.FieldList) *ast.CommentGroup {
|
|
for _, field := range fieldList.List {
|
|
if field.Pos() == pos {
|
|
if field.Doc.Text() != "" {
|
|
return field.Doc
|
|
}
|
|
return field.Comment
|
|
}
|
|
|
|
if nestedFieldList := extractFieldList(field.Type); nestedFieldList != nil {
|
|
if c := findFieldComment(pos, nestedFieldList); c != nil {
|
|
return c
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func FormatHover(h *HoverJSON, options *Options) (string, error) {
|
|
signature := formatSignature(h, options)
|
|
|
|
switch options.HoverKind {
|
|
case SingleLine:
|
|
return h.SingleLine, nil
|
|
case NoDocumentation:
|
|
return signature, nil
|
|
case Structured:
|
|
b, err := json.Marshal(h)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(b), nil
|
|
}
|
|
|
|
link := formatLink(h, options)
|
|
doc := formatDoc(h, options)
|
|
|
|
var b strings.Builder
|
|
parts := []string{signature, doc, link}
|
|
for i, el := range parts {
|
|
if el != "" {
|
|
b.WriteString(el)
|
|
|
|
// Don't write out final newline.
|
|
if i == len(parts) {
|
|
continue
|
|
}
|
|
// If any elements of the remainder of the list are non-empty,
|
|
// write a newline.
|
|
if anyNonEmpty(parts[i+1:]) {
|
|
if options.PreferredContentFormat == protocol.Markdown {
|
|
b.WriteString("\n\n")
|
|
} else {
|
|
b.WriteRune('\n')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return b.String(), nil
|
|
}
|
|
|
|
func formatSignature(h *HoverJSON, options *Options) string {
|
|
signature := h.Signature
|
|
if signature != "" && options.PreferredContentFormat == protocol.Markdown {
|
|
signature = fmt.Sprintf("```go\n%s\n```", signature)
|
|
}
|
|
return signature
|
|
}
|
|
|
|
func formatLink(h *HoverJSON, options *Options) string {
|
|
if !options.LinksInHover || options.LinkTarget == "" || h.LinkPath == "" {
|
|
return ""
|
|
}
|
|
plainLink := BuildLink(options.LinkTarget, h.LinkPath, h.LinkAnchor)
|
|
switch options.PreferredContentFormat {
|
|
case protocol.Markdown:
|
|
return fmt.Sprintf("[`%s` on %s](%s)", h.SymbolName, options.LinkTarget, plainLink)
|
|
case protocol.PlainText:
|
|
return ""
|
|
default:
|
|
return plainLink
|
|
}
|
|
}
|
|
|
|
// BuildLink constructs a link with the given target, path, and anchor.
|
|
func BuildLink(target, path, anchor string) string {
|
|
link := fmt.Sprintf("https://%s/%s", target, path)
|
|
if target == "pkg.go.dev" {
|
|
link += "?utm_source=gopls"
|
|
}
|
|
if anchor == "" {
|
|
return link
|
|
}
|
|
return link + "#" + anchor
|
|
}
|
|
|
|
func formatDoc(h *HoverJSON, options *Options) string {
|
|
var doc string
|
|
switch options.HoverKind {
|
|
case SynopsisDocumentation:
|
|
doc = h.Synopsis
|
|
case FullDocumentation:
|
|
doc = h.FullDocumentation
|
|
}
|
|
if options.PreferredContentFormat == protocol.Markdown {
|
|
return CommentToMarkdown(doc)
|
|
}
|
|
return doc
|
|
}
|
|
|
|
func anyNonEmpty(x []string) bool {
|
|
for _, el := range x {
|
|
if el != "" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|