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
5package source
6
7import (
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	"golang.org/x/tools/internal/event"
21	"golang.org/x/tools/internal/lsp/protocol"
22	"golang.org/x/tools/internal/typeparams"
23	errors "golang.org/x/xerrors"
24)
25
26type 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
66func 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
96func 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, i.Declaration.fullSpec)
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, i.Inferred)
130	}
131	if obj := i.Declaration.obj; obj != nil {
132		h.SingleLine = objectString(obj, i.qf, nil)
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
221func 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.
241func objectString(obj types.Object, qf types.Qualifier, inferred *types.Signature) string {
242	// If the signature type was inferred, prefer the preferred signature with a
243	// comment showing the generic signature.
244	if sig, _ := obj.Type().(*types.Signature); sig != nil && len(typeparams.ForSignature(sig)) > 0 && inferred != nil {
245		obj2 := types.NewFunc(obj.Pos(), obj.Pkg(), obj.Name(), inferred)
246		str := types.ObjectString(obj2, qf)
247		// Try to avoid overly long lines.
248		if len(str) > 60 {
249			str += "\n"
250		} else {
251			str += " "
252		}
253		str += "// " + types.TypeString(sig, qf)
254		return str
255	}
256	str := types.ObjectString(obj, qf)
257	switch obj := obj.(type) {
258	case *types.Const:
259		str = fmt.Sprintf("%s = %s", str, obj.Val())
260
261		// Try to add a formatted duration as an inline comment
262		typ, ok := obj.Type().(*types.Named)
263		if !ok {
264			break
265		}
266		pkg := typ.Obj().Pkg()
267		if pkg.Path() == "time" && typ.Obj().Name() == "Duration" {
268			if d, ok := constant.Int64Val(obj.Val()); ok {
269				str += " // " + time.Duration(d).String()
270			}
271		}
272	}
273	return str
274}
275
276// HoverInfo returns a HoverInformation struct for an ast node and its type
277// object.
278func HoverInfo(ctx context.Context, s Snapshot, pkg Package, obj types.Object, node ast.Node, spec ast.Spec) (*HoverInformation, error) {
279	var info *HoverInformation
280
281	switch node := node.(type) {
282	case *ast.Ident:
283		// The package declaration.
284		for _, f := range pkg.GetSyntax() {
285			if f.Name == node {
286				info = &HoverInformation{comment: f.Doc}
287			}
288		}
289	case *ast.ImportSpec:
290		// Try to find the package documentation for an imported package.
291		if pkgName, ok := obj.(*types.PkgName); ok {
292			imp, err := pkg.GetImport(pkgName.Imported().Path())
293			if err != nil {
294				return nil, err
295			}
296			// Assume that only one file will contain package documentation,
297			// so pick the first file that has a doc comment.
298			for _, file := range imp.GetSyntax() {
299				if file.Doc != nil {
300					info = &HoverInformation{source: obj, comment: file.Doc}
301					break
302				}
303			}
304		}
305		info = &HoverInformation{source: node}
306	case *ast.GenDecl:
307		switch obj := obj.(type) {
308		case *types.TypeName, *types.Var, *types.Const, *types.Func:
309			var err error
310			info, err = formatGenDecl(node, spec, obj, obj.Type())
311			if err != nil {
312				return nil, err
313			}
314		}
315	case *ast.TypeSpec:
316		if obj.Parent() == types.Universe {
317			if obj.Name() == "error" {
318				info = &HoverInformation{source: node}
319			} else {
320				info = &HoverInformation{source: node.Name} // comments not needed for builtins
321			}
322		}
323	case *ast.FuncDecl:
324		switch obj.(type) {
325		case *types.Func:
326			info = &HoverInformation{source: obj, comment: node.Doc}
327		case *types.Builtin:
328			info = &HoverInformation{source: node.Type, comment: node.Doc}
329		case *types.Var:
330			// Object is a function param or the field of an anonymous struct
331			// declared with ':='. Skip the first one because only fields
332			// can have docs.
333			if isFunctionParam(obj, node) {
334				break
335			}
336
337			field, err := s.PosToField(ctx, pkg, obj.Pos())
338			if err != nil {
339				return nil, err
340			}
341
342			if field != nil {
343				comment := field.Doc
344				if comment.Text() == "" {
345					comment = field.Comment
346				}
347				info = &HoverInformation{source: obj, comment: comment}
348			}
349		}
350	}
351
352	if info == nil {
353		info = &HoverInformation{source: obj}
354	}
355
356	if info.comment != nil {
357		info.FullDocumentation = info.comment.Text()
358		info.Synopsis = doc.Synopsis(info.FullDocumentation)
359	}
360
361	return info, nil
362}
363
364// isFunctionParam returns true if the passed object is either an incoming
365// or an outgoing function param
366func isFunctionParam(obj types.Object, node *ast.FuncDecl) bool {
367	for _, f := range node.Type.Params.List {
368		if f.Pos() == obj.Pos() {
369			return true
370		}
371	}
372	if node.Type.Results != nil {
373		for _, f := range node.Type.Results.List {
374			if f.Pos() == obj.Pos() {
375				return true
376			}
377		}
378	}
379	return false
380}
381
382func formatGenDecl(node *ast.GenDecl, spec ast.Spec, obj types.Object, typ types.Type) (*HoverInformation, error) {
383	if _, ok := typ.(*types.Named); ok {
384		switch typ.Underlying().(type) {
385		case *types.Interface, *types.Struct:
386			return formatGenDecl(node, spec, obj, typ.Underlying())
387		}
388	}
389	if spec == nil {
390		for _, s := range node.Specs {
391			if s.Pos() <= obj.Pos() && obj.Pos() <= s.End() {
392				spec = s
393				break
394			}
395		}
396	}
397	if spec == nil {
398		return nil, errors.Errorf("no spec for node %v at position %v", node, obj.Pos())
399	}
400
401	// If we have a field or method.
402	switch obj.(type) {
403	case *types.Var, *types.Const, *types.Func:
404		return formatVar(spec, obj, node), nil
405	}
406	// Handle types.
407	switch spec := spec.(type) {
408	case *ast.TypeSpec:
409		return formatTypeSpec(spec, node), nil
410	case *ast.ValueSpec:
411		return &HoverInformation{source: spec, comment: spec.Doc}, nil
412	case *ast.ImportSpec:
413		return &HoverInformation{source: spec, comment: spec.Doc}, nil
414	}
415	return nil, errors.Errorf("unable to format spec %v (%T)", spec, spec)
416}
417
418func formatTypeSpec(spec *ast.TypeSpec, decl *ast.GenDecl) *HoverInformation {
419	comment := spec.Doc
420	if comment == nil && decl != nil {
421		comment = decl.Doc
422	}
423	if comment == nil {
424		comment = spec.Comment
425	}
426	return &HoverInformation{
427		source:      spec.Type,
428		comment:     comment,
429		typeName:    spec.Name.Name,
430		isTypeAlias: spec.Assign.IsValid(),
431	}
432}
433
434func formatVar(node ast.Spec, obj types.Object, decl *ast.GenDecl) *HoverInformation {
435	var fieldList *ast.FieldList
436	switch spec := node.(type) {
437	case *ast.TypeSpec:
438		switch t := spec.Type.(type) {
439		case *ast.StructType:
440			fieldList = t.Fields
441		case *ast.InterfaceType:
442			fieldList = t.Methods
443		}
444	case *ast.ValueSpec:
445		// Try to extract the field list of an anonymous struct
446		if fieldList = extractFieldList(spec.Type); fieldList != nil {
447			break
448		}
449
450		comment := spec.Doc
451		if comment == nil {
452			comment = decl.Doc
453		}
454		if comment == nil {
455			comment = spec.Comment
456		}
457		return &HoverInformation{source: obj, comment: comment}
458	}
459
460	if fieldList != nil {
461		comment := findFieldComment(obj.Pos(), fieldList)
462		return &HoverInformation{source: obj, comment: comment}
463	}
464	return &HoverInformation{source: obj, comment: decl.Doc}
465}
466
467// extractFieldList recursively tries to extract a field list.
468// If it is not found, nil is returned.
469func extractFieldList(specType ast.Expr) *ast.FieldList {
470	switch t := specType.(type) {
471	case *ast.StructType:
472		return t.Fields
473	case *ast.InterfaceType:
474		return t.Methods
475	case *ast.ArrayType:
476		return extractFieldList(t.Elt)
477	case *ast.MapType:
478		// Map value has a greater chance to be a struct
479		if fields := extractFieldList(t.Value); fields != nil {
480			return fields
481		}
482		return extractFieldList(t.Key)
483	case *ast.ChanType:
484		return extractFieldList(t.Value)
485	}
486	return nil
487}
488
489// findFieldComment visits all fields in depth-first order and returns
490// the comment of a field with passed position. If no comment is found,
491// nil is returned.
492func findFieldComment(pos token.Pos, fieldList *ast.FieldList) *ast.CommentGroup {
493	for _, field := range fieldList.List {
494		if field.Pos() == pos {
495			if field.Doc.Text() != "" {
496				return field.Doc
497			}
498			return field.Comment
499		}
500
501		if nestedFieldList := extractFieldList(field.Type); nestedFieldList != nil {
502			if c := findFieldComment(pos, nestedFieldList); c != nil {
503				return c
504			}
505		}
506	}
507	return nil
508}
509
510func FormatHover(h *HoverInformation, options *Options) (string, error) {
511	signature := h.Signature
512	if signature != "" && options.PreferredContentFormat == protocol.Markdown {
513		signature = fmt.Sprintf("```go\n%s\n```", signature)
514	}
515
516	switch options.HoverKind {
517	case SingleLine:
518		return h.SingleLine, nil
519	case NoDocumentation:
520		return signature, nil
521	case Structured:
522		b, err := json.Marshal(h)
523		if err != nil {
524			return "", err
525		}
526		return string(b), nil
527	}
528	link := formatLink(h, options)
529	switch options.HoverKind {
530	case SynopsisDocumentation:
531		doc := formatDoc(h.Synopsis, options)
532		return formatHover(options, signature, link, doc), nil
533	case FullDocumentation:
534		doc := formatDoc(h.FullDocumentation, options)
535		return formatHover(options, signature, link, doc), nil
536	}
537	return "", errors.Errorf("no hover for %v", h.source)
538}
539
540func formatLink(h *HoverInformation, options *Options) string {
541	if !options.LinksInHover || options.LinkTarget == "" || h.LinkPath == "" {
542		return ""
543	}
544	plainLink := BuildLink(options.LinkTarget, h.LinkPath, h.LinkAnchor)
545	switch options.PreferredContentFormat {
546	case protocol.Markdown:
547		return fmt.Sprintf("[`%s` on %s](%s)", h.symbolName, options.LinkTarget, plainLink)
548	case protocol.PlainText:
549		return ""
550	default:
551		return plainLink
552	}
553}
554
555// BuildLink constructs a link with the given target, path, and anchor.
556func BuildLink(target, path, anchor string) string {
557	link := fmt.Sprintf("https://%s/%s", target, path)
558	if target == "pkg.go.dev" {
559		link += "?utm_source=gopls"
560	}
561	if anchor == "" {
562		return link
563	}
564	return link + "#" + anchor
565}
566
567func formatDoc(doc string, options *Options) string {
568	if options.PreferredContentFormat == protocol.Markdown {
569		return CommentToMarkdown(doc)
570	}
571	return doc
572}
573
574func formatHover(options *Options, x ...string) string {
575	var b strings.Builder
576	for i, el := range x {
577		if el != "" {
578			b.WriteString(el)
579
580			// Don't write out final newline.
581			if i == len(x) {
582				continue
583			}
584			// If any elements of the remainder of the list are non-empty,
585			// write a newline.
586			if anyNonEmpty(x[i+1:]) {
587				if options.PreferredContentFormat == protocol.Markdown {
588					b.WriteString("\n\n")
589				} else {
590					b.WriteRune('\n')
591				}
592			}
593		}
594	}
595	return b.String()
596}
597
598func anyNonEmpty(x []string) bool {
599	for _, el := range x {
600		if el != "" {
601			return true
602		}
603	}
604	return false
605}
606