// 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: 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 _, pkg, _ := FindFileInPackage(i.pkg, i.Declaration.MappedRange[0].URI()); i.pkg == pkg { return "", "", "" } } // Don't return links for other unexported types. if !obj.Exported() { return "", "", "" } var rTypeName string switch obj := obj.(type) { case *types.Var: if obj.IsField() { // If the object is a field, and we have an associated selector // composite literal, or struct, we can determine the link. switch typ := i.enclosing.(type) { case *types.Named: rTypeName = typ.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 named, ok := i.enclosing.(*types.Named); ok { rTypeName = named.Obj().Name() } else if !rtyp.Obj().Exported() { 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. var doc *ast.CommentGroup for _, file := range imp.GetSyntax() { if file.Doc != nil { info = &HoverInformation{source: obj, comment: doc} } } } 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 }