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  }