From 82a1feac904416727ad5e396e7ab6c8f766276a4 Mon Sep 17 00:00:00 2001 From: abhay1999 Date: Wed, 8 Apr 2026 20:14:55 +0530 Subject: [PATCH 1/2] gopls/hover: show named func type doc when hovering over func literal When a func literal is implicitly converted to a named function type (e.g. fs.WalkDirFunc), hovering over its "func" keyword now shows the documentation and signature of that named type, with a link to pkg.go.dev. Previously, hovering over "func" in: filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { ... }) showed only the anonymous signature "func(path string, d fs.DirEntry, err error) error". Now it shows the docs for fs.WalkDirFunc, which is often the information the user actually wants. Implementation: - Detect the case in the hover switch: when cur.Node() is *ast.FuncType and the cursor is at the "func" token and the parent is a *ast.FuncLit. - Use typesutil.TypesFromContext to find the named function type the literal must satisfy. - Load the declaring package and build a hoverResult with the named type's documentation, signature, and pkg.go.dev link. - If no named type is found, fall back to the existing anonymous-type hover behavior. Add a marker test in hover/funclittype.txt covering both cases. Fixes golang/go#76191 --- gopls/internal/golang/hover.go | 95 +++++++++++++++++++ .../marker/testdata/hover/funclittype.txt | 50 ++++++++++ 2 files changed, 145 insertions(+) create mode 100644 gopls/internal/test/marker/testdata/hover/funclittype.txt diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index 65618fc3d20..8db69c0108c 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -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" @@ -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 { @@ -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. diff --git a/gopls/internal/test/marker/testdata/hover/funclittype.txt b/gopls/internal/test/marker/testdata/hover/funclittype.txt new file mode 100644 index 00000000000..6f78b48d5e0 --- /dev/null +++ b/gopls/internal/test/marker/testdata/hover/funclittype.txt @@ -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 From cb9d241c63954f1dcd85bb0944c8ac8cf2548d06 Mon Sep 17 00:00:00 2001 From: abhay1999 Date: Mon, 13 Apr 2026 11:26:37 +0530 Subject: [PATCH 2/2] gopls/completion: validate ident source range before use as replacement In invalid Go code, the parser's error recovery can produce *ast.Ident nodes whose Name spans more source text than the actual identifier token. When setSurrounding uses such a node's Pos/End as the completion replacement range, the resulting text edit silently deletes valid surrounding code. Guard against this by verifying, in setSurrounding, that the source bytes at ident.Pos() exactly match ident.Name. If they don't, the AST node is malformed and using it as a replacement range would cause data loss; return early so that getSurrounding falls back to a zero-width insertion at the cursor. Fixes golang/go#77481 Signed-off-by: abhay1999 --- gopls/internal/golang/completion/completion.go | 16 ++++++++++++++++ .../test/marker/testdata/completion/bad.txt | 14 ++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go index 314165a7c7b..4db06ddc3a3 100644 --- a/gopls/internal/golang/completion/completion.go +++ b/gopls/internal/golang/completion/completion.go @@ -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, diff --git a/gopls/internal/test/marker/testdata/completion/bad.txt b/gopls/internal/test/marker/testdata/completion/bad.txt index 44630162be7..b971a922932 100644 --- a/gopls/internal/test/marker/testdata/completion/bad.txt +++ b/gopls/internal/test/marker/testdata/completion/bad.txt @@ -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 +}