blob: 260bd0f0bcc089c2a65cbf6e853ccdc2a134501e [file] [log] [blame]
// Copyright 2018 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 golang
import (
"context"
"fmt"
"go/ast"
"go/token"
"go/types"
"strings"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/typesinternal"
)
// SignatureHelp returns information about the signature of the innermost
// function call enclosing the position, or nil if there is none.
func SignatureHelp(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, params *protocol.SignatureHelpParams) (*protocol.SignatureInformation, error) {
ctx, done := event.Start(ctx, "golang.SignatureHelp")
defer done()
// We need full type-checking here, as we must type-check function bodies in
// order to provide signature help at the requested position.
pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
if err != nil {
return nil, fmt.Errorf("getting file for SignatureHelp: %w", err)
}
pos, err := pgf.PositionPos(params.Position)
if err != nil {
return nil, err
}
// Find a call expression surrounding the query position.
var callExpr *ast.CallExpr
path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos)
if path == nil {
return nil, fmt.Errorf("cannot find node enclosing position")
}
info := pkg.TypesInfo()
var fnval ast.Expr
loop:
for i, node := range path {
switch node := node.(type) {
case *ast.Ident:
// If the selected text is a function/method Ident or SelectorExpr,
// even one not in function call position,
// show help for its signature. Example:
// once.Do(initialize⁁)
// should show help for initialize, not once.Do.
if t := info.TypeOf(node); t != nil &&
info.Defs[node] == nil &&
is[*types.Signature](t.Underlying()) {
if sel, ok := path[i+1].(*ast.SelectorExpr); ok && sel.Sel == node {
fnval = sel // e.g. fmt.Println⁁
} else {
fnval = node
}
break loop
}
case *ast.CallExpr:
// Beware: the ')' may be missing.
if node.Lparen <= pos && pos <= node.Rparen {
callExpr = node
fnval = callExpr.Fun
break loop
}
case *ast.FuncLit, *ast.FuncType, *ast.CompositeLit:
// The user is within an anonymous function or
// a composite literal, which may be the argument
// to the *ast.CallExpr.
// Don't show signature help in this case.
return nil, nil
case *ast.BasicLit:
// golang/go#43397: don't offer signature help when the user is typing
// in a string literal unless it was manually invoked or help is already active.
if node.Kind == token.STRING &&
(params.Context == nil || (params.Context.TriggerKind != protocol.SigInvoked && !params.Context.IsRetrigger)) {
return nil, nil
}
}
}
if fnval == nil {
return nil, nil
}
// Get the type information for the function being called.
var sig *types.Signature
if tv, ok := info.Types[fnval]; !ok {
return nil, fmt.Errorf("cannot get type for Fun %[1]T (%[1]v)", fnval)
} else if tv.IsType() {
return nil, nil // a conversion, not a call
} else if sig, ok = tv.Type.Underlying().(*types.Signature); !ok {
return nil, fmt.Errorf("call operand is not a func or type: %[1]T (%[1]v)", fnval)
}
// Inv: sig != nil
// Get the object representing the function, if available.
// There is no object in certain cases such as calling a function returned by
// a function (e.g. "foo()()").
var obj types.Object
switch t := fnval.(type) {
case *ast.Ident:
obj = info.ObjectOf(t)
case *ast.SelectorExpr:
obj = info.ObjectOf(t.Sel)
}
if obj != nil && isBuiltin(obj) {
// Special handling for error.Error, which is the only builtin method.
if obj.Name() == "Error" {
return &protocol.SignatureInformation{
Label: "Error() string",
// TODO(skewb1k): move the docstring for error.Error to builtin.go and reuse it across all relevant LSP methods.
Documentation: stringToSigInfoDocumentation("Error returns the error message.", snapshot.Options()),
Parameters: nil,
ActiveParameter: nil,
}, nil
}
s, err := NewBuiltinSignature(ctx, snapshot, obj.Name())
if err != nil {
return nil, err
}
return signatureInformation(s, snapshot.Options(), pos, callExpr)
}
mq := MetadataQualifierForFile(snapshot, pgf.File, pkg.Metadata())
qual := typesinternal.FileQualifier(pgf.File, pkg.Types())
var (
comment *ast.CommentGroup
name string
)
if obj != nil {
comment, err = HoverDocForObject(ctx, snapshot, pkg.FileSet(), obj)
if err != nil {
return nil, err
}
name = obj.Name()
} else {
name = "func"
}
s, err := NewSignature(ctx, snapshot, pkg, sig, comment, qual, mq)
if err != nil {
return nil, err
}
s.name = name
return signatureInformation(s, snapshot.Options(), pos, callExpr)
}
func signatureInformation(sig *signature, options *settings.Options, pos token.Pos, call *ast.CallExpr) (*protocol.SignatureInformation, error) {
paramInfo := make([]protocol.ParameterInformation, 0, len(sig.params))
for _, p := range sig.params {
paramInfo = append(paramInfo, protocol.ParameterInformation{Label: p})
}
return &protocol.SignatureInformation{
Label: sig.name + sig.Format(),
Documentation: stringToSigInfoDocumentation(sig.doc, options),
Parameters: paramInfo,
ActiveParameter: activeParameter(sig, pos, call),
}, nil
}
// activeParameter returns a pointer to a variable containing
// the index of the active parameter (if known), or nil otherwise.
func activeParameter(sig *signature, pos token.Pos, call *ast.CallExpr) *uint32 {
if call == nil {
return nil
}
numParams := uint32(len(sig.params))
if numParams == 0 {
return nil
}
// Check if the position is even in the range of the arguments.
if !(call.Lparen < pos && pos <= call.Rparen) {
return nil
}
var activeParam uint32
for _, arg := range call.Args {
if pos <= arg.End() {
break
}
// Don't advance the active parameter for the last parameter of a variadic function.
if !sig.variadic || activeParam < numParams-1 {
activeParam++
}
}
return &activeParam
}
func stringToSigInfoDocumentation(s string, options *settings.Options) *protocol.Or_SignatureInformation_documentation {
v := s
k := protocol.PlainText
if options.PreferredContentFormat == protocol.Markdown {
v = DocCommentToMarkdown(s, options)
// whether or not content is newline terminated may not matter for LSP clients,
// but our tests expect trailing newlines to be stripped.
v = strings.TrimSuffix(v, "\n") // TODO(pjw): change the golden files
k = protocol.Markdown
}
return &protocol.Or_SignatureInformation_documentation{
Value: protocol.MarkupContent{
Kind: k,
Value: v,
},
}
}