Skip to content

Commit

Permalink
gopls/internal/lsp: hover over linkname directives.
Browse files Browse the repository at this point in the history
Enables hover on the second argument in go:linkname directives.

Make findLinkname a bit more generic so it can support
both (goto) definition and hover.

Updates golang/go#57312

Change-Id: I9ac7cfceb62dada69bf7fced1ac03877947bb615
Reviewed-on: https://go-review.googlesource.com/c/tools/+/479695
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Robert Findley <rfindley@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
  • Loading branch information
vikblom authored and findleyr committed May 1, 2023
1 parent ec61ad3 commit 7590fe4
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 29 deletions.
28 changes: 25 additions & 3 deletions gopls/internal/lsp/source/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,16 +121,38 @@ func hover(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Po
}
}

// Handle linkname directive by overriding what to look for.
var linkedRange *protocol.Range // range referenced by linkname directive, or nil
if pkgPath, name, offset := parseLinkname(ctx, snapshot, fh, pp); pkgPath != "" && name != "" {
// rng covering 2nd linkname argument: pkgPath.name.
rng, err := pgf.PosRange(pgf.Tok.Pos(offset), pgf.Tok.Pos(offset+len(pkgPath)+len(".")+len(name)))
if err != nil {
return protocol.Range{}, nil, fmt.Errorf("range over linkname arg: %w", err)
}
linkedRange = &rng

pkg, pgf, pos, err = findLinkname(ctx, snapshot, PackagePath(pkgPath), name)
if err != nil {
return protocol.Range{}, nil, fmt.Errorf("find linkname: %w", err)
}
}

// The general case: compute hover information for the object referenced by
// the identifier at pos.
ident, obj, selectedType := referencedObject(pkg, pgf, pos)
if obj == nil || ident == nil {
return protocol.Range{}, nil, nil // no object to hover
}

rng, err := pgf.NodeRange(ident)
if err != nil {
return protocol.Range{}, nil, err
// Unless otherwise specified, rng covers the ident being hovered.
var rng protocol.Range
if linkedRange != nil {
rng = *linkedRange
} else {
rng, err = pgf.NodeRange(ident)
if err != nil {
return protocol.Range{}, nil, err
}
}

// By convention, we qualify hover information relative to the package
Expand Down
71 changes: 45 additions & 26 deletions gopls/internal/lsp/source/linkname.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,54 +23,68 @@ var ErrNoLinkname = errors.New("no linkname directive found")

// LinknameDefinition finds the definition of the linkname directive in fh at pos.
// If there is no linkname directive at pos, returns ErrNoLinkname.
func LinknameDefinition(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.Location, error) {
pkgPath, name := parseLinkname(ctx, snapshot, fh, pos)
func LinknameDefinition(ctx context.Context, snapshot Snapshot, fh FileHandle, from protocol.Position) ([]protocol.Location, error) {
pkgPath, name, _ := parseLinkname(ctx, snapshot, fh, from)
if pkgPath == "" {
return nil, ErrNoLinkname
}
return findLinkname(ctx, snapshot, fh, pos, PackagePath(pkgPath), name)

_, pgf, pos, err := findLinkname(ctx, snapshot, PackagePath(pkgPath), name)
if err != nil {
return nil, fmt.Errorf("find linkname: %w", err)
}
loc, err := pgf.PosLocation(pos, pos+token.Pos(len(name)))
if err != nil {
return nil, fmt.Errorf("location of linkname: %w", err)
}
return []protocol.Location{loc}, nil
}

// parseLinkname attempts to parse a go:linkname declaration at the given pos.
// If successful, it returns the package path and object name referenced by the second
// argument of the linkname directive.
// If successful, it returns
// - package path referenced
// - object name referenced
// - byte offset in fh of the start of the link target
// of the linkname directives 2nd argument.
//
// If the position is not in the second argument of a go:linkname directive, or parsing fails, it returns "", "".
func parseLinkname(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) (pkgPath, name string) {
// If the position is not in the second argument of a go:linkname directive,
// or parsing fails, it returns "", "", 0.
func parseLinkname(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) (pkgPath, name string, targetOffset int) {
// TODO(adonovan): opt: parsing isn't necessary here.
// We're only looking for a line comment.
pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
if err != nil {
return "", ""
return "", "", 0
}

offset, err := pgf.Mapper.PositionOffset(pos)
if err != nil {
return "", ""
return "", "", 0
}

// Looking for pkgpath in '//go:linkname f pkgpath.g'.
// (We ignore 1-arg linkname directives.)
directive, end := findLinknameAtOffset(pgf, offset)
parts := strings.Fields(directive)
if len(parts) != 3 {
return "", ""
return "", "", 0
}

// Inside 2nd arg [start, end]?
// (Assumes no trailing spaces.)
start := end - len(parts[2])
if !(start <= offset && offset <= end) {
return "", ""
return "", "", 0
}
linkname := parts[2]

// Split the pkg path from the name.
dot := strings.LastIndexByte(linkname, '.')
if dot < 0 {
return "", ""
return "", "", 0
}
return linkname[:dot], linkname[dot+1:]

return linkname[:dot], linkname[dot+1:], start
}

// findLinknameAtOffset returns the first linkname directive on line and its end offset.
Expand All @@ -80,9 +94,17 @@ func findLinknameAtOffset(pgf *ParsedGoFile, offset int) (string, int) {
for _, com := range grp.List {
if strings.HasPrefix(com.Text, "//go:linkname") {
p := safetoken.Position(pgf.Tok, com.Pos())
end := p.Offset + len(com.Text)

// Sometimes source code (typically tests) has another
// comment after the directive, trim that away.
text := com.Text
if i := strings.LastIndex(text, "//"); i != 0 {
text = strings.TrimSpace(text[:i])
}

end := p.Offset + len(text)
if p.Offset <= offset && offset < end {
return com.Text, end
return text, end
}
}
}
Expand All @@ -92,14 +114,14 @@ func findLinknameAtOffset(pgf *ParsedGoFile, offset int) (string, int) {

// findLinkname searches dependencies of packages containing fh for an object
// with linker name matching the given package path and name.
func findLinkname(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position, pkgPath PackagePath, name string) ([]protocol.Location, error) {
func findLinkname(ctx context.Context, snapshot Snapshot, pkgPath PackagePath, name string) (Package, *ParsedGoFile, token.Pos, error) {
// Typically the linkname refers to a forward dependency
// or a reverse dependency, but in general it may refer
// to any package in the workspace.
var pkgMeta *Metadata
metas, err := snapshot.AllMetadata(ctx)
if err != nil {
return nil, err
return nil, nil, token.NoPos, err
}
RemoveIntermediateTestVariants(&metas)
for _, meta := range metas {
Expand All @@ -109,29 +131,26 @@ func findLinkname(ctx context.Context, snapshot Snapshot, fh FileHandle, pos pro
}
}
if pkgMeta == nil {
return nil, fmt.Errorf("cannot find package %q", pkgPath)
return nil, nil, token.NoPos, fmt.Errorf("cannot find package %q", pkgPath)
}

// When found, type check the desired package (snapshot.TypeCheck in TypecheckFull mode),
pkgs, err := snapshot.TypeCheck(ctx, pkgMeta.ID)
if err != nil {
return nil, err
return nil, nil, token.NoPos, err
}
pkg := pkgs[0]

obj := pkg.GetTypes().Scope().Lookup(name)
if obj == nil {
return nil, fmt.Errorf("package %q does not define %s", pkgPath, name)
return nil, nil, token.NoPos, fmt.Errorf("package %q does not define %s", pkgPath, name)
}

objURI := safetoken.StartPosition(pkg.FileSet(), obj.Pos())
pgf, err := pkg.File(span.URIFromPath(objURI.Filename))
if err != nil {
return nil, err
return nil, nil, token.NoPos, err
}
loc, err := pgf.PosLocation(obj.Pos(), obj.Pos()+token.Pos(len(name)))
if err != nil {
return nil, err
}
return []protocol.Location{loc}, nil

return pkg, pgf, obj.Pos(), nil
}
29 changes: 29 additions & 0 deletions gopls/internal/regtest/marker/testdata/hover/linkname.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
This test check hover on the 2nd argument in go:linkname directives.
-- go.mod --
module mod.com

-- upper/upper.go --
package upper

import (
_ "unsafe"
_ "mod.com/lower"
)

//go:linkname foo mod.com/lower.bar //@hover("mod.com/lower.bar", "mod.com/lower.bar", bar)
func foo() string

-- lower/lower.go --
package lower

// bar does foo.
func bar() string {
return "foo by bar"
}

-- @bar/hover.md --
```go
func bar() string
```

bar does foo.
39 changes: 39 additions & 0 deletions gopls/internal/regtest/misc/hover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,3 +382,42 @@ var C int
})
}
}

const linknameHover = `
-- go.mod --
module mod.com
-- upper/upper.go --
package upper
import (
_ "unsafe"
_ "mod.com/lower"
)
//go:linkname foo mod.com/lower.bar
func foo() string
-- lower/lower.go --
package lower
// bar does foo.
func bar() string {
return "foo by bar"
}`

func TestHoverLinknameDirective(t *testing.T) {
Run(t, linknameHover, func(t *testing.T, env *Env) {
// Jump from directives 2nd arg.
env.OpenFile("upper/upper.go")
from := env.RegexpSearch("upper/upper.go", `lower.bar`)

hover, _ := env.Hover(from)
content := hover.Value

expect := "bar does foo"
if !strings.Contains(content, expect) {
t.Errorf("hover: %q does not contain: %q", content, expect)
}
})
}

0 comments on commit 7590fe4

Please sign in to comment.