github.com/v2fly/tools@v0.100.0/internal/lsp/source/hover.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package source 6 7 import ( 8 "context" 9 "encoding/json" 10 "fmt" 11 "go/ast" 12 "go/constant" 13 "go/doc" 14 "go/format" 15 "go/token" 16 "go/types" 17 "strings" 18 "time" 19 20 "github.com/v2fly/tools/internal/event" 21 "github.com/v2fly/tools/internal/lsp/protocol" 22 "github.com/v2fly/tools/internal/span" 23 errors "golang.org/x/xerrors" 24 ) 25 26 type HoverInformation struct { 27 // Signature is the symbol's signature. 28 Signature string `json:"signature"` 29 30 // SingleLine is a single line describing the symbol. 31 // This is recommended only for use in clients that show a single line for hover. 32 SingleLine string `json:"singleLine"` 33 34 // Synopsis is a single sentence synopsis of the symbol's documentation. 35 Synopsis string `json:"synopsis"` 36 37 // FullDocumentation is the symbol's full documentation. 38 FullDocumentation string `json:"fullDocumentation"` 39 40 // LinkPath is the pkg.go.dev link for the given symbol. 41 // For example, the "go/ast" part of "pkg.go.dev/go/ast#Node". 42 LinkPath string `json:"linkPath"` 43 44 // LinkAnchor is the pkg.go.dev link anchor for the given symbol. 45 // For example, the "Node" part of "pkg.go.dev/go/ast#Node". 46 LinkAnchor string `json:"linkAnchor"` 47 48 // importPath is the import path for the package containing the given 49 // symbol. 50 importPath string 51 52 // symbolName is the types.Object.Name for the given symbol. 53 symbolName string 54 55 source interface{} 56 comment *ast.CommentGroup 57 58 // typeName contains the identifier name when the identifier is a type declaration. 59 // If it is not empty, the hover will have the prefix "type <typeName> ". 60 typeName string 61 // isTypeAlias indicates whether the identifier is a type alias declaration. 62 // If it is true, the hover will have the prefix "type <typeName> = ". 63 isTypeAlias bool 64 } 65 66 func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) { 67 ident, err := Identifier(ctx, snapshot, fh, position) 68 if err != nil { 69 return nil, nil 70 } 71 h, err := HoverIdentifier(ctx, ident) 72 if err != nil { 73 return nil, err 74 } 75 rng, err := ident.Range() 76 if err != nil { 77 return nil, err 78 } 79 // See golang/go#36998: don't link to modules matching GOPRIVATE. 80 if snapshot.View().IsGoPrivatePath(h.importPath) { 81 h.LinkPath = "" 82 } 83 hover, err := FormatHover(h, snapshot.View().Options()) 84 if err != nil { 85 return nil, err 86 } 87 return &protocol.Hover{ 88 Contents: protocol.MarkupContent{ 89 Kind: snapshot.View().Options().PreferredContentFormat, 90 Value: hover, 91 }, 92 Range: rng, 93 }, nil 94 } 95 96 func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverInformation, error) { 97 ctx, done := event.Start(ctx, "source.Hover") 98 defer done() 99 100 fset := i.Snapshot.FileSet() 101 h, err := HoverInfo(ctx, i.Snapshot, i.pkg, i.Declaration.obj, i.Declaration.node) 102 if err != nil { 103 return nil, err 104 } 105 // Determine the symbol's signature. 106 switch x := h.source.(type) { 107 case ast.Node: 108 var b strings.Builder 109 if err := format.Node(&b, fset, x); err != nil { 110 return nil, err 111 } 112 h.Signature = b.String() 113 if h.typeName != "" { 114 prefix := "type " + h.typeName + " " 115 if h.isTypeAlias { 116 prefix += "= " 117 } 118 h.Signature = prefix + h.Signature 119 } 120 case types.Object: 121 // If the variable is implicitly declared in a type switch, we need to 122 // manually generate its object string. 123 if typ := i.Declaration.typeSwitchImplicit; typ != nil { 124 if v, ok := x.(*types.Var); ok { 125 h.Signature = fmt.Sprintf("var %s %s", v.Name(), types.TypeString(typ, i.qf)) 126 break 127 } 128 } 129 h.Signature = objectString(x, i.qf) 130 } 131 if obj := i.Declaration.obj; obj != nil { 132 h.SingleLine = objectString(obj, i.qf) 133 } 134 obj := i.Declaration.obj 135 if obj == nil { 136 return h, nil 137 } 138 switch obj := obj.(type) { 139 case *types.PkgName: 140 h.importPath = obj.Imported().Path() 141 h.LinkPath = h.importPath 142 h.symbolName = obj.Name() 143 if mod, version, ok := moduleAtVersion(h.LinkPath, i); ok { 144 h.LinkPath = strings.Replace(h.LinkPath, mod, mod+"@"+version, 1) 145 } 146 return h, nil 147 case *types.Builtin: 148 h.importPath = "builtin" 149 h.LinkPath = h.importPath 150 h.LinkAnchor = obj.Name() 151 h.symbolName = h.LinkAnchor 152 return h, nil 153 } 154 // Check if the identifier is test-only (and is therefore not part of a 155 // package's API). This is true if the request originated in a test package, 156 // and if the declaration is also found in the same test package. 157 if i.pkg != nil && obj.Pkg() != nil && i.pkg.ForTest() != "" { 158 if _, err := i.pkg.File(i.Declaration.MappedRange[0].URI()); err == nil { 159 return h, nil 160 } 161 } 162 // Don't return links for other unexported types. 163 if !obj.Exported() { 164 return h, nil 165 } 166 var rTypeName string 167 switch obj := obj.(type) { 168 case *types.Var: 169 // If the object is a field, and we have an associated selector 170 // composite literal, or struct, we can determine the link. 171 if obj.IsField() { 172 if named, ok := i.enclosing.(*types.Named); ok { 173 rTypeName = named.Obj().Name() 174 } 175 } 176 case *types.Func: 177 typ, ok := obj.Type().(*types.Signature) 178 if !ok { 179 return h, nil 180 } 181 if r := typ.Recv(); r != nil { 182 switch rtyp := Deref(r.Type()).(type) { 183 case *types.Struct: 184 rTypeName = r.Name() 185 case *types.Named: 186 // If we have an unexported type, see if the enclosing type is 187 // exported (we may have an interface or struct we can link 188 // to). If not, don't show any link. 189 if !rtyp.Obj().Exported() { 190 if named, ok := i.enclosing.(*types.Named); ok && named.Obj().Exported() { 191 rTypeName = named.Obj().Name() 192 } else { 193 return h, nil 194 } 195 } else { 196 rTypeName = rtyp.Obj().Name() 197 } 198 } 199 } 200 } 201 if obj.Pkg() == nil { 202 event.Log(ctx, fmt.Sprintf("nil package for %s", obj)) 203 return h, nil 204 } 205 h.importPath = obj.Pkg().Path() 206 h.LinkPath = h.importPath 207 if mod, version, ok := moduleAtVersion(h.LinkPath, i); ok { 208 h.LinkPath = strings.Replace(h.LinkPath, mod, mod+"@"+version, 1) 209 } 210 if rTypeName != "" { 211 h.LinkAnchor = fmt.Sprintf("%s.%s", rTypeName, obj.Name()) 212 h.symbolName = fmt.Sprintf("(%s.%s).%s", obj.Pkg().Name(), rTypeName, obj.Name()) 213 return h, nil 214 } 215 // For most cases, the link is "package/path#symbol". 216 h.LinkAnchor = obj.Name() 217 h.symbolName = fmt.Sprintf("%s.%s", obj.Pkg().Name(), obj.Name()) 218 return h, nil 219 } 220 221 func moduleAtVersion(path string, i *IdentifierInfo) (string, string, bool) { 222 if strings.ToLower(i.Snapshot.View().Options().LinkTarget) != "pkg.go.dev" { 223 return "", "", false 224 } 225 impPkg, err := i.pkg.GetImport(path) 226 if err != nil { 227 return "", "", false 228 } 229 if impPkg.Version() == nil { 230 return "", "", false 231 } 232 version, modpath := impPkg.Version().Version, impPkg.Version().Path 233 if modpath == "" || version == "" { 234 return "", "", false 235 } 236 return modpath, version, true 237 } 238 239 // objectString is a wrapper around the types.ObjectString function. 240 // It handles adding more information to the object string. 241 func objectString(obj types.Object, qf types.Qualifier) string { 242 str := types.ObjectString(obj, qf) 243 switch obj := obj.(type) { 244 case *types.Const: 245 str = fmt.Sprintf("%s = %s", str, obj.Val()) 246 247 // Try to add a formatted duration as an inline comment 248 typ, ok := obj.Type().(*types.Named) 249 if !ok { 250 break 251 } 252 pkg := typ.Obj().Pkg() 253 if pkg.Path() == "time" && typ.Obj().Name() == "Duration" { 254 if d, ok := constant.Int64Val(obj.Val()); ok { 255 str += " // " + time.Duration(d).String() 256 } 257 } 258 } 259 return str 260 } 261 262 // HoverInfo returns a HoverInformation struct for an ast node and its type 263 // object. 264 func HoverInfo(ctx context.Context, s Snapshot, pkg Package, obj types.Object, node ast.Node) (*HoverInformation, error) { 265 var info *HoverInformation 266 267 switch node := node.(type) { 268 case *ast.Ident: 269 // The package declaration. 270 for _, f := range pkg.GetSyntax() { 271 if f.Name == node { 272 info = &HoverInformation{comment: f.Doc} 273 } 274 } 275 case *ast.ImportSpec: 276 // Try to find the package documentation for an imported package. 277 if pkgName, ok := obj.(*types.PkgName); ok { 278 imp, err := pkg.GetImport(pkgName.Imported().Path()) 279 if err != nil { 280 return nil, err 281 } 282 // Assume that only one file will contain package documentation, 283 // so pick the first file that has a doc comment. 284 for _, file := range imp.GetSyntax() { 285 if file.Doc != nil { 286 info = &HoverInformation{source: obj, comment: file.Doc} 287 break 288 } 289 } 290 } 291 info = &HoverInformation{source: node} 292 case *ast.GenDecl: 293 switch obj := obj.(type) { 294 case *types.TypeName, *types.Var, *types.Const, *types.Func: 295 var err error 296 info, err = formatGenDecl(node, obj, obj.Type()) 297 if err != nil { 298 return nil, err 299 } 300 } 301 case *ast.TypeSpec: 302 if obj.Parent() == types.Universe { 303 if obj.Name() == "error" { 304 info = &HoverInformation{source: node} 305 } else { 306 info = &HoverInformation{source: node.Name} // comments not needed for builtins 307 } 308 } 309 case *ast.FuncDecl: 310 switch obj.(type) { 311 case *types.Func: 312 info = &HoverInformation{source: obj, comment: node.Doc} 313 case *types.Builtin: 314 info = &HoverInformation{source: node.Type, comment: node.Doc} 315 case *types.Var: 316 // Object is a function param or the field of an anonymous struct 317 // declared with ':='. Skip the first one because only fields 318 // can have docs. 319 if isFunctionParam(obj, node) { 320 break 321 } 322 323 f := s.FileSet().File(obj.Pos()) 324 if f == nil { 325 break 326 } 327 328 pgf, err := pkg.File(span.URIFromPath(f.Name())) 329 if err != nil { 330 return nil, err 331 } 332 posToField, err := s.PosToField(ctx, pgf) 333 if err != nil { 334 return nil, err 335 } 336 337 if field := posToField[obj.Pos()]; field != nil { 338 comment := field.Doc 339 if comment.Text() == "" { 340 comment = field.Comment 341 } 342 info = &HoverInformation{source: obj, comment: comment} 343 } 344 } 345 } 346 347 if info == nil { 348 info = &HoverInformation{source: obj} 349 } 350 351 if info.comment != nil { 352 info.FullDocumentation = info.comment.Text() 353 info.Synopsis = doc.Synopsis(info.FullDocumentation) 354 } 355 356 return info, nil 357 } 358 359 // isFunctionParam returns true if the passed object is either an incoming 360 // or an outgoing function param 361 func isFunctionParam(obj types.Object, node *ast.FuncDecl) bool { 362 for _, f := range node.Type.Params.List { 363 if f.Pos() == obj.Pos() { 364 return true 365 } 366 } 367 if node.Type.Results != nil { 368 for _, f := range node.Type.Results.List { 369 if f.Pos() == obj.Pos() { 370 return true 371 } 372 } 373 } 374 return false 375 } 376 377 func formatGenDecl(node *ast.GenDecl, obj types.Object, typ types.Type) (*HoverInformation, error) { 378 if _, ok := typ.(*types.Named); ok { 379 switch typ.Underlying().(type) { 380 case *types.Interface, *types.Struct: 381 return formatGenDecl(node, obj, typ.Underlying()) 382 } 383 } 384 var spec ast.Spec 385 for _, s := range node.Specs { 386 if s.Pos() <= obj.Pos() && obj.Pos() <= s.End() { 387 spec = s 388 break 389 } 390 } 391 if spec == nil { 392 return nil, errors.Errorf("no spec for node %v at position %v", node, obj.Pos()) 393 } 394 395 // If we have a field or method. 396 switch obj.(type) { 397 case *types.Var, *types.Const, *types.Func: 398 return formatVar(spec, obj, node), nil 399 } 400 // Handle types. 401 switch spec := spec.(type) { 402 case *ast.TypeSpec: 403 comment := spec.Doc 404 if comment == nil { 405 comment = node.Doc 406 } 407 if comment == nil { 408 comment = spec.Comment 409 } 410 return &HoverInformation{ 411 source: spec.Type, 412 comment: comment, 413 typeName: spec.Name.Name, 414 isTypeAlias: spec.Assign.IsValid(), 415 }, nil 416 case *ast.ValueSpec: 417 return &HoverInformation{source: spec, comment: spec.Doc}, nil 418 case *ast.ImportSpec: 419 return &HoverInformation{source: spec, comment: spec.Doc}, nil 420 } 421 return nil, errors.Errorf("unable to format spec %v (%T)", spec, spec) 422 } 423 424 func formatVar(node ast.Spec, obj types.Object, decl *ast.GenDecl) *HoverInformation { 425 var fieldList *ast.FieldList 426 switch spec := node.(type) { 427 case *ast.TypeSpec: 428 switch t := spec.Type.(type) { 429 case *ast.StructType: 430 fieldList = t.Fields 431 case *ast.InterfaceType: 432 fieldList = t.Methods 433 } 434 case *ast.ValueSpec: 435 // Try to extract the field list of an anonymous struct 436 if fieldList = extractFieldList(spec.Type); fieldList != nil { 437 break 438 } 439 440 comment := spec.Doc 441 if comment == nil { 442 comment = decl.Doc 443 } 444 if comment == nil { 445 comment = spec.Comment 446 } 447 return &HoverInformation{source: obj, comment: comment} 448 } 449 450 if fieldList != nil { 451 comment := findFieldComment(obj.Pos(), fieldList) 452 return &HoverInformation{source: obj, comment: comment} 453 } 454 return &HoverInformation{source: obj, comment: decl.Doc} 455 } 456 457 // extractFieldList recursively tries to extract a field list. 458 // If it is not found, nil is returned. 459 func extractFieldList(specType ast.Expr) *ast.FieldList { 460 switch t := specType.(type) { 461 case *ast.StructType: 462 return t.Fields 463 case *ast.InterfaceType: 464 return t.Methods 465 case *ast.ArrayType: 466 return extractFieldList(t.Elt) 467 case *ast.MapType: 468 // Map value has a greater chance to be a struct 469 if fields := extractFieldList(t.Value); fields != nil { 470 return fields 471 } 472 return extractFieldList(t.Key) 473 case *ast.ChanType: 474 return extractFieldList(t.Value) 475 } 476 return nil 477 } 478 479 // findFieldComment visits all fields in depth-first order and returns 480 // the comment of a field with passed position. If no comment is found, 481 // nil is returned. 482 func findFieldComment(pos token.Pos, fieldList *ast.FieldList) *ast.CommentGroup { 483 for _, field := range fieldList.List { 484 if field.Pos() == pos { 485 if field.Doc.Text() != "" { 486 return field.Doc 487 } 488 return field.Comment 489 } 490 491 if nestedFieldList := extractFieldList(field.Type); nestedFieldList != nil { 492 if c := findFieldComment(pos, nestedFieldList); c != nil { 493 return c 494 } 495 } 496 } 497 return nil 498 } 499 500 func FormatHover(h *HoverInformation, options *Options) (string, error) { 501 signature := h.Signature 502 if signature != "" && options.PreferredContentFormat == protocol.Markdown { 503 signature = fmt.Sprintf("```go\n%s\n```", signature) 504 } 505 506 switch options.HoverKind { 507 case SingleLine: 508 return h.SingleLine, nil 509 case NoDocumentation: 510 return signature, nil 511 case Structured: 512 b, err := json.Marshal(h) 513 if err != nil { 514 return "", err 515 } 516 return string(b), nil 517 } 518 link := formatLink(h, options) 519 switch options.HoverKind { 520 case SynopsisDocumentation: 521 doc := formatDoc(h.Synopsis, options) 522 return formatHover(options, signature, link, doc), nil 523 case FullDocumentation: 524 doc := formatDoc(h.FullDocumentation, options) 525 return formatHover(options, signature, link, doc), nil 526 } 527 return "", errors.Errorf("no hover for %v", h.source) 528 } 529 530 func formatLink(h *HoverInformation, options *Options) string { 531 if !options.LinksInHover || options.LinkTarget == "" || h.LinkPath == "" { 532 return "" 533 } 534 plainLink := BuildLink(options.LinkTarget, h.LinkPath, h.LinkAnchor) 535 switch options.PreferredContentFormat { 536 case protocol.Markdown: 537 return fmt.Sprintf("[`%s` on %s](%s)", h.symbolName, options.LinkTarget, plainLink) 538 case protocol.PlainText: 539 return "" 540 default: 541 return plainLink 542 } 543 } 544 545 // BuildLink constructs a link with the given target, path, and anchor. 546 func BuildLink(target, path, anchor string) string { 547 link := fmt.Sprintf("https://%s/%s", target, path) 548 if target == "pkg.go.dev" { 549 link += "?utm_source=gopls" 550 } 551 if anchor == "" { 552 return link 553 } 554 return link + "#" + anchor 555 } 556 557 func formatDoc(doc string, options *Options) string { 558 if options.PreferredContentFormat == protocol.Markdown { 559 return CommentToMarkdown(doc) 560 } 561 return doc 562 } 563 564 func formatHover(options *Options, x ...string) string { 565 var b strings.Builder 566 for i, el := range x { 567 if el != "" { 568 b.WriteString(el) 569 570 // Don't write out final newline. 571 if i == len(x) { 572 continue 573 } 574 // If any elements of the remainder of the list are non-empty, 575 // write a newline. 576 if anyNonEmpty(x[i+1:]) { 577 if options.PreferredContentFormat == protocol.Markdown { 578 b.WriteString("\n\n") 579 } else { 580 b.WriteRune('\n') 581 } 582 } 583 } 584 } 585 return b.String() 586 } 587 588 func anyNonEmpty(x []string) bool { 589 for _, el := range x { 590 if el != "" { 591 return true 592 } 593 } 594 return false 595 }