github.com/jd-ly/tools@v0.5.7/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/doc"
    13  	"go/format"
    14  	"go/types"
    15  	"strings"
    16  
    17  	"github.com/jd-ly/tools/internal/event"
    18  	"github.com/jd-ly/tools/internal/lsp/protocol"
    19  	errors "golang.org/x/xerrors"
    20  )
    21  
    22  type HoverInformation struct {
    23  	// Signature is the symbol's signature.
    24  	Signature string `json:"signature"`
    25  
    26  	// SingleLine is a single line describing the symbol.
    27  	// This is recommended only for use in clients that show a single line for hover.
    28  	SingleLine string `json:"singleLine"`
    29  
    30  	// Synopsis is a single sentence synopsis of the symbol's documentation.
    31  	Synopsis string `json:"synopsis"`
    32  
    33  	// FullDocumentation is the symbol's full documentation.
    34  	FullDocumentation string `json:"fullDocumentation"`
    35  
    36  	// LinkPath is the pkg.go.dev link for the given symbol.
    37  	// For example, the "go/ast" part of "pkg.go.dev/go/ast#Node".
    38  	LinkPath string `json:"linkPath"`
    39  
    40  	// LinkAnchor is the pkg.go.dev link anchor for the given symbol.
    41  	// For example, the "Node" part of "pkg.go.dev/go/ast#Node".
    42  	LinkAnchor string `json:"linkAnchor"`
    43  
    44  	// importPath is the import path for the package containing the given
    45  	// symbol.
    46  	importPath string
    47  
    48  	// symbolName is the types.Object.Name for the given symbol.
    49  	symbolName string
    50  
    51  	source  interface{}
    52  	comment *ast.CommentGroup
    53  
    54  	// isTypeName reports whether the identifier is a type name. In such cases,
    55  	// the hover has the prefix "type ".
    56  	isType bool
    57  }
    58  
    59  func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) {
    60  	ident, err := Identifier(ctx, snapshot, fh, position)
    61  	if err != nil {
    62  		return nil, nil
    63  	}
    64  	h, err := HoverIdentifier(ctx, ident)
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  	rng, err := ident.Range()
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	// See golang/go#36998: don't link to modules matching GOPRIVATE.
    73  	if snapshot.View().IsGoPrivatePath(h.importPath) {
    74  		h.LinkPath = ""
    75  	}
    76  	hover, err := FormatHover(h, snapshot.View().Options())
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	return &protocol.Hover{
    81  		Contents: protocol.MarkupContent{
    82  			Kind:  snapshot.View().Options().PreferredContentFormat,
    83  			Value: hover,
    84  		},
    85  		Range: rng,
    86  	}, nil
    87  }
    88  
    89  func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverInformation, error) {
    90  	ctx, done := event.Start(ctx, "source.Hover")
    91  	defer done()
    92  
    93  	fset := i.Snapshot.FileSet()
    94  	h, err := HoverInfo(ctx, i.pkg, i.Declaration.obj, i.Declaration.node)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  	// Determine the symbol's signature.
    99  	switch x := h.source.(type) {
   100  	case ast.Node:
   101  		var b strings.Builder
   102  		if err := format.Node(&b, fset, x); err != nil {
   103  			return nil, err
   104  		}
   105  		h.Signature = b.String()
   106  		if h.isType {
   107  			h.Signature = "type " + h.Signature
   108  		}
   109  	case types.Object:
   110  		// If the variable is implicitly declared in a type switch, we need to
   111  		// manually generate its object string.
   112  		if typ := i.Declaration.typeSwitchImplicit; typ != nil {
   113  			if v, ok := x.(*types.Var); ok {
   114  				h.Signature = fmt.Sprintf("var %s %s", v.Name(), types.TypeString(typ, i.qf))
   115  				break
   116  			}
   117  		}
   118  		h.Signature = objectString(x, i.qf)
   119  	}
   120  	if obj := i.Declaration.obj; obj != nil {
   121  		h.SingleLine = objectString(obj, i.qf)
   122  	}
   123  	obj := i.Declaration.obj
   124  	if obj == nil {
   125  		return h, nil
   126  	}
   127  	switch obj := obj.(type) {
   128  	case *types.PkgName:
   129  		h.importPath = obj.Imported().Path()
   130  		h.LinkPath = h.importPath
   131  		h.symbolName = obj.Name()
   132  		if mod, version, ok := moduleAtVersion(h.LinkPath, i); ok {
   133  			h.LinkPath = strings.Replace(h.LinkPath, mod, mod+"@"+version, 1)
   134  		}
   135  		return h, nil
   136  	case *types.Builtin:
   137  		h.importPath = "builtin"
   138  		h.LinkPath = h.importPath
   139  		h.LinkAnchor = obj.Name()
   140  		h.symbolName = h.LinkAnchor
   141  		return h, nil
   142  	}
   143  	// Check if the identifier is test-only (and is therefore not part of a
   144  	// package's API). This is true if the request originated in a test package,
   145  	// and if the declaration is also found in the same test package.
   146  	if i.pkg != nil && obj.Pkg() != nil && i.pkg.ForTest() != "" {
   147  		if _, err := i.pkg.File(i.Declaration.MappedRange[0].URI()); err == nil {
   148  			return h, nil
   149  		}
   150  	}
   151  	// Don't return links for other unexported types.
   152  	if !obj.Exported() {
   153  		return h, nil
   154  	}
   155  	var rTypeName string
   156  	switch obj := obj.(type) {
   157  	case *types.Var:
   158  		// If the object is a field, and we have an associated selector
   159  		// composite literal, or struct, we can determine the link.
   160  		if obj.IsField() {
   161  			if named, ok := i.enclosing.(*types.Named); ok {
   162  				rTypeName = named.Obj().Name()
   163  			}
   164  		}
   165  	case *types.Func:
   166  		typ, ok := obj.Type().(*types.Signature)
   167  		if !ok {
   168  			return h, nil
   169  		}
   170  		if r := typ.Recv(); r != nil {
   171  			switch rtyp := Deref(r.Type()).(type) {
   172  			case *types.Struct:
   173  				rTypeName = r.Name()
   174  			case *types.Named:
   175  				// If we have an unexported type, see if the enclosing type is
   176  				// exported (we may have an interface or struct we can link
   177  				// to). If not, don't show any link.
   178  				if !rtyp.Obj().Exported() {
   179  					if named, ok := i.enclosing.(*types.Named); ok && named.Obj().Exported() {
   180  						rTypeName = named.Obj().Name()
   181  					} else {
   182  						return h, nil
   183  					}
   184  				} else {
   185  					rTypeName = rtyp.Obj().Name()
   186  				}
   187  			}
   188  		}
   189  	}
   190  	h.importPath = obj.Pkg().Path()
   191  	h.LinkPath = h.importPath
   192  	if mod, version, ok := moduleAtVersion(h.LinkPath, i); ok {
   193  		h.LinkPath = strings.Replace(h.LinkPath, mod, mod+"@"+version, 1)
   194  	}
   195  	if rTypeName != "" {
   196  		h.LinkAnchor = fmt.Sprintf("%s.%s", rTypeName, obj.Name())
   197  		h.symbolName = fmt.Sprintf("(%s.%s).%s", obj.Pkg().Name(), rTypeName, obj.Name())
   198  		return h, nil
   199  	}
   200  	// For most cases, the link is "package/path#symbol".
   201  	h.LinkAnchor = obj.Name()
   202  	h.symbolName = fmt.Sprintf("%s.%s", obj.Pkg().Name(), obj.Name())
   203  	return h, nil
   204  }
   205  
   206  func moduleAtVersion(path string, i *IdentifierInfo) (string, string, bool) {
   207  	if strings.ToLower(i.Snapshot.View().Options().LinkTarget) != "pkg.go.dev" {
   208  		return "", "", false
   209  	}
   210  	impPkg, err := i.pkg.GetImport(path)
   211  	if err != nil {
   212  		return "", "", false
   213  	}
   214  	if impPkg.Version() == nil {
   215  		return "", "", false
   216  	}
   217  	version, modpath := impPkg.Version().Version, impPkg.Version().Path
   218  	if modpath == "" || version == "" {
   219  		return "", "", false
   220  	}
   221  	return modpath, version, true
   222  }
   223  
   224  // objectString is a wrapper around the types.ObjectString function.
   225  // It handles adding more information to the object string.
   226  func objectString(obj types.Object, qf types.Qualifier) string {
   227  	str := types.ObjectString(obj, qf)
   228  	switch obj := obj.(type) {
   229  	case *types.Const:
   230  		str = fmt.Sprintf("%s = %s", str, obj.Val())
   231  	}
   232  	return str
   233  }
   234  
   235  // HoverInfo returns a HoverInformation struct for an ast node and its type
   236  // object.
   237  func HoverInfo(ctx context.Context, pkg Package, obj types.Object, node ast.Node) (*HoverInformation, error) {
   238  	var info *HoverInformation
   239  
   240  	switch node := node.(type) {
   241  	case *ast.Ident:
   242  		// The package declaration.
   243  		for _, f := range pkg.GetSyntax() {
   244  			if f.Name == node {
   245  				info = &HoverInformation{comment: f.Doc}
   246  			}
   247  		}
   248  	case *ast.ImportSpec:
   249  		// Try to find the package documentation for an imported package.
   250  		if pkgName, ok := obj.(*types.PkgName); ok {
   251  			imp, err := pkg.GetImport(pkgName.Imported().Path())
   252  			if err != nil {
   253  				return nil, err
   254  			}
   255  			// Assume that only one file will contain package documentation,
   256  			// so pick the first file that has a doc comment.
   257  			for _, file := range imp.GetSyntax() {
   258  				if file.Doc != nil {
   259  					info = &HoverInformation{source: obj, comment: file.Doc}
   260  					break
   261  				}
   262  			}
   263  		}
   264  		info = &HoverInformation{source: node}
   265  	case *ast.GenDecl:
   266  		switch obj := obj.(type) {
   267  		case *types.TypeName, *types.Var, *types.Const, *types.Func:
   268  			var err error
   269  			info, err = formatGenDecl(node, obj, obj.Type())
   270  			if err != nil {
   271  				return nil, err
   272  			}
   273  			_, info.isType = obj.(*types.TypeName)
   274  		}
   275  	case *ast.TypeSpec:
   276  		if obj.Parent() == types.Universe {
   277  			if obj.Name() == "error" {
   278  				info = &HoverInformation{source: node}
   279  			} else {
   280  				info = &HoverInformation{source: node.Name} // comments not needed for builtins
   281  			}
   282  		}
   283  	case *ast.FuncDecl:
   284  		switch obj.(type) {
   285  		case *types.Func:
   286  			info = &HoverInformation{source: obj, comment: node.Doc}
   287  		case *types.Builtin:
   288  			info = &HoverInformation{source: node.Type, comment: node.Doc}
   289  		}
   290  	}
   291  
   292  	if info == nil {
   293  		info = &HoverInformation{source: obj}
   294  	}
   295  
   296  	if info.comment != nil {
   297  		info.FullDocumentation = info.comment.Text()
   298  		info.Synopsis = doc.Synopsis(info.FullDocumentation)
   299  	}
   300  
   301  	return info, nil
   302  }
   303  
   304  func formatGenDecl(node *ast.GenDecl, obj types.Object, typ types.Type) (*HoverInformation, error) {
   305  	if _, ok := typ.(*types.Named); ok {
   306  		switch typ.Underlying().(type) {
   307  		case *types.Interface, *types.Struct:
   308  			return formatGenDecl(node, obj, typ.Underlying())
   309  		}
   310  	}
   311  	var spec ast.Spec
   312  	for _, s := range node.Specs {
   313  		if s.Pos() <= obj.Pos() && obj.Pos() <= s.End() {
   314  			spec = s
   315  			break
   316  		}
   317  	}
   318  	if spec == nil {
   319  		return nil, errors.Errorf("no spec for node %v at position %v", node, obj.Pos())
   320  	}
   321  
   322  	// If we have a field or method.
   323  	switch obj.(type) {
   324  	case *types.Var, *types.Const, *types.Func:
   325  		return formatVar(spec, obj, node), nil
   326  	}
   327  	// Handle types.
   328  	switch spec := spec.(type) {
   329  	case *ast.TypeSpec:
   330  		if len(node.Specs) > 1 {
   331  			// If multiple types are declared in the same block.
   332  			return &HoverInformation{source: spec.Type, comment: spec.Doc}, nil
   333  		} else {
   334  			return &HoverInformation{source: spec, comment: node.Doc}, nil
   335  		}
   336  	case *ast.ValueSpec:
   337  		return &HoverInformation{source: spec, comment: spec.Doc}, nil
   338  	case *ast.ImportSpec:
   339  		return &HoverInformation{source: spec, comment: spec.Doc}, nil
   340  	}
   341  	return nil, errors.Errorf("unable to format spec %v (%T)", spec, spec)
   342  }
   343  
   344  func formatVar(node ast.Spec, obj types.Object, decl *ast.GenDecl) *HoverInformation {
   345  	var fieldList *ast.FieldList
   346  	switch spec := node.(type) {
   347  	case *ast.TypeSpec:
   348  		switch t := spec.Type.(type) {
   349  		case *ast.StructType:
   350  			fieldList = t.Fields
   351  		case *ast.InterfaceType:
   352  			fieldList = t.Methods
   353  		}
   354  	case *ast.ValueSpec:
   355  		comment := spec.Doc
   356  		if comment == nil {
   357  			comment = decl.Doc
   358  		}
   359  		if comment == nil {
   360  			comment = spec.Comment
   361  		}
   362  		return &HoverInformation{source: obj, comment: comment}
   363  	}
   364  	// If we have a struct or interface declaration,
   365  	// we need to match the object to the corresponding field or method.
   366  	if fieldList != nil {
   367  		for i := 0; i < len(fieldList.List); i++ {
   368  			field := fieldList.List[i]
   369  			if field.Pos() <= obj.Pos() && obj.Pos() <= field.End() {
   370  				if field.Doc.Text() != "" {
   371  					return &HoverInformation{source: obj, comment: field.Doc}
   372  				}
   373  				return &HoverInformation{source: obj, comment: field.Comment}
   374  			}
   375  		}
   376  	}
   377  	return &HoverInformation{source: obj, comment: decl.Doc}
   378  }
   379  
   380  func FormatHover(h *HoverInformation, options *Options) (string, error) {
   381  	signature := h.Signature
   382  	if signature != "" && options.PreferredContentFormat == protocol.Markdown {
   383  		signature = fmt.Sprintf("```go\n%s\n```", signature)
   384  	}
   385  
   386  	switch options.HoverKind {
   387  	case SingleLine:
   388  		return h.SingleLine, nil
   389  	case NoDocumentation:
   390  		return signature, nil
   391  	case Structured:
   392  		b, err := json.Marshal(h)
   393  		if err != nil {
   394  			return "", err
   395  		}
   396  		return string(b), nil
   397  	}
   398  	link := formatLink(h, options)
   399  	switch options.HoverKind {
   400  	case SynopsisDocumentation:
   401  		doc := formatDoc(h.Synopsis, options)
   402  		return formatHover(options, signature, link, doc), nil
   403  	case FullDocumentation:
   404  		doc := formatDoc(h.FullDocumentation, options)
   405  		return formatHover(options, signature, link, doc), nil
   406  	}
   407  	return "", errors.Errorf("no hover for %v", h.source)
   408  }
   409  
   410  func formatLink(h *HoverInformation, options *Options) string {
   411  	if !options.LinksInHover || options.LinkTarget == "" || h.LinkPath == "" {
   412  		return ""
   413  	}
   414  	plainLink := BuildLink(options.LinkTarget, h.LinkPath, h.LinkAnchor)
   415  	switch options.PreferredContentFormat {
   416  	case protocol.Markdown:
   417  		return fmt.Sprintf("[`%s` on %s](%s)", h.symbolName, options.LinkTarget, plainLink)
   418  	case protocol.PlainText:
   419  		return ""
   420  	default:
   421  		return plainLink
   422  	}
   423  }
   424  
   425  // BuildLink constructs a link with the given target, path, and anchor.
   426  func BuildLink(target, path, anchor string) string {
   427  	link := fmt.Sprintf("https://%s/%s", target, path)
   428  	if target == "pkg.go.dev" {
   429  		link += "?utm_source=gopls"
   430  	}
   431  	if anchor == "" {
   432  		return link
   433  	}
   434  	return link + "#" + anchor
   435  }
   436  
   437  func formatDoc(doc string, options *Options) string {
   438  	if options.PreferredContentFormat == protocol.Markdown {
   439  		return CommentToMarkdown(doc)
   440  	}
   441  	return doc
   442  }
   443  
   444  func formatHover(options *Options, x ...string) string {
   445  	var b strings.Builder
   446  	for i, el := range x {
   447  		if el != "" {
   448  			b.WriteString(el)
   449  
   450  			// Don't write out final newline.
   451  			if i == len(x) {
   452  				continue
   453  			}
   454  			// If any elements of the remainder of the list are non-empty,
   455  			// write a newline.
   456  			if anyNonEmpty(x[i+1:]) {
   457  				if options.PreferredContentFormat == protocol.Markdown {
   458  					b.WriteString("\n\n")
   459  				} else {
   460  					b.WriteRune('\n')
   461  				}
   462  			}
   463  		}
   464  	}
   465  	return b.String()
   466  }
   467  
   468  func anyNonEmpty(x []string) bool {
   469  	for _, el := range x {
   470  		if el != "" {
   471  			return true
   472  		}
   473  	}
   474  	return false
   475  }