gopls/internal/golang: Definition: jump to assembly

This CL adds support for jumping to the definition of a function
implemented in assembly. The first Definition query jumps to the
Go declaration, as usual; this func has no body. Executing a
second Definition query jumps to the assembly implementation,
if found.

+ Test, doc, relnote

Fixes golang/go#60531

Change-Id: I943a05d4a2a5b6a398450131831f49cc7c0754e4
Reviewed-on: https://go-review.googlesource.com/c/tools/+/612537
Reviewed-by: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Alan Donovan 2024-09-12 22:55:36 -04:00
Родитель 8fcd92f832
Коммит 5aac53c5ff
6 изменённых файлов: 119 добавлений и 12 удалений

Просмотреть файл

@ -22,6 +22,8 @@ A definition query also works in these unexpected places:
(like [`hover`](passive.md#hover)) the location of the linked symbol.
- On a file name in a **[`go:embed` directive](https://pkg.go.dev/embed)**,
it returns the location of the embedded file.
- On the declaration of a non-Go function (a `func` with no body),
it returns the location of the assembly implementation, if any,
<!-- On a built-in symbol such as `append` or `unsafe.Pointer`, `definition` reports
the location of the declaration in the builtin or unsafe pseudo-packages,

Просмотреть файл

@ -46,3 +46,11 @@ Editors may use this for syntax coloring.
Now, function signature help can be used on any identifier with a function
signature, not just within the parentheses of a function being called.
## Jump to assembly definition
A Definition query on a reference to a function jumps to the
function's Go `func` declaration. If the function is implemented in C
or assembly, the function has no body. Executing a second Definition
query (while already at the Go declaration) will navigate you to the
assembly implementation.

19
gopls/internal/cache/load.go поставляемый
Просмотреть файл

@ -412,18 +412,15 @@ func buildMetadata(updates map[PackageID]*metadata.Package, pkg *packages.Packag
updates[id] = mp
for _, filename := range pkg.CompiledGoFiles {
uri := protocol.URIFromPath(filename)
mp.CompiledGoFiles = append(mp.CompiledGoFiles, uri)
}
for _, filename := range pkg.GoFiles {
uri := protocol.URIFromPath(filename)
mp.GoFiles = append(mp.GoFiles, uri)
}
for _, filename := range pkg.IgnoredFiles {
uri := protocol.URIFromPath(filename)
mp.IgnoredFiles = append(mp.IgnoredFiles, uri)
copyURIs := func(dst *[]protocol.DocumentURI, src []string) {
for _, filename := range src {
*dst = append(*dst, protocol.URIFromPath(filename))
}
}
copyURIs(&mp.CompiledGoFiles, pkg.CompiledGoFiles)
copyURIs(&mp.GoFiles, pkg.GoFiles)
copyURIs(&mp.IgnoredFiles, pkg.IgnoredFiles)
copyURIs(&mp.OtherFiles, pkg.OtherFiles)
depsByImpPath := make(map[ImportPath]PackageID)
depsByPkgPath := make(map[PackagePath]PackageID)

3
gopls/internal/cache/metadata/metadata.go поставляемый
Просмотреть файл

@ -45,10 +45,11 @@ type Package struct {
PkgPath PackagePath
Name PackageName
// these three fields are as defined by go/packages.Package
// These fields are as defined by go/packages.Package
GoFiles []protocol.DocumentURI
CompiledGoFiles []protocol.DocumentURI
IgnoredFiles []protocol.DocumentURI
OtherFiles []protocol.DocumentURI
ForTest PackagePath // q in a "p [q.test]" package, else ""
TypesSizes types.Sizes

Просмотреть файл

@ -12,12 +12,15 @@ import (
"go/parser"
"go/token"
"go/types"
"regexp"
"strings"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/cache/metadata"
"golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/astutil"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/internal/event"
)
@ -92,6 +95,18 @@ func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p
return builtinDefinition(ctx, snapshot, obj)
}
// Non-go (e.g. assembly) symbols
//
// When already at the definition of a Go function without
// a body, we jump to its non-Go (C or assembly) definition.
for _, decl := range pgf.File.Decls {
if decl, ok := decl.(*ast.FuncDecl); ok &&
decl.Body == nil &&
astutil.NodeContains(decl.Name, pos) {
return nonGoDefinition(ctx, snapshot, pkg, decl.Name.Name)
}
}
// Finally, map the object position.
loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, obj.Pos(), adjustedObjEnd(obj))
if err != nil {
@ -353,3 +368,40 @@ func mapPosition(ctx context.Context, fset *token.FileSet, s file.Source, start,
m := protocol.NewMapper(fh.URI(), content)
return m.PosLocation(file, start, end)
}
// nonGoDefinition returns the location of the definition of a non-Go symbol.
// Only assembly is supported for now.
func nonGoDefinition(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, symbol string) ([]protocol.Location, error) {
// Examples:
// TEXT runtime·foo(SB)
// TEXT ·foo<ABIInternal>(SB)
// TODO(adonovan): why does ^TEXT cause it not to match?
pattern := regexp.MustCompile("TEXT\\b.*·(" + regexp.QuoteMeta(symbol) + ")[\\(<]")
for _, uri := range pkg.Metadata().OtherFiles {
if strings.HasSuffix(uri.Path(), ".s") {
fh, err := snapshot.ReadFile(ctx, uri)
if err != nil {
return nil, err // context cancelled
}
content, err := fh.Content()
if err != nil {
continue // can't read file
}
if match := pattern.FindSubmatchIndex(content); match != nil {
mapper := protocol.NewMapper(uri, content)
loc, err := mapper.OffsetLocation(match[2], match[3])
if err != nil {
return nil, err
}
return []protocol.Location{loc}, nil
}
}
}
// TODO(adonovan): try C files
// This may be reached for functions that aren't implemented
// in assembly (e.g. compiler intrinsics like getg).
return nil, fmt.Errorf("can't find non-Go definition of %s", symbol)
}

Просмотреть файл

@ -5,9 +5,11 @@
package misc
import (
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"testing"
@ -595,3 +597,48 @@ func _(err error) {
}
})
}
func TestAssemblyDefinition(t *testing.T) {
// This test cannot be expressed as a marker test because
// the expect package ignores markers (@loc) within a .s file.
const src = `
-- go.mod --
module mod.com
-- foo_darwin_arm64.s --
// assembly implementation
TEXT ·foo(SB),NOSPLIT,$0
RET
-- a.go --
//go:build darwin && arm64
package a
// Go declaration
func foo(int) int
var _ = foo(123) // call
`
Run(t, src, func(t *testing.T, env *Env) {
env.OpenFile("a.go")
locString := func(loc protocol.Location) string {
return fmt.Sprintf("%s:%s", filepath.Base(loc.URI.Path()), loc.Range)
}
// Definition at the call"foo(123)" takes us to the Go declaration.
callLoc := env.RegexpSearch("a.go", regexp.QuoteMeta("foo(123)"))
declLoc := env.GoToDefinition(callLoc)
if got, want := locString(declLoc), "a.go:5:5-5:8"; got != want {
t.Errorf("Definition(call): got %s, want %s", got, want)
}
// Definition a second time takes us to the assembly implementation.
implLoc := env.GoToDefinition(declLoc)
if got, want := locString(implLoc), "foo_darwin_arm64.s:2:6-2:9"; got != want {
t.Errorf("Definition(go decl): got %s, want %s", got, want)
}
})
}