blob: b315b7383d49704506c111eef4e40e4c2ba9a704 [file] [log] [blame]
// 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 golang
import (
"bytes"
"context"
"encoding/json"
"fmt"
"go/ast"
"go/constant"
"go/doc"
"go/format"
"go/token"
"go/types"
"io/fs"
"path/filepath"
"sort"
"strconv"
"strings"
"text/tabwriter"
"time"
"unicode/utf8"
"golang.org/x/text/unicode/runenames"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/cache/metadata"
"golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
gastutil "golang.org/x/tools/gopls/internal/util/astutil"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/gopls/internal/util/slices"
"golang.org/x/tools/gopls/internal/util/typesutil"
"golang.org/x/tools/internal/aliases"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/stdlib"
"golang.org/x/tools/internal/tokeninternal"
"golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal"
)
// hoverJSON contains the structured result of a hover query. It is
// formatted in one of several formats as determined by the HoverKind
// setting, one of which is JSON.
//
// We believe this is used only by govim.
// TODO(adonovan): see if we can wean all clients of this interface.
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 human-readable name to use for the symbol in links.
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".
// It may have a module version suffix "@v1.2.3".
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"`
// stdVersion is the Go release version at which this symbol became available.
// It is nil for non-std library.
stdVersion *stdlib.Version
// New fields go below, and are unexported. The existing
// exported fields are underspecified and have already
// constrained our movements too much. A detailed JSON
// interface might be nice, but it needs a design and a
// precise specification.
// typeDecl is the declaration syntax for a type,
// or "" for a non-type.
typeDecl string
// methods is the list of descriptions of methods of a type,
// omitting any that are obvious from typeDecl.
// It is "" for a non-type.
methods string
// promotedFields is the list of descriptions of accessible
// fields of a (struct) type that were promoted through an
// embedded field.
promotedFields string
}
// Hover implements the "textDocument/hover" RPC for Go files.
// It may return nil even on success.
//
// If pkgURL is non-nil, it should be used to generate doc links.
func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position, pkgURL func(path PackagePath, fragment string) protocol.URI) (*protocol.Hover, error) {
ctx, done := event.Start(ctx, "golang.Hover")
defer done()
rng, h, err := hover(ctx, snapshot, fh, position)
if err != nil {
return nil, err
}
if h == nil {
return nil, nil
}
hover, err := formatHover(h, snapshot.Options(), pkgURL)
if err != nil {
return nil, err
}
return &protocol.Hover{
Contents: protocol.MarkupContent{
Kind: snapshot.Options().PreferredContentFormat,
Value: hover,
},
Range: rng,
}, nil
}
// hover computes hover information at the given position. If we do not support
// hovering at the position, it returns _, nil, nil: an error is only returned
// if the position is valid but we fail to compute hover information.
//
// TODO(adonovan): strength-reduce file.Handle to protocol.DocumentURI.
func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp protocol.Position) (protocol.Range, *hoverJSON, error) {
// Check for hover inside the builtin file before attempting type checking
// below. NarrowestPackageForFile may or may not succeed, depending on
// whether this is a GOROOT view, but even if it does succeed the resulting
// package will be command-line-arguments package. The user should get a
// hover for the builtin object, not the object type checked from the
// builtin.go.
if snapshot.IsBuiltin(fh.URI()) {
pgf, err := snapshot.BuiltinFile(ctx)
if err != nil {
return protocol.Range{}, nil, err
}
pos, err := pgf.PositionPos(pp)
if err != nil {
return protocol.Range{}, nil, err
}
path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos)
if id, ok := path[0].(*ast.Ident); ok {
rng, err := pgf.NodeRange(id)
if err != nil {
return protocol.Range{}, nil, err
}
var obj types.Object
if id.Name == "Error" {
obj = types.Universe.Lookup("error").Type().Underlying().(*types.Interface).Method(0)
} else {
obj = types.Universe.Lookup(id.Name)
}
if obj != nil {
h, err := hoverBuiltin(ctx, snapshot, obj)
return rng, h, err
}
}
return protocol.Range{}, nil, nil // no object to hover
}
pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
if err != nil {
return protocol.Range{}, nil, err
}
pos, err := pgf.PositionPos(pp)
if err != nil {
return protocol.Range{}, nil, err
}
// Handle hovering over the package name, which does not have an associated
// object.
// As with import paths, we allow hovering just after the package name.
if pgf.File.Name != nil && gastutil.NodeContains(pgf.File.Name, pos) {
return hoverPackageName(pkg, pgf)
}
// Handle hovering over embed directive argument.
pattern, embedRng := parseEmbedDirective(pgf.Mapper, pp)
if pattern != "" {
return hoverEmbed(fh, embedRng, pattern)
}
// hoverRange is the range reported to the client (e.g. for highlighting).
// It may be an expansion around the selected identifier,
// for instance when hovering over a linkname directive or doc link.
var hoverRange *protocol.Range
// Handle linkname directive by overriding what to look for.
if pkgPath, name, offset := parseLinkname(pgf.Mapper, pp); pkgPath != "" && name != "" {
// rng covering 2nd linkname argument: pkgPath.name.
rng, err := pgf.PosRange(pgf.Tok.Pos(offset), pgf.Tok.Pos(offset+len(pkgPath)+len(".")+len(name)))
if err != nil {
return protocol.Range{}, nil, fmt.Errorf("range over linkname arg: %w", err)
}
hoverRange = &rng
pkg, pgf, pos, err = findLinkname(ctx, snapshot, PackagePath(pkgPath), name)
if err != nil {
return protocol.Range{}, nil, fmt.Errorf("find linkname: %w", err)
}
}
// Handle hovering over a doc link
if obj, rng, _ := parseDocLink(pkg, pgf, pos); obj != nil {
// Built-ins have no position.
if isBuiltin(obj) {
h, err := hoverBuiltin(ctx, snapshot, obj)
return rng, h, err
}
// Find position in declaring file.
hoverRange = &rng
objURI := safetoken.StartPosition(pkg.FileSet(), obj.Pos())
pkg, pgf, err = NarrowestPackageForFile(ctx, snapshot, protocol.URIFromPath(objURI.Filename))
if err != nil {
return protocol.Range{}, nil, err
}
pos = pgf.Tok.Pos(objURI.Offset)
}
// Handle hovering over import paths, which do not have an associated
// identifier.
for _, spec := range pgf.File.Imports {
if gastutil.NodeContains(spec, pos) {
rng, hoverJSON, err := hoverImport(ctx, snapshot, pkg, pgf, spec)
if err != nil {
return protocol.Range{}, nil, err
}
if hoverRange == nil {
hoverRange = &rng
}
return *hoverRange, hoverJSON, nil // (hoverJSON may be nil)
}
}
// Handle hovering over (non-import-path) literals.
if path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos); len(path) > 0 {
if lit, _ := path[0].(*ast.BasicLit); lit != nil {
return hoverLit(pgf, lit, pos)
}
}
// Handle hover over identifier.
// The general case: compute hover information for the object referenced by
// the identifier at pos.
ident, obj, selectedType := referencedObject(pkg, pgf, pos)
if obj == nil || ident == nil {
return protocol.Range{}, nil, nil // no object to hover
}
// Unless otherwise specified, rng covers the ident being hovered.
if hoverRange == nil {
rng, err := pgf.NodeRange(ident)
if err != nil {
return protocol.Range{}, nil, err
}
hoverRange = &rng
}
// By convention, we qualify hover information relative to the package
// from which the request originated.
qf := typesutil.FileQualifier(pgf.File, pkg.Types(), pkg.TypesInfo())
// Handle type switch identifiers as a special case, since they don't have an
// object.
//
// There's not much useful information to provide.
if selectedType != nil {
fakeObj := types.NewVar(obj.Pos(), obj.Pkg(), obj.Name(), selectedType)
signature := types.ObjectString(fakeObj, qf)
return *hoverRange, &hoverJSON{
Signature: signature,
SingleLine: signature,
SymbolName: fakeObj.Name(),
}, nil
}
if isBuiltin(obj) {
// Built-ins have no position.
h, err := hoverBuiltin(ctx, snapshot, obj)
return *hoverRange, h, err
}
// For all other objects, consider the full syntax of their declaration in
// order to correctly compute their documentation, signature, and link.
//
// Beware: decl{PGF,Pos} are not necessarily associated with pkg.FileSet().
declPGF, declPos, err := parseFull(ctx, snapshot, pkg.FileSet(), obj.Pos())
if err != nil {
return protocol.Range{}, nil, fmt.Errorf("re-parsing declaration of %s: %v", obj.Name(), err)
}
decl, spec, field := findDeclInfo([]*ast.File{declPGF.File}, declPos) // may be nil^3
comment := chooseDocComment(decl, spec, field)
docText := comment.Text()
// By default, types.ObjectString provides a reasonable signature.
signature := objectString(obj, qf, declPos, declPGF.Tok, spec)
singleLineSignature := signature
// Display struct tag for struct fields at the end of the signature.
if field != nil && field.Tag != nil {
signature += " " + field.Tag.Value
}
// TODO(rfindley): we could do much better for inferred signatures.
// TODO(adonovan): fuse the two calls below.
if inferred := inferredSignature(pkg.TypesInfo(), ident); inferred != nil {
if s := inferredSignatureString(obj, qf, inferred); s != "" {
signature = s
}
}
// Compute size information for types,
// and (size, offset) for struct fields.
//
// Also, if a struct type's field ordering is significantly
// wasteful of space, report its optimal size.
//
// This information is useful when debugging crashes or
// optimizing layout. To reduce distraction, we show it only
// when hovering over the declaring identifier,
// but not referring identifiers.
//
// Size and alignment vary across OS/ARCH.
// Gopls will select the appropriate build configuration when
// viewing a type declaration in a build-tagged file, but will
// use the default build config for all other types, even
// if they embed platform-variant types.
//
var sizeOffset string // optional size/offset description
if def, ok := pkg.TypesInfo().Defs[ident]; ok && ident.Pos() == def.Pos() {
// This is the declaring identifier.
// (We can't simply use ident.Pos() == obj.Pos() because
// referencedObject prefers the TypeName for an embedded field).
// format returns the decimal and hex representation of x.
format := func(x int64) string {
if x < 10 {
return fmt.Sprintf("%d", x)
}
return fmt.Sprintf("%[1]d (%#[1]x)", x)
}
path := pathEnclosingObjNode(pgf.File, pos)
// Build string of form "size=... (X% wasted), offset=...".
size, wasted, offset := computeSizeOffsetInfo(pkg, path, obj)
var buf strings.Builder
if size >= 0 {
fmt.Fprintf(&buf, "size=%s", format(size))
if wasted >= 20 { // >=20% wasted
fmt.Fprintf(&buf, " (%d%% wasted)", wasted)
}
}
if offset >= 0 {
if buf.Len() > 0 {
buf.WriteString(", ")
}
fmt.Fprintf(&buf, "offset=%s", format(offset))
}
sizeOffset = buf.String()
}
var typeDecl, methods, fields string
// For "objects defined by a type spec", the signature produced by
// objectString is insufficient:
// (1) large structs are formatted poorly, with no newlines
// (2) we lose inline comments
// Furthermore, we include a summary of their method set.
_, isTypeName := obj.(*types.TypeName)
_, isTypeParam := aliases.Unalias(obj.Type()).(*types.TypeParam)
if isTypeName && !isTypeParam {
spec, ok := spec.(*ast.TypeSpec)
if !ok {
// We cannot find a TypeSpec for this type or alias declaration
// (that is not a type parameter or a built-in).
// This should be impossible even for ill-formed trees;
// we suspect that AST repair may be creating inconsistent
// positions. Don't report a bug in that case. (#64241)
errorf := fmt.Errorf
if !declPGF.Fixed() {
errorf = bug.Errorf
}
return protocol.Range{}, nil, errorf("type name %q without type spec", obj.Name())
}
// Format the type's declaration syntax.
{
// Don't duplicate comments.
spec2 := *spec
spec2.Doc = nil
spec2.Comment = nil
var b strings.Builder
b.WriteString("type ")
fset := tokeninternal.FileSetFor(declPGF.Tok)
// TODO(adonovan): use a smarter formatter that omits
// inaccessible fields (non-exported ones from other packages).
if err := format.Node(&b, fset, &spec2); err != nil {
return protocol.Range{}, nil, err
}
typeDecl = b.String()
// Splice in size/offset at end of first line.
// "type T struct { // size=..."
if sizeOffset != "" {
nl := strings.IndexByte(typeDecl, '\n')
if nl < 0 {
nl = len(typeDecl)
}
typeDecl = typeDecl[:nl] + " // " + sizeOffset + typeDecl[nl:]
}
}
// Promoted fields
//
// Show a table of accessible fields of the (struct)
// type that may not be visible in the syntax (above)
// due to promotion through embedded fields.
//
// Example:
//
// // Embedded fields:
// foo int // through x.y
// z string // through x.y
if prom := promotedFields(obj.Type(), pkg.Types()); len(prom) > 0 {
var b strings.Builder
b.WriteString("// Embedded fields:\n")
w := tabwriter.NewWriter(&b, 0, 8, 1, ' ', 0)
for _, f := range prom {
fmt.Fprintf(w, "%s\t%s\t// through %s\t\n",
f.field.Name(),
types.TypeString(f.field.Type(), qf),
f.path)
}
w.Flush()
b.WriteByte('\n')
fields = b.String()
}
// -- methods --
// For an interface type, explicit methods will have
// already been displayed when the node was formatted
// above. Don't list these again.
var skip map[string]bool
if iface, ok := spec.Type.(*ast.InterfaceType); ok {
if iface.Methods.List != nil {
for _, m := range iface.Methods.List {
if len(m.Names) == 1 {
if skip == nil {
skip = make(map[string]bool)
}
skip[m.Names[0].Name] = true
}
}
}
}
// Display all the type's accessible methods,
// including those that require a pointer receiver,
// and those promoted from embedded struct fields or
// embedded interfaces.
var b strings.Builder
for _, m := range typeutil.IntuitiveMethodSet(obj.Type(), nil) {
if !accessibleTo(m.Obj(), pkg.Types()) {
continue // inaccessible
}
if skip[m.Obj().Name()] {
continue // redundant with format.Node above
}
if b.Len() > 0 {
b.WriteByte('\n')
}
// Use objectString for its prettier rendering of method receivers.
b.WriteString(objectString(m.Obj(), qf, token.NoPos, nil, nil))
}
methods = b.String()
signature = typeDecl + "\n" + methods
} else {
// Non-types
if sizeOffset != "" {
signature += " // " + sizeOffset
}
}
// Compute link data (on pkg.go.dev or other documentation host).
//
// If linkPath is empty, the symbol is not linkable.
var (
linkName string // => link title, always non-empty
linkPath string // => link path
anchor string // link anchor
linkMeta *metadata.Package // metadata for the linked package
)
{
linkMeta = findFileInDeps(snapshot, pkg.Metadata(), declPGF.URI)
if linkMeta == nil {
return protocol.Range{}, nil, bug.Errorf("no package data for %s", declPGF.URI)
}
// For package names, we simply link to their imported package.
if pkgName, ok := obj.(*types.PkgName); ok {
linkName = pkgName.Name()
linkPath = pkgName.Imported().Path()
impID := linkMeta.DepsByPkgPath[PackagePath(pkgName.Imported().Path())]
linkMeta = snapshot.Metadata(impID)
if linkMeta == nil {
// Broken imports have fake package paths, so it is not a bug if we
// don't have metadata. As of writing, there is no way to distinguish
// broken imports from a true bug where expected metadata is missing.
return protocol.Range{}, nil, fmt.Errorf("no package data for %s", declPGF.URI)
}
} else {
// For all others, check whether the object is in the package scope, or
// an exported field or method of an object in the package scope.
//
// We try to match pkgsite's heuristics for what is linkable, and what is
// not.
var recv types.Object
switch obj := obj.(type) {
case *types.Func:
sig := obj.Type().(*types.Signature)
if sig.Recv() != nil {
tname := typeToObject(sig.Recv().Type())
if tname != nil { // beware typed nil
recv = tname
}
}
case *types.Var:
if obj.IsField() {
if spec, ok := spec.(*ast.TypeSpec); ok {
typeName := spec.Name
scopeObj, _ := obj.Pkg().Scope().Lookup(typeName.Name).(*types.TypeName)
if scopeObj != nil {
if st, _ := scopeObj.Type().Underlying().(*types.Struct); st != nil {
for i := 0; i < st.NumFields(); i++ {
if obj == st.Field(i) {
recv = scopeObj
}
}
}
}
}
}
}
// Even if the object is not available in package documentation, it may
// be embedded in a documented receiver. Detect this by searching
// enclosing selector expressions.
//
// TODO(rfindley): pkgsite doesn't document fields from embedding, just
// methods.
if recv == nil || !recv.Exported() {
path := pathEnclosingObjNode(pgf.File, pos)
if enclosing := searchForEnclosing(pkg.TypesInfo(), path); enclosing != nil {
recv = enclosing
} else {
recv = nil // note: just recv = ... could result in a typed nil.
}
}
pkg := obj.Pkg()
if recv != nil {
linkName = fmt.Sprintf("(%s.%s).%s", pkg.Name(), recv.Name(), obj.Name())
if obj.Exported() && recv.Exported() && isPackageLevel(recv) {
linkPath = pkg.Path()
anchor = fmt.Sprintf("%s.%s", recv.Name(), obj.Name())
}
} else {
linkName = fmt.Sprintf("%s.%s", pkg.Name(), obj.Name())
if obj.Exported() && isPackageLevel(obj) {
linkPath = pkg.Path()
anchor = obj.Name()
}
}
}
}
if snapshot.IsGoPrivatePath(linkPath) || linkMeta.ForTest != "" {
linkPath = ""
} else if linkMeta.Module != nil && linkMeta.Module.Version != "" {
mod := linkMeta.Module
linkPath = strings.Replace(linkPath, mod.Path, mod.Path+"@"+mod.Version, 1)
}
var version *stdlib.Version
if symbol := StdSymbolOf(obj); symbol != nil {
version = &symbol.Version
}
return *hoverRange, &hoverJSON{
Synopsis: doc.Synopsis(docText),
FullDocumentation: docText,
SingleLine: singleLineSignature,
SymbolName: linkName,
Signature: signature,
LinkPath: linkPath,
LinkAnchor: anchor,
typeDecl: typeDecl,
methods: methods,
promotedFields: fields,
stdVersion: version,
}, nil
}
// hoverBuiltin computes hover information when hovering over a builtin
// identifier.
func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Object) (*hoverJSON, error) {
// Special handling for error.Error, which is the only builtin method.
//
// TODO(rfindley): can this be unified with the handling below?
if obj.Name() == "Error" {
signature := obj.String()
return &hoverJSON{
Signature: signature,
SingleLine: signature,
// TODO(rfindley): these are better than the current behavior.
// SymbolName: "(error).Error",
// LinkPath: "builtin",
// LinkAnchor: "error.Error",
}, nil
}
pgf, ident, err := builtinDecl(ctx, snapshot, obj)
if err != nil {
return nil, err
}
var (
comment *ast.CommentGroup
decl ast.Decl
)
path, _ := astutil.PathEnclosingInterval(pgf.File, ident.Pos(), ident.Pos())
for _, n := range path {
switch n := n.(type) {
case *ast.GenDecl:
// Separate documentation and signature.
comment = n.Doc
node2 := *n
node2.Doc = nil
decl = &node2
case *ast.FuncDecl:
// Ditto.
comment = n.Doc
node2 := *n
node2.Doc = nil
decl = &node2
}
}
signature := formatNodeFile(pgf.Tok, decl)
// Replace fake types with their common equivalent.
// TODO(rfindley): we should instead use obj.Type(), which would have the
// *actual* types of the builtin call.
signature = replacer.Replace(signature)
docText := comment.Text()
return &hoverJSON{
Synopsis: doc.Synopsis(docText),
FullDocumentation: docText,
Signature: signature,
SingleLine: obj.String(),
SymbolName: obj.Name(),
LinkPath: "builtin",
LinkAnchor: obj.Name(),
}, nil
}
// hoverImport computes hover information when hovering over the import path of
// imp in the file pgf of pkg.
//
// If we do not have metadata for the hovered import, it returns _
func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, imp *ast.ImportSpec) (protocol.Range, *hoverJSON, error) {
rng, err := pgf.NodeRange(imp.Path)
if err != nil {
return protocol.Range{}, nil, err
}
importPath := metadata.UnquoteImportPath(imp)
if importPath == "" {
return protocol.Range{}, nil, fmt.Errorf("invalid import path")
}
impID := pkg.Metadata().DepsByImpPath[importPath]
if impID == "" {
return protocol.Range{}, nil, fmt.Errorf("no package data for import %q", importPath)
}
impMetadata := snapshot.Metadata(impID)
if impMetadata == nil {
return protocol.Range{}, nil, bug.Errorf("failed to resolve import ID %q", impID)
}
// Find the first file with a package doc comment.
var comment *ast.CommentGroup
for _, f := range impMetadata.CompiledGoFiles {
fh, err := snapshot.ReadFile(ctx, f)
if err != nil {
if ctx.Err() != nil {
return protocol.Range{}, nil, ctx.Err()
}
continue
}
pgf, err := snapshot.ParseGo(ctx, fh, parsego.Header)
if err != nil {
if ctx.Err() != nil {
return protocol.Range{}, nil, ctx.Err()
}
continue
}
if pgf.File.Doc != nil {
comment = pgf.File.Doc
break
}
}
docText := comment.Text()
return rng, &hoverJSON{
Synopsis: doc.Synopsis(docText),
FullDocumentation: docText,
}, nil
}
// hoverPackageName computes hover information for the package name of the file
// pgf in pkg.
func hoverPackageName(pkg *cache.Package, pgf *parsego.File) (protocol.Range, *hoverJSON, error) {
var comment *ast.CommentGroup
for _, pgf := range pkg.CompiledGoFiles() {
if pgf.File.Doc != nil {
comment = pgf.File.Doc
break
}
}
rng, err := pgf.NodeRange(pgf.File.Name)
if err != nil {
return protocol.Range{}, nil, err
}
docText := comment.Text()
return rng, &hoverJSON{
Synopsis: doc.Synopsis(docText),
FullDocumentation: docText,
// Note: including a signature is redundant, since the cursor is already on the
// package name.
}, nil
}
// hoverLit computes hover information when hovering over the basic literal lit
// in the file pgf. The provided pos must be the exact position of the cursor,
// as it is used to extract the hovered rune in strings.
//
// For example, hovering over "\u2211" in "foo \u2211 bar" yields:
//
// '∑', U+2211, N-ARY SUMMATION
func hoverLit(pgf *parsego.File, lit *ast.BasicLit, pos token.Pos) (protocol.Range, *hoverJSON, error) {
var (
value string // if non-empty, a constant value to format in hover
r rune // if non-zero, format a description of this rune in hover
start, end token.Pos // hover span
)
// Extract a rune from the current position.
// 'Ω', "...Ω...", or 0x03A9 => 'Ω', U+03A9, GREEK CAPITAL LETTER OMEGA
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 protocol.Range{}, nil, nil
}
r, _ = utf8.DecodeRuneInString(s)
if r == utf8.RuneError {
return protocol.Range{}, nil, fmt.Errorf("rune error")
}
start, end = lit.Pos(), lit.End()
case token.INT:
// Short literals (e.g. 99 decimal, 07 octal) are uninteresting.
if len(lit.Value) < 3 {
return protocol.Range{}, nil, nil
}
v := constant.MakeFromLiteral(lit.Value, lit.Kind, 0)
if v.Kind() != constant.Int {
return protocol.Range{}, nil, nil
}
switch lit.Value[:2] {
case "0x", "0X":
// As a special case, try to recognize hexadecimal literals as runes if
// they are within the range of valid unicode values.
if v, ok := constant.Int64Val(v); ok && v > 0 && v <= utf8.MaxRune && utf8.ValidRune(rune(v)) {
r = rune(v)
}
fallthrough
case "0o", "0O", "0b", "0B":
// Format the decimal value of non-decimal literals.
value = v.ExactString()
start, end = lit.Pos(), lit.End()
default:
return protocol.Range{}, nil, nil
}
case token.STRING:
// It's a string, scan only if it contains a unicode escape sequence under or before the
// current cursor position.
litOffset, err := safetoken.Offset(pgf.Tok, lit.Pos())
if err != nil {
return protocol.Range{}, nil, err
}
offset, err := safetoken.Offset(pgf.Tok, pos)
if err != nil {
return protocol.Range{}, nil, 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 protocol.Range{}, nil, 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 protocol.Range{}, nil, nil
}
// Only the rune escape sequence part of the string has to be highlighted, recompute the range.
runeLen := len(lit.Value) - (i + len(tail))
start = token.Pos(int(lit.Pos()) + i)
end = token.Pos(int(start) + runeLen)
break
}
}
}
if value == "" && r == 0 { // nothing to format
return protocol.Range{}, nil, nil
}
rng, err := pgf.PosRange(start, end)
if err != nil {
return protocol.Range{}, nil, err
}
var b strings.Builder
if value != "" {
b.WriteString(value)
}
if r != 0 {
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 b.Len() > 0 {
b.WriteString(", ")
}
if strconv.IsPrint(r) {
fmt.Fprintf(&b, "'%c', ", r)
}
fmt.Fprintf(&b, "U+%04X, %s", r, runeName)
}
hover := b.String()
return rng, &hoverJSON{
Synopsis: hover,
FullDocumentation: hover,
}, nil
}
// hoverEmbed computes hover information for a filepath.Match pattern.
// Assumes that the pattern is relative to the location of fh.
func hoverEmbed(fh file.Handle, rng protocol.Range, pattern string) (protocol.Range, *hoverJSON, error) {
s := &strings.Builder{}
dir := filepath.Dir(fh.URI().Path())
var matches []string
err := filepath.WalkDir(dir, func(abs string, d fs.DirEntry, e error) error {
if e != nil {
return e
}
rel, err := filepath.Rel(dir, abs)
if err != nil {
return err
}
ok, err := filepath.Match(pattern, rel)
if err != nil {
return err
}
if ok && !d.IsDir() {
matches = append(matches, rel)
}
return nil
})
if err != nil {
return protocol.Range{}, nil, err
}
for _, m := range matches {
// TODO: Renders each file as separate markdown paragraphs.
// If forcing (a single) newline is possible it might be more clear.
fmt.Fprintf(s, "%s\n\n", m)
}
json := &hoverJSON{
Signature: fmt.Sprintf("Embedding %q", pattern),
Synopsis: s.String(),
FullDocumentation: s.String(),
}
return rng, json, nil
}
// inferredSignatureString is a wrapper around the types.ObjectString function
// that adds more information to inferred signatures. It will return an empty string
// if the passed types.Object is not a signature.
func inferredSignatureString(obj types.Object, qf types.Qualifier, inferred *types.Signature) string {
// If the signature type was inferred, prefer the inferred signature with a
// comment showing the generic signature.
if sig, _ := obj.Type().Underlying().(*types.Signature); sig != nil && sig.TypeParams().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
}
return ""
}
// objectString is a wrapper around the types.ObjectString function.
// It handles adding more information to the object string.
// If spec is non-nil, it may be used to format additional declaration
// syntax, and file must be the token.File describing its positions.
//
// Precondition: obj is not a built-in function or method.
func objectString(obj types.Object, qf types.Qualifier, declPos token.Pos, file *token.File, spec ast.Spec) string {
str := types.ObjectString(obj, qf)
switch obj := obj.(type) {
case *types.Func:
// We fork ObjectString to improve its rendering of methods:
// specifically, we show the receiver name,
// and replace the period in (T).f by a space (#62190).
sig := obj.Type().(*types.Signature)
var buf bytes.Buffer
buf.WriteString("func ")
if recv := sig.Recv(); recv != nil {
buf.WriteByte('(')
if _, ok := recv.Type().(*types.Interface); ok {
// gcimporter creates abstract methods of
// named interfaces using the interface type
// (not the named type) as the receiver.
// Don't print it in full.
buf.WriteString("interface")
} else {
// Show receiver name (go/types does not).
name := recv.Name()
if name != "" && name != "_" {
buf.WriteString(name)
buf.WriteString(" ")
}
types.WriteType(&buf, recv.Type(), qf)
}
buf.WriteByte(')')
buf.WriteByte(' ') // space (go/types uses a period)
} else if s := qf(obj.Pkg()); s != "" {
buf.WriteString(s)
buf.WriteString(".")
}
buf.WriteString(obj.Name())
types.WriteSignature(&buf, sig, qf)
str = buf.String()
case *types.Const:
// Show value of a constant.
var (
declaration = obj.Val().String() // default formatted declaration
comment = "" // if non-empty, a clarifying comment
)
// Try to use the original declaration.
switch obj.Val().Kind() {
case constant.String:
// Usually the original declaration of a string doesn't carry much information.
// Also strings can be very long. So, just use the constant's value.
default:
if spec, _ := spec.(*ast.ValueSpec); spec != nil {
for i, name := range spec.Names {
if declPos == name.Pos() {
if i < len(spec.Values) {
originalDeclaration := formatNodeFile(file, spec.Values[i])
if originalDeclaration != declaration {
comment = declaration
declaration = originalDeclaration
}
}
break
}
}
}
}
// Special formatting cases.
switch typ := aliases.Unalias(obj.Type()).(type) {
case *types.Named:
// Try to add a formatted duration as an inline comment.
pkg := typ.Obj().Pkg()
if pkg.Path() == "time" && typ.Obj().Name() == "Duration" && obj.Val().Kind() == constant.Int {
if d, ok := constant.Int64Val(obj.Val()); ok {
comment = time.Duration(d).String()
}
}
}
if comment == declaration {
comment = ""
}
str += " = " + declaration
if comment != "" {
str += " // " + comment
}
}
return str
}
// HoverDocForObject returns the best doc comment for obj (for which
// fset provides file/line information).
//
// TODO(rfindley): there appears to be zero(!) tests for this functionality.
func HoverDocForObject(ctx context.Context, snapshot *cache.Snapshot, fset *token.FileSet, obj types.Object) (*ast.CommentGroup, error) {
if is[*types.TypeName](obj) && is[*types.TypeParam](obj.Type()) {
return nil, nil
}
pgf, pos, err := parseFull(ctx, snapshot, fset, obj.Pos())
if err != nil {
return nil, fmt.Errorf("re-parsing: %v", err)
}
decl, spec, field := findDeclInfo([]*ast.File{pgf.File}, pos)
return chooseDocComment(decl, spec, field), nil
}
func chooseDocComment(decl ast.Decl, spec ast.Spec, field *ast.Field) *ast.CommentGroup {
if field != nil {
if field.Doc != nil {
return field.Doc
}
if field.Comment != nil {
return field.Comment
}
return nil
}
switch decl := decl.(type) {
case *ast.FuncDecl:
return decl.Doc
case *ast.GenDecl:
switch spec := spec.(type) {
case *ast.ValueSpec:
if spec.Doc != nil {
return spec.Doc
}
if decl.Doc != nil {
return decl.Doc
}
return spec.Comment
case *ast.TypeSpec:
if spec.Doc != nil {
return spec.Doc
}
if decl.Doc != nil {
return decl.Doc
}
return spec.Comment
}
}
return nil
}
// parseFull fully parses the file corresponding to position pos (for
// which fset provides file/line information).
//
// It returns the resulting parsego.File as well as new pos contained
// in the parsed file.
//
// BEWARE: the provided FileSet is used only to interpret the provided
// pos; the resulting File and Pos may belong to the same or a
// different FileSet, such as one synthesized by the parser cache, if
// parse-caching is enabled.
func parseFull(ctx context.Context, snapshot *cache.Snapshot, fset *token.FileSet, pos token.Pos) (*parsego.File, token.Pos, error) {
f := fset.File(pos)
if f == nil {
return nil, 0, bug.Errorf("internal error: no file for position %d", pos)
}
uri := protocol.URIFromPath(f.Name())
fh, err := snapshot.ReadFile(ctx, uri)
if err != nil {
return nil, 0, err
}
pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full)
if err != nil {
return nil, 0, err
}
offset, err := safetoken.Offset(f, pos)
if err != nil {
return nil, 0, bug.Errorf("offset out of bounds in %q", uri)
}
fullPos, err := safetoken.Pos(pgf.Tok, offset)
if err != nil {
return nil, 0, err
}
return pgf, fullPos, nil
}
// If pkgURL is non-nil, it should be used to generate doc links.
func formatHover(h *hoverJSON, options *settings.Options, pkgURL func(path PackagePath, fragment string) protocol.URI) (string, error) {
maybeMarkdown := func(s string) string {
if s != "" && options.PreferredContentFormat == protocol.Markdown {
s = fmt.Sprintf("```go\n%s\n```", strings.Trim(s, "\n"))
}
return s
}
switch options.HoverKind {
case settings.SingleLine:
return h.SingleLine, nil
case settings.NoDocumentation:
return maybeMarkdown(h.Signature), nil
case settings.Structured:
b, err := json.Marshal(h)
if err != nil {
return "", err
}
return string(b), nil
case settings.SynopsisDocumentation,
settings.FullDocumentation:
// For types, we display TypeDecl and Methods,
// but not Signature, which is redundant (= TypeDecl + "\n" + Methods).
// For all other symbols, we display Signature;
// TypeDecl and Methods are empty.
// (This awkwardness is to preserve JSON compatibility.)
parts := []string{
maybeMarkdown(h.Signature),
maybeMarkdown(h.typeDecl),
formatDoc(h, options),
maybeMarkdown(h.promotedFields),
maybeMarkdown(h.methods),
fmt.Sprintf("Added in %v", h.stdVersion),
formatLink(h, options, pkgURL),
}
if h.typeDecl != "" {
parts[0] = "" // type: suppress redundant Signature
}
if h.stdVersion == nil || *h.stdVersion == stdlib.Version(0) {
parts[5] = "" // suppress stdlib version if not applicable or initial version 1.0
}
parts = slices.Remove(parts, "")
var b strings.Builder
for i, part := range parts {
if i > 0 {
if options.PreferredContentFormat == protocol.Markdown {
b.WriteString("\n\n")
} else {
b.WriteByte('\n')
}
}
b.WriteString(part)
}
return b.String(), nil
default:
return "", fmt.Errorf("invalid HoverKind: %v", options.HoverKind)
}
}
// StdSymbolOf returns the std lib symbol information of the given obj.
// It returns nil if the input obj is not an exported standard library symbol.
func StdSymbolOf(obj types.Object) *stdlib.Symbol {
if !obj.Exported() || obj.Pkg() == nil {
return nil
}
// Symbols that not defined in standard library should return early.
// TODO(hxjiang): The returned slices is binary searchable.
symbols := stdlib.PackageSymbols[obj.Pkg().Path()]
if symbols == nil {
return nil
}
// Handle Function, Type, Const & Var.
if isPackageLevel(obj) {
for _, s := range symbols {
if s.Kind == stdlib.Method || s.Kind == stdlib.Field {
continue
}
if s.Name == obj.Name() {
return &s
}
}
return nil
}
// Handle Method.
if fn, _ := obj.(*types.Func); fn != nil {
isPtr, named := typesinternal.ReceiverNamed(fn.Type().(*types.Signature).Recv())
if isPackageLevel(named.Obj()) {
for _, s := range symbols {
if s.Kind != stdlib.Method {
continue
}
ptr, recv, name := s.SplitMethod()
if ptr == isPtr && recv == named.Obj().Name() && name == fn.Name() {
return &s
}
}
return nil
}
}
// Handle Field.
if v, _ := obj.(*types.Var); v != nil && v.IsField() {
for _, s := range symbols {
if s.Kind != stdlib.Field {
continue
}
typeName, fieldName := s.SplitField()
if fieldName != v.Name() {
continue
}
typeObj := obj.Pkg().Scope().Lookup(typeName)
if typeObj == nil {
continue
}
if fieldObj, _, _ := types.LookupFieldOrMethod(typeObj.Type(), true, obj.Pkg(), fieldName); obj == fieldObj {
return &s
}
}
return nil
}
return nil
}
// If pkgURL is non-nil, it should be used to generate doc links.
func formatLink(h *hoverJSON, options *settings.Options, pkgURL func(path PackagePath, fragment string) protocol.URI) string {
if options.LinksInHover == settings.LinksInHover_None || h.LinkPath == "" {
return ""
}
var url protocol.URI
var caption string
if pkgURL != nil { // LinksInHover == "gopls"
path, _, _ := strings.Cut(h.LinkPath, "@") // remove optional module version suffix
url = pkgURL(PackagePath(path), h.LinkAnchor)
caption = "in gopls doc viewer"
} else {
if options.LinkTarget == "" {
return ""
}
url = cache.BuildLink(options.LinkTarget, h.LinkPath, h.LinkAnchor)
caption = "on " + options.LinkTarget
}
switch options.PreferredContentFormat {
case protocol.Markdown:
return fmt.Sprintf("[`%s` %s](%s)", h.SymbolName, caption, url)
case protocol.PlainText:
return ""
default:
return url
}
}
func formatDoc(h *hoverJSON, options *settings.Options) string {
var doc string
switch options.HoverKind {
case settings.SynopsisDocumentation:
doc = h.Synopsis
case settings.FullDocumentation:
doc = h.FullDocumentation
}
if options.PreferredContentFormat == protocol.Markdown {
return CommentToMarkdown(doc, options)
}
return doc
}
// findDeclInfo returns the syntax nodes involved in the declaration of the
// types.Object with position pos, searching the given list of file syntax
// trees.
//
// Pos may be the position of the name-defining identifier in a FuncDecl,
// ValueSpec, TypeSpec, Field, or as a special case the position of
// Ellipsis.Elt in an ellipsis field.
//
// If found, the resulting decl, spec, and field will be the inner-most
// instance of each node type surrounding pos.
//
// If field is non-nil, pos is the position of a field Var. If field is nil and
// spec is non-nil, pos is the position of a Var, Const, or TypeName object. If
// both field and spec are nil and decl is non-nil, pos is the position of a
// Func object.
//
// It returns a nil decl if no object-defining node is found at pos.
//
// TODO(rfindley): this function has tricky semantics, and may be worth unit
// testing and/or refactoring.
func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spec, field *ast.Field) {
found := false
// Visit the files in search of the node at pos.
stack := make([]ast.Node, 0, 20)
// Allocate the closure once, outside the loop.
f := func(n ast.Node) bool {
if found {
return false
}
if n != nil {
stack = append(stack, n) // push
} else {
stack = stack[:len(stack)-1] // pop
return false
}
// Skip subtrees (incl. files) that don't contain the search point.
if !(n.Pos() <= pos && pos < n.End()) {
return false
}
switch n := n.(type) {
case *ast.Field:
findEnclosingDeclAndSpec := func() {
for i := len(stack) - 1; i >= 0; i-- {
switch n := stack[i].(type) {
case ast.Spec:
spec = n
case ast.Decl:
decl = n
return
}
}
}
// Check each field name since you can have
// multiple names for the same type expression.
for _, id := range n.Names {
if id.Pos() == pos {
field = n
findEnclosingDeclAndSpec()
found = true
return false
}
}
// Check *ast.Field itself. This handles embedded
// fields which have no associated *ast.Ident name.
if n.Pos() == pos {
field = n
findEnclosingDeclAndSpec()
found = true
return false
}
// Also check "X" in "...X". This makes it easy to format variadic
// signature params properly.
//
// TODO(rfindley): I don't understand this comment. How does finding the
// field in this case make it easier to format variadic signature params?
if ell, ok := n.Type.(*ast.Ellipsis); ok && ell.Elt != nil && ell.Elt.Pos() == pos {
field = n
findEnclosingDeclAndSpec()
found = true
return false
}
case *ast.FuncDecl:
if n.Name.Pos() == pos {
decl = n
found = true
return false
}
case *ast.GenDecl:
for _, s := range n.Specs {
switch s := s.(type) {
case *ast.TypeSpec:
if s.Name.Pos() == pos {
decl = n
spec = s
found = true
return false
}
case *ast.ValueSpec:
for _, id := range s.Names {
if id.Pos() == pos {
decl = n
spec = s
found = true
return false
}
}
}
}
}
return true
}
for _, file := range files {
ast.Inspect(file, f)
if found {
return decl, spec, field
}
}
return nil, nil, nil
}
type promotedField struct {
path string // path (e.g. "x.y" through embedded fields)
field *types.Var
}
// promotedFields returns the list of accessible promoted fields of a struct type t.
// (Logic plundered from x/tools/cmd/guru/describe.go.)
func promotedFields(t types.Type, from *types.Package) []promotedField {
wantField := func(f *types.Var) bool {
if !accessibleTo(f, from) {
return false
}
// Check that the field is not shadowed.
obj, _, _ := types.LookupFieldOrMethod(t, true, f.Pkg(), f.Name())
return obj == f
}
var fields []promotedField
var visit func(t types.Type, stack []*types.Named)
visit = func(t types.Type, stack []*types.Named) {
tStruct, ok := typesinternal.Unpointer(t).Underlying().(*types.Struct)
if !ok {
return
}
fieldloop:
for i := 0; i < tStruct.NumFields(); i++ {
f := tStruct.Field(i)
// Handle recursion through anonymous fields.
if f.Anonymous() {
if _, named := typesinternal.ReceiverNamed(f); named != nil {
// If we've already visited this named type
// on this path, break the cycle.
for _, x := range stack {
if x.Origin() == named.Origin() {
continue fieldloop
}
}
visit(f.Type(), append(stack, named))
}
}
// Save accessible promoted fields.
if len(stack) > 0 && wantField(f) {
var path strings.Builder
for i, t := range stack {
if i > 0 {
path.WriteByte('.')
}
path.WriteString(t.Obj().Name())
}
fields = append(fields, promotedField{
path: path.String(),
field: f,
})
}
}
}
visit(t, nil)
return fields
}
func accessibleTo(obj types.Object, pkg *types.Package) bool {
return obj.Exported() || obj.Pkg() == pkg
}
// computeSizeOffsetInfo reports the size of obj (if a type or struct
// field), its wasted space percentage (if a struct type), and its
// offset (if a struct field). It returns -1 for undefined components.
func computeSizeOffsetInfo(pkg *cache.Package, path []ast.Node, obj types.Object) (size, wasted, offset int64) {
size, wasted, offset = -1, -1, -1
var free typeparams.Free
sizes := pkg.TypesSizes()
// size (types and fields)
if v, ok := obj.(*types.Var); ok && v.IsField() || is[*types.TypeName](obj) {
// If the field's type has free type parameters,
// its size cannot be computed.
if !free.Has(obj.Type()) {
size = sizes.Sizeof(obj.Type())
}
// wasted space (struct types)
if tStruct, ok := obj.Type().Underlying().(*types.Struct); ok && is[*types.TypeName](obj) && size > 0 {
var fields []*types.Var
for i := 0; i < tStruct.NumFields(); i++ {
fields = append(fields, tStruct.Field(i))
}
if len(fields) > 0 {
// Sort into descending (most compact) order
// and recompute size of entire struct.
sort.Slice(fields, func(i, j int) bool {
return sizes.Sizeof(fields[i].Type()) >
sizes.Sizeof(fields[j].Type())
})
offsets := sizes.Offsetsof(fields)
compactSize := offsets[len(offsets)-1] + sizes.Sizeof(fields[len(fields)-1].Type())
wasted = 100 * (size - compactSize) / size
}
}
}
// offset (fields)
if v, ok := obj.(*types.Var); ok && v.IsField() {
// Find enclosing struct type.
var tStruct *types.Struct
for _, n := range path {
if n, ok := n.(*ast.StructType); ok {
tStruct = pkg.TypesInfo().TypeOf(n).(*types.Struct)
break
}
}
if tStruct != nil {
var fields []*types.Var
for i := 0; i < tStruct.NumFields(); i++ {
f := tStruct.Field(i)
// If any preceding field's type has free type parameters,
// its offset cannot be computed.
if free.Has(f.Type()) {
break
}
fields = append(fields, f)
if f == v {
offsets := sizes.Offsetsof(fields)
offset = offsets[len(offsets)-1]
break
}
}
}
}
return
}