1
0
mirror of https://github.com/golang/go synced 2024-11-18 15:14:44 -07:00
go/internal/lsp/source/hover.go
Heschi Kreinick a9439ae9c1 internal/lsp: replace ParseGoHandle with concrete data
ParseGoHandles serve two purposes: they pin cache entries so that
redundant calculations are cached, and they allow users to obtain the
actual parsed AST. The former is an implementation detail, and the
latter turns out to just be an annoyance.

Parsed Go files are obtained from two places. By far the most common is
from a type checked package. But a type checked package must by
definition have already parsed all the files it contains, so the PGH
is already computed and cannot have failed. Type checked packages can
simply return the parsed file without requiring a separate Check
operation. We do want to pin the cache entries in this case, which I've
done by holding on to the PGH in cache.pkg.

There are some cases where we directly parse a file, such as for the
FoldingRange LSP call, which doesn't need type information. Those parses
can actually fail, so we do need an error check. But we don't need the
PGH; in all cases we are immediately using and discarding it.

So it turns out we don't actually need the PGH type at all, at least not
in the public API. Instead, we can pass around a concrete struct that
has the various pieces of data directly available.

This uncovered a bug in typeCheck: it should fail if it encounters any
real errors.

Change-Id: I203bf2dd79d5d65c01392d69c2cf4f7744fde7fc
Reviewed-on: https://go-review.googlesource.com/c/tools/+/244021
Run-TryBot: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2020-07-28 17:35:11 +00:00

458 lines
12 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"
"fmt"
"go/ast"
"go/doc"
"go/format"
"go/token"
"go/types"
"strings"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/lsp/protocol"
errors "golang.org/x/xerrors"
)
type HoverInformation struct {
// 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"`
// 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"`
// ImportPath is the import path for the package containing the given symbol.
ImportPath string
// Link is the pkg.go.dev anchor for the given symbol.
// For example, "go/ast#Node".
Link string `json:"link"`
// SymbolName is the types.Object.Name for the given symbol.
SymbolName string
source interface{}
comment *ast.CommentGroup
}
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 {
return nil, nil
}
h, err := HoverIdentifier(ctx, ident)
if err != nil {
return nil, err
}
rng, err := ident.Range()
if err != nil {
return nil, err
}
// See golang/go#36998: don't link to modules matching GOPRIVATE.
if snapshot.View().IsGoPrivatePath(h.ImportPath) {
h.Link = ""
}
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 HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverInformation, error) {
ctx, done := event.Start(ctx, "source.Hover")
defer done()
fset := i.Snapshot.View().Session().Cache().FileSet()
h, err := hover(ctx, fset, i.pkg, i.Declaration)
if err != nil {
return nil, err
}
// Determine the symbol's signature.
switch x := h.source.(type) {
case ast.Node:
var b strings.Builder
if err := format.Node(&b, fset, x); err != nil {
return nil, err
}
h.Signature = b.String()
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)
}
if obj := i.Declaration.obj; obj != nil {
h.SingleLine = objectString(obj, i.qf)
}
h.ImportPath, h.Link, h.SymbolName = pathLinkAndSymbolName(i)
return h, nil
}
func pathLinkAndSymbolName(i *IdentifierInfo) (string, string, string) {
obj := i.Declaration.obj
if obj == nil {
return "", "", ""
}
switch obj := obj.(type) {
case *types.PkgName:
path := obj.Imported().Path()
link := path
if mod, version, ok := moduleAtVersion(path, i); ok {
link = strings.Replace(path, mod, mod+"@"+version, 1)
}
return path, link, obj.Name()
case *types.Builtin:
return "builtin", fmt.Sprintf("builtin#%s", obj.Name()), obj.Name()
}
// 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 "", "", ""
}
}
// Don't return links for other unexported types.
if !obj.Exported() {
return "", "", ""
}
var rTypeName string
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() {
if named, ok := i.enclosing.(*types.Named); ok {
rTypeName = named.Obj().Name()
}
}
case *types.Func:
typ, ok := obj.Type().(*types.Signature)
if !ok {
return "", "", ""
}
if r := typ.Recv(); r != nil {
switch rtyp := deref(r.Type()).(type) {
case *types.Struct:
rTypeName = r.Name()
case *types.Named:
// 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 named := i.enclosing.(*types.Named); ok && named.Obj().Exported() {
rTypeName = named.Obj().Name()
} else {
return "", "", ""
}
} else {
rTypeName = rtyp.Obj().Name()
}
}
}
}
path := obj.Pkg().Path()
link := path
if mod, version, ok := moduleAtVersion(path, i); ok {
link = strings.Replace(path, mod, mod+"@"+version, 1)
}
if rTypeName != "" {
link = fmt.Sprintf("%s#%s.%s", link, rTypeName, obj.Name())
symbol := fmt.Sprintf("(%s.%s).%s", obj.Pkg().Name(), rTypeName, obj.Name())
return path, link, symbol
}
// For most cases, the link is "package/path#symbol".
link = fmt.Sprintf("%s#%s", link, obj.Name())
symbolName := fmt.Sprintf("%s.%s", obj.Pkg().Name(), obj.Name())
return path, link, symbolName
}
func moduleAtVersion(path string, i *IdentifierInfo) (string, string, bool) {
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.Module() == nil {
return "", "", false
}
version, modpath := impPkg.Module().Version, impPkg.Module().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) string {
str := types.ObjectString(obj, qf)
switch obj := obj.(type) {
case *types.Const:
str = fmt.Sprintf("%s = %s", str, obj.Val())
}
return str
}
func hover(ctx context.Context, fset *token.FileSet, pkg Package, d Declaration) (*HoverInformation, error) {
_, done := event.Start(ctx, "source.hover")
defer done()
return hoverInfo(pkg, d.obj, d.node)
}
func hoverInfo(pkg Package, obj types.Object, node ast.Node) (*HoverInformation, error) {
var info *HoverInformation
switch node := node.(type) {
case *ast.Ident:
// The package declaration.
for _, f := range pkg.GetSyntax() {
if f.Name == node {
info = &HoverInformation{comment: f.Doc}
}
}
case *ast.ImportSpec:
// Try to find the package documentation for an imported package.
if pkgName, ok := obj.(*types.PkgName); ok {
imp, err := pkg.GetImport(pkgName.Imported().Path())
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 = &HoverInformation{source: obj, comment: file.Doc}
break
}
}
}
info = &HoverInformation{source: node}
case *ast.GenDecl:
switch obj := obj.(type) {
case *types.TypeName, *types.Var, *types.Const, *types.Func:
var err error
info, err = formatGenDecl(node, obj, obj.Type())
if err != nil {
return nil, err
}
}
case *ast.TypeSpec:
if obj.Parent() == types.Universe {
if obj.Name() == "error" {
info = &HoverInformation{source: node}
} else {
info = &HoverInformation{source: node.Name} // comments not needed for builtins
}
}
case *ast.FuncDecl:
switch obj.(type) {
case *types.Func:
info = &HoverInformation{source: obj, comment: node.Doc}
case *types.Builtin:
info = &HoverInformation{source: node.Type, comment: node.Doc}
}
}
if info == nil {
info = &HoverInformation{source: obj}
}
if info.comment != nil {
info.FullDocumentation = info.comment.Text()
info.Synopsis = doc.Synopsis(info.FullDocumentation)
}
return info, nil
}
func formatGenDecl(node *ast.GenDecl, obj types.Object, typ types.Type) (*HoverInformation, error) {
if _, ok := typ.(*types.Named); ok {
switch typ.Underlying().(type) {
case *types.Interface, *types.Struct:
return formatGenDecl(node, obj, typ.Underlying())
}
}
var spec ast.Spec
for _, s := range node.Specs {
if s.Pos() <= obj.Pos() && obj.Pos() <= s.End() {
spec = s
break
}
}
if spec == nil {
return nil, errors.Errorf("no spec for node %v at position %v", node, obj.Pos())
}
// If we have a field or method.
switch obj.(type) {
case *types.Var, *types.Const, *types.Func:
return formatVar(spec, obj, node), nil
}
// Handle types.
switch spec := spec.(type) {
case *ast.TypeSpec:
if len(node.Specs) > 1 {
// If multiple types are declared in the same block.
return &HoverInformation{source: spec.Type, comment: spec.Doc}, nil
} else {
return &HoverInformation{source: spec, comment: node.Doc}, nil
}
case *ast.ValueSpec:
return &HoverInformation{source: spec, comment: spec.Doc}, nil
case *ast.ImportSpec:
return &HoverInformation{source: spec, comment: spec.Doc}, nil
}
return nil, errors.Errorf("unable to format spec %v (%T)", spec, spec)
}
func formatVar(node ast.Spec, obj types.Object, decl *ast.GenDecl) *HoverInformation {
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:
comment := spec.Doc
if comment == nil {
comment = decl.Doc
}
if comment == nil {
comment = spec.Comment
}
return &HoverInformation{source: obj, comment: comment}
}
// If we have a struct or interface declaration,
// we need to match the object to the corresponding field or method.
if fieldList != nil {
for i := 0; i < len(fieldList.List); i++ {
field := fieldList.List[i]
if field.Pos() <= obj.Pos() && obj.Pos() <= field.End() {
if field.Doc.Text() != "" {
return &HoverInformation{source: obj, comment: field.Doc}
}
return &HoverInformation{source: obj, comment: field.Comment}
}
}
}
return &HoverInformation{source: obj, comment: decl.Doc}
}
func FormatHover(h *HoverInformation, options Options) (string, error) {
signature := h.Signature
if signature != "" && options.PreferredContentFormat == protocol.Markdown {
signature = fmt.Sprintf("```go\n%s\n```", signature)
}
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)
switch options.HoverKind {
case SynopsisDocumentation:
doc := formatDoc(h.Synopsis, options)
return formatHover(options, signature, link, doc), nil
case FullDocumentation:
doc := formatDoc(h.FullDocumentation, options)
return formatHover(options, signature, link, doc), nil
}
return "", errors.Errorf("no hover for %v", h.source)
}
func formatLink(h *HoverInformation, options Options) string {
if !options.LinksInHover || options.LinkTarget == "" || h.Link == "" {
return ""
}
plainLink := fmt.Sprintf("https://%s/%s", options.LinkTarget, h.Link)
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
}
}
func formatDoc(doc string, options Options) string {
if options.PreferredContentFormat == protocol.Markdown {
return CommentToMarkdown(doc)
}
return doc
}
func formatHover(options Options, x ...string) string {
var b strings.Builder
for i, el := range x {
if el != "" {
b.WriteString(el)
// Don't write out final newline.
if i == len(x) {
continue
}
// If any elements of the remainder of the list are non-empty,
// write a newline.
if anyNonEmpty(x[i+1:]) {
if options.PreferredContentFormat == protocol.Markdown {
b.WriteString("\n\n")
} else {
b.WriteRune('\n')
}
}
}
}
return b.String()
}
func anyNonEmpty(x []string) bool {
for _, el := range x {
if el != "" {
return true
}
}
return false
}