Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions gopls/internal/golang/completion/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,22 @@ func (c *completer) setSurrounding(ident *ast.Ident) {
return
}

// In invalid code, parser error recovery can produce *ast.Ident nodes
// whose Name spans more source text than the actual identifier token
// (e.g. by merging adjacent tokens during recovery). Using such a node's
// range as the completion replacement range would silently delete valid
// surrounding code. Guard against this by verifying that the source bytes
// at the ident's position exactly match its Name.
// See golang/go#77481.
startOff, err := safetoken.Offset(c.pgf.Tok, ident.Pos())
if err != nil {
return
}
endOff := startOff + len(ident.Name)
if endOff > len(c.pgf.Src) || string(c.pgf.Src[startOff:endOff]) != ident.Name {
return
}

c.surrounding = &Selection{
content: ident.Name,
cursor: c.pos,
Expand Down
95 changes: 95 additions & 0 deletions gopls/internal/golang/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"golang.org/x/tools/gopls/internal/util/cursorutil"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/gopls/internal/util/tokeninternal"
"golang.org/x/tools/gopls/internal/util/typesutil"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/stdlib"
Expand Down Expand Up @@ -321,6 +322,35 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng pr
return hoverReturnStatement(pgf, cur)
case *ast.Ident:
// fall through to rest of function
case *ast.FuncType:
// When hovering over the "func" keyword of a func literal that is
// implicitly converted to a named function type, show the named
// type's documentation (e.g. hovering over "func" in a call like
// filepath.WalkDir(dir, func(path string, ...) error { ... }) shows
// the docs for fs.WalkDirFunc).
//
// Note: FindByPos at a FuncLit's "func" token returns the inner
// *ast.FuncType node (not *ast.FuncLit), so we check the parent.
if inToken(node.Func, "func", posRange.Pos(), posRange.End()) {
if lit, ok := cur.Parent().Node().(*ast.FuncLit); ok {
if rng, res, err := hoverFuncLit(ctx, snapshot, pkg, pgf, cur.Parent(), lit); res != nil || err != nil {
return rng, res, err
}
}
}
// No named type found; fall through to the generic ast.Expr behavior.
if _, ok := pkg.TypesInfo().Types[node]; !ok {
return protocol.Range{}, nil, nil
}
exprResult := &hoverResult{
Synopsis: goastutil.NodeDescription(node),
FullDocumentation: types.TypeString(pkg.TypesInfo().TypeOf(node), qual),
}
exprHighlight, err := pgf.NodeRange(node)
if err != nil {
return protocol.Range{}, nil, err
}
return exprHighlight, exprResult, nil
case ast.Expr:
tv, ok := pkg.TypesInfo().Types[node]
if !ok {
Expand Down Expand Up @@ -1142,6 +1172,71 @@ func hoverConstantExpr(pgf *parsego.File, expr ast.Expr, tv types.TypeAndValue,
}, nil
}

// hoverFuncLit handles hover over the "func" keyword of a func literal that
// is implicitly converted to a named function type (e.g. fs.WalkDirFunc).
// It returns a non-nil result if a named type is found; otherwise it returns
// nil, nil, nil to signal that the caller should fall through to default hover.
func hoverFuncLit(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, cur inspector.Cursor, lit *ast.FuncLit) (protocol.Range, *hoverResult, error) {
// Use TypesFromContext to find the type the func literal must satisfy.
var named *types.Named
for _, t := range typesutil.TypesFromContext(pkg.TypesInfo(), cur) {
n, ok := types.Unalias(t).(*types.Named)
if !ok {
continue
}
if _, ok := n.Underlying().(*types.Signature); !ok {
continue // not a named function type
}
named = n
break
}
if named == nil {
return protocol.Range{}, nil, nil
}

obj := named.Obj()

// Load the declaring package to get documentation and signature.
declPkg, declPGF, declPos, err := NarrowestDeclaringPackage(ctx, snapshot, pkg, obj)
if err != nil {
return protocol.Range{}, nil, err
}

decl, spec, _ := findDeclInfo([]*ast.File{declPGF.File}, declPos)
var docText string
if docComment := chooseDocComment(decl, spec, nil); docComment != nil {
docText = docComment.Text()
}

qual := typesinternal.FileQualifier(pgf.File, pkg.Types())
signature := objectString(obj, qual, declPos, declPGF.Tok, spec)

var linkPath, anchor string
if obj.Exported() && typesinternal.IsPackageLevel(obj) && !snapshot.IsGoPrivatePath(obj.Pkg().Path()) {
linkPath = obj.Pkg().Path()
anchor = obj.Name()
if declPkg.Metadata().Module != nil && declPkg.Metadata().Module.Version != "" {
mod := declPkg.Metadata().Module
linkPath = strings.Replace(linkPath, mod.Path, cache.ResolvedString(mod), 1)
}
}

rng, err := pgf.NodeRange(lit.Type)
if err != nil {
return protocol.Range{}, nil, err
}

return rng, &hoverResult{
Synopsis: doc.Synopsis(docText),
FullDocumentation: docText,
Signature: signature,
SingleLine: signature,
SymbolName: fmt.Sprintf("%s.%s", obj.Pkg().Name(), obj.Name()),
LinkPath: linkPath,
LinkAnchor: anchor,
}, nil
}

func hoverReturnStatement(pgf *parsego.File, curReturn inspector.Cursor) (protocol.Range, *hoverResult, error) {
var funcType *ast.FuncType
// Find innermost enclosing function.
Expand Down
14 changes: 14 additions & 0 deletions gopls/internal/test/marker/testdata/completion/bad.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,17 @@ func random3(y ...int) { //@item(random3, "random3", "func(y ...int)", "func"),i
var fn2 func(badParam) //@item(fn2, "fn2", "func(badParam)", "var"),diag("fn2", re"declared (and|but) not used"),diag("badParam", re"(undeclared name|undefined): badParam")
//@complete("", arr, ch, fn1, fn2, m, y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff)
}

-- bad77481/bad77481.go --
package bad77481

// Regression test for golang/go#77481: completing an identifier inside a struct
// with invalid syntax must not delete surrounding valid struct fields.
// The parser may produce malformed AST nodes during error recovery; gopls must
// validate the ident's source range before using it as a completion replacement
// range. Verify that completion at the end of an incomplete field type does not
// crash.
type Theme struct {
Color colore //@diag("colore", re"(undeclared name|undefined): colore"),complete(re"colore()")
MenuHeight float32
}
50 changes: 50 additions & 0 deletions gopls/internal/test/marker/testdata/hover/funclittype.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
This test checks that hovering over the "func" keyword of a func literal
that is implicitly converted to a named function type shows the named
type's documentation.

-- go.mod --
module example.com

go 1.18

-- a/a.go --
package a

// WalkFunc is the type of the function called by Walk to visit each
// file or directory.
type WalkFunc func(path string, err error) error

// Walk calls fn for each file.
func Walk(root string, fn WalkFunc) {}

-- b/b.go --
package b

import "example.com/a"

func _() {
// Hovering over "func" shows WalkFunc's documentation.
a.Walk(".", func(path string, err error) error { //@hover("func", "func(path string, err error) error", funclittype)
return nil
})

// Hovering over "func" when no named type — falls back to showing type.
f := func(x int) int { return x } //@hover("func", "func(x int) int", funclitdefault)
_ = f
}

-- @funclittype --
```go
type a.WalkFunc func(path string, err error) error
```

---

WalkFunc is the type of the function called by Walk to visit each file or directory.


---

[`a.WalkFunc` on pkg.go.dev](https://pkg.go.dev/example.com/a#WalkFunc)
-- @funclitdefault --
func(x int) int