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>
diff --git a/gopls/doc/features/navigation.md b/gopls/doc/features/navigation.md
index 5daac90..0037134 100644
--- a/gopls/doc/features/navigation.md
+++ b/gopls/doc/features/navigation.md
@@ -22,6 +22,8 @@
   (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,
diff --git a/gopls/doc/release/v0.17.0.md b/gopls/doc/release/v0.17.0.md
index 45eecef..c575229 100644
--- a/gopls/doc/release/v0.17.0.md
+++ b/gopls/doc/release/v0.17.0.md
@@ -46,3 +46,11 @@
 
 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.
diff --git a/gopls/internal/cache/load.go b/gopls/internal/cache/load.go
index 6c176fc..9373766 100644
--- a/gopls/internal/cache/load.go
+++ b/gopls/internal/cache/load.go
@@ -412,18 +412,15 @@
 
 	updates[id] = mp
 
-	for _, filename := range pkg.CompiledGoFiles {
-		uri := protocol.URIFromPath(filename)
-		mp.CompiledGoFiles = append(mp.CompiledGoFiles, uri)
+	copyURIs := func(dst *[]protocol.DocumentURI, src []string) {
+		for _, filename := range src {
+			*dst = append(*dst, protocol.URIFromPath(filename))
+		}
 	}
-	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(&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)
diff --git a/gopls/internal/cache/metadata/metadata.go b/gopls/internal/cache/metadata/metadata.go
index 7860f33..e42aac3 100644
--- a/gopls/internal/cache/metadata/metadata.go
+++ b/gopls/internal/cache/metadata/metadata.go
@@ -45,10 +45,11 @@
 	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
diff --git a/gopls/internal/golang/definition.go b/gopls/internal/golang/definition.go
index 438fe2a..f20fe85 100644
--- a/gopls/internal/golang/definition.go
+++ b/gopls/internal/golang/definition.go
@@ -12,12 +12,15 @@
 	"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 @@
 		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 @@
 	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)
+}
diff --git a/gopls/internal/test/integration/misc/definition_test.go b/gopls/internal/test/integration/misc/definition_test.go
index 6b364e2..71f255b 100644
--- a/gopls/internal/test/integration/misc/definition_test.go
+++ b/gopls/internal/test/integration/misc/definition_test.go
@@ -5,9 +5,11 @@
 package misc
 
 import (
+	"fmt"
 	"os"
 	"path"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"testing"
 
@@ -595,3 +597,48 @@
 		}
 	})
 }
+
+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)
+		}
+	})
+}