// 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 } pos, err := pgf.Mapper.Pos(position) if err != nil { return 0, MappedRange{}, err } // 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 }