Skip to content

Commit f0af81c

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/goasm: support Definition in Go *.s assembly
This CL provides a minimal implementation of the Definition query within Go assembly files, plus a test. For now it only works for references to package-level symbols in the same package or a dependency. Details: - add file.Kind Asm and protocol.LanguageKind "go.s". - include .s files in metadata.Graph.IDs mapping. - set LanguageKind correctly in gopls CLI. Also: - add String() method to file.Handle. - add convenient forward deps iterator to Graph. - internal/extract: extract notes from .s files too. Updates golang/go#71754 Change-Id: I0c518c3279f825411221ebe23dc04654e129fc56 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649461 Auto-Submit: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]> Commit-Queue: Alan Donovan <[email protected]>
1 parent 300465c commit f0af81c

File tree

16 files changed

+304
-26
lines changed

16 files changed

+304
-26
lines changed

gopls/internal/cache/fs_memoized.go

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ type diskFile struct {
4141
err error
4242
}
4343

44+
func (h *diskFile) String() string { return h.uri.Path() }
45+
4446
func (h *diskFile) URI() protocol.DocumentURI { return h.uri }
4547

4648
func (h *diskFile) Identity() file.Identity {

gopls/internal/cache/fs_overlay.go

+2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ type overlay struct {
6464
saved bool
6565
}
6666

67+
func (o *overlay) String() string { return o.uri.Path() }
68+
6769
func (o *overlay) URI() protocol.DocumentURI { return o.uri }
6870

6971
func (o *overlay) Identity() file.Identity {

gopls/internal/cache/metadata/graph.go

+36
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
package metadata
66

77
import (
8+
"iter"
89
"sort"
10+
"strings"
911

1012
"golang.org/x/tools/go/packages"
1113
"golang.org/x/tools/gopls/internal/protocol"
@@ -99,6 +101,11 @@ func newGraph(pkgs map[PackageID]*Package) *Graph {
99101
for _, uri := range mp.GoFiles {
100102
uris[uri] = struct{}{}
101103
}
104+
for _, uri := range mp.OtherFiles {
105+
if strings.HasSuffix(string(uri), ".s") { // assembly
106+
uris[uri] = struct{}{}
107+
}
108+
}
102109
for uri := range uris {
103110
uriIDs[uri] = append(uriIDs[uri], id)
104111
}
@@ -160,6 +167,35 @@ func (g *Graph) ReverseReflexiveTransitiveClosure(ids ...PackageID) map[PackageI
160167
return seen
161168
}
162169

170+
// ForwardReflexiveTransitiveClosure returns an iterator over the
171+
// specified nodes and all their forward dependencies, in an arbitrary
172+
// topological (dependencies-first) order. The order may vary.
173+
func (g *Graph) ForwardReflexiveTransitiveClosure(ids ...PackageID) iter.Seq[*Package] {
174+
return func(yield func(*Package) bool) {
175+
seen := make(map[PackageID]bool)
176+
var visit func(PackageID) bool
177+
visit = func(id PackageID) bool {
178+
if !seen[id] {
179+
seen[id] = true
180+
if mp := g.Packages[id]; mp != nil {
181+
for _, depID := range mp.DepsByPkgPath {
182+
if !visit(depID) {
183+
return false
184+
}
185+
}
186+
if !yield(mp) {
187+
return false
188+
}
189+
}
190+
}
191+
return true
192+
}
193+
for _, id := range ids {
194+
visit(id)
195+
}
196+
}
197+
}
198+
163199
// breakImportCycles breaks import cycles in the metadata by deleting
164200
// Deps* edges. It modifies only metadata present in the 'updates'
165201
// subset. This function has an internal test.

gopls/internal/cache/parse_cache_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ type fakeFileHandle struct {
218218
hash file.Hash
219219
}
220220

221+
func (h fakeFileHandle) String() string {
222+
return h.uri.Path()
223+
}
224+
221225
func (h fakeFileHandle) URI() protocol.DocumentURI {
222226
return h.uri
223227
}

gopls/internal/cache/session.go

+1
Original file line numberDiff line numberDiff line change
@@ -1084,6 +1084,7 @@ type brokenFile struct {
10841084
err error
10851085
}
10861086

1087+
func (b brokenFile) String() string { return b.uri.Path() }
10871088
func (b brokenFile) URI() protocol.DocumentURI { return b.uri }
10881089
func (b brokenFile) Identity() file.Identity { return file.Identity{URI: b.uri} }
10891090
func (b brokenFile) SameContentsOnDisk() bool { return false }

gopls/internal/cache/snapshot.go

+21
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,8 @@ func fileKind(fh file.Handle) file.Kind {
323323
return file.Sum
324324
case ".work":
325325
return file.Work
326+
case ".s":
327+
return file.Asm
326328
}
327329
return file.UnknownKind
328330
}
@@ -645,13 +647,32 @@ func (s *Snapshot) Tests(ctx context.Context, ids ...PackageID) ([]*testfuncs.In
645647
return indexes, s.forEachPackage(ctx, ids, pre, post)
646648
}
647649

650+
// NarrowestMetadataForFile returns metadata for the narrowest package
651+
// (the one with the fewest files) that encloses the specified file.
652+
// The result may be a test variant, but never an intermediate test variant.
653+
func (snapshot *Snapshot) NarrowestMetadataForFile(ctx context.Context, uri protocol.DocumentURI) (*metadata.Package, error) {
654+
mps, err := snapshot.MetadataForFile(ctx, uri)
655+
if err != nil {
656+
return nil, err
657+
}
658+
metadata.RemoveIntermediateTestVariants(&mps)
659+
if len(mps) == 0 {
660+
return nil, fmt.Errorf("no package metadata for file %s", uri)
661+
}
662+
return mps[0], nil
663+
}
664+
648665
// MetadataForFile returns a new slice containing metadata for each
649666
// package containing the Go file identified by uri, ordered by the
650667
// number of CompiledGoFiles (i.e. "narrowest" to "widest" package),
651668
// and secondarily by IsIntermediateTestVariant (false < true).
652669
// The result may include tests and intermediate test variants of
653670
// importable packages.
654671
// It returns an error if the context was cancelled.
672+
//
673+
// TODO(adonovan): in nearly all cases the caller must use
674+
// [metadata.RemoveIntermediateTestVariants]. Make this a parameter to
675+
// force the caller to consider it (and reduce code).
655676
func (s *Snapshot) MetadataForFile(ctx context.Context, uri protocol.DocumentURI) ([]*metadata.Package, error) {
656677
if s.view.typ == AdHocView {
657678
// As described in golang/go#57209, in ad-hoc workspaces (where we load ./

gopls/internal/cmd/cmd.go

+16-1
Original file line numberDiff line numberDiff line change
@@ -773,10 +773,25 @@ func (c *connection) openFile(ctx context.Context, uri protocol.DocumentURI) (*c
773773
return nil, file.err
774774
}
775775

776+
// Choose language ID from file extension.
777+
var langID protocol.LanguageKind // "" eventually maps to file.UnknownKind
778+
switch filepath.Ext(uri.Path()) {
779+
case ".go":
780+
langID = "go"
781+
case ".mod":
782+
langID = "go.mod"
783+
case ".sum":
784+
langID = "go.sum"
785+
case ".work":
786+
langID = "go.work"
787+
case ".s":
788+
langID = "go.s"
789+
}
790+
776791
p := &protocol.DidOpenTextDocumentParams{
777792
TextDocument: protocol.TextDocumentItem{
778793
URI: uri,
779-
LanguageID: "go",
794+
LanguageID: langID,
780795
Version: 1,
781796
Text: string(file.mapper.Content),
782797
},

gopls/internal/cmd/definition.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func (d *definition) Run(ctx context.Context, args ...string) error {
9696
}
9797

9898
if len(locs) == 0 {
99-
return fmt.Errorf("%v: not an identifier", from)
99+
return fmt.Errorf("%v: no definition location (not an identifier?)", from)
100100
}
101101
file, err = conn.openFile(ctx, locs[0].URI)
102102
if err != nil {

gopls/internal/file/file.go

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ type Handle interface {
4949
// Content returns the contents of a file.
5050
// If the file is not available, returns a nil slice and an error.
5151
Content() ([]byte, error)
52+
// String returns the file's path.
53+
String() string
5254
}
5355

5456
// A Source maps URIs to Handles.

gopls/internal/file/kind.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ const (
2828
Tmpl
2929
// Work is a go.work file.
3030
Work
31+
// Asm is a Go assembly (.s) file.
32+
Asm
3133
)
3234

3335
func (k Kind) String() string {
@@ -42,13 +44,15 @@ func (k Kind) String() string {
4244
return "tmpl"
4345
case Work:
4446
return "go.work"
47+
case Asm:
48+
return "Go assembly"
4549
default:
4650
return fmt.Sprintf("internal error: unknown file kind %d", k)
4751
}
4852
}
4953

5054
// KindForLang returns the gopls file [Kind] associated with the given LSP
51-
// LanguageKind string from protocol.TextDocumentItem.LanguageID,
55+
// LanguageKind string from the LanguageID field of [protocol.TextDocumentItem],
5256
// or UnknownKind if the language is not one recognized by gopls.
5357
func KindForLang(langID protocol.LanguageKind) Kind {
5458
switch langID {
@@ -62,6 +66,8 @@ func KindForLang(langID protocol.LanguageKind) Kind {
6266
return Tmpl
6367
case "go.work":
6468
return Work
69+
case "go.s":
70+
return Asm
6571
default:
6672
return UnknownKind
6773
}

gopls/internal/goasm/definition.go

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package goasm
6+
7+
import (
8+
"bytes"
9+
"context"
10+
"fmt"
11+
"go/token"
12+
"strings"
13+
"unicode"
14+
15+
"golang.org/x/tools/gopls/internal/cache"
16+
"golang.org/x/tools/gopls/internal/cache/metadata"
17+
"golang.org/x/tools/gopls/internal/file"
18+
"golang.org/x/tools/gopls/internal/protocol"
19+
"golang.org/x/tools/gopls/internal/util/morestrings"
20+
"golang.org/x/tools/internal/event"
21+
)
22+
23+
// Definition handles the textDocument/definition request for Go assembly files.
24+
func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.Location, error) {
25+
ctx, done := event.Start(ctx, "goasm.Definition")
26+
defer done()
27+
28+
mp, err := snapshot.NarrowestMetadataForFile(ctx, fh.URI())
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
// Read the file.
34+
content, err := fh.Content()
35+
if err != nil {
36+
return nil, err
37+
}
38+
mapper := protocol.NewMapper(fh.URI(), content)
39+
offset, err := mapper.PositionOffset(position)
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
// Figure out the selected symbol.
45+
// For now, just find the identifier around the cursor.
46+
//
47+
// TODO(adonovan): use a real asm parser; see cmd/asm/internal/asm/parse.go.
48+
// Ideally this would just be just another attribute of the
49+
// type-checked cache.Package.
50+
nonIdentRune := func(r rune) bool { return !isIdentRune(r) }
51+
i := bytes.LastIndexFunc(content[:offset], nonIdentRune)
52+
j := bytes.IndexFunc(content[offset:], nonIdentRune)
53+
if j < 0 || j == 0 {
54+
return nil, nil // identifier runs to EOF, or not an identifier
55+
}
56+
sym := string(content[i+1 : offset+j])
57+
sym = strings.ReplaceAll(sym, "·", ".") // (U+00B7 MIDDLE DOT)
58+
sym = strings.ReplaceAll(sym, "∕", "/") // (U+2215 DIVISION SLASH)
59+
if sym != "" && sym[0] == '.' {
60+
sym = string(mp.PkgPath) + sym
61+
}
62+
63+
// package-qualified symbol?
64+
if pkgpath, name, ok := morestrings.CutLast(sym, "."); ok {
65+
// Find declaring package among dependencies.
66+
//
67+
// TODO(adonovan): assembly may legally reference
68+
// non-dependencies. For example, sync/atomic calls
69+
// internal/runtime/atomic. Perhaps we should search
70+
// the entire metadata graph, but that's path-dependent.
71+
var declaring *metadata.Package
72+
for pkg := range snapshot.MetadataGraph().ForwardReflexiveTransitiveClosure(mp.ID) {
73+
if pkg.PkgPath == metadata.PackagePath(pkgpath) {
74+
declaring = pkg
75+
break
76+
}
77+
}
78+
if declaring == nil {
79+
return nil, fmt.Errorf("package %q is not a dependency", pkgpath)
80+
}
81+
82+
pkgs, err := snapshot.TypeCheck(ctx, declaring.ID)
83+
if err != nil {
84+
return nil, err
85+
}
86+
pkg := pkgs[0]
87+
def := pkg.Types().Scope().Lookup(name)
88+
if def == nil {
89+
return nil, fmt.Errorf("no symbol %q in package %q", name, pkgpath)
90+
}
91+
loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, def.Pos(), def.Pos())
92+
if err == nil {
93+
return []protocol.Location{loc}, nil
94+
}
95+
}
96+
97+
// TODO(adonovan): support jump to var, block label, and other
98+
// TEXT, DATA, and GLOBAL symbols in the same file. Needs asm parser.
99+
100+
return nil, nil
101+
}
102+
103+
// The assembler allows center dot (· U+00B7) and
104+
// division slash (∕ U+2215) to work as identifier characters.
105+
func isIdentRune(r rune) bool {
106+
return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '·' || r == '∕'
107+
}
108+
109+
// TODO(rfindley): avoid the duplicate column mapping here, by associating a
110+
// column mapper with each file handle.
111+
// TODO(adonovan): plundered from ../golang; factor.
112+
func mapPosition(ctx context.Context, fset *token.FileSet, s file.Source, start, end token.Pos) (protocol.Location, error) {
113+
file := fset.File(start)
114+
uri := protocol.URIFromPath(file.Name())
115+
fh, err := s.ReadFile(ctx, uri)
116+
if err != nil {
117+
return protocol.Location{}, err
118+
}
119+
content, err := fh.Content()
120+
if err != nil {
121+
return protocol.Location{}, err
122+
}
123+
m := protocol.NewMapper(fh.URI(), content)
124+
return m.PosLocation(file, start, end)
125+
}

gopls/internal/golang/snapshot.go

+2-12
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,9 @@ import (
1414
"golang.org/x/tools/gopls/internal/protocol"
1515
)
1616

17-
// NarrowestMetadataForFile returns metadata for the narrowest package
18-
// (the one with the fewest files) that encloses the specified file.
19-
// The result may be a test variant, but never an intermediate test variant.
17+
//go:fix inline
2018
func NarrowestMetadataForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (*metadata.Package, error) {
21-
mps, err := snapshot.MetadataForFile(ctx, uri)
22-
if err != nil {
23-
return nil, err
24-
}
25-
metadata.RemoveIntermediateTestVariants(&mps)
26-
if len(mps) == 0 {
27-
return nil, fmt.Errorf("no package metadata for file %s", uri)
28-
}
29-
return mps[0], nil
19+
return snapshot.NarrowestMetadataForFile(ctx, uri)
3020
}
3121

3222
// NarrowestPackageForFile is a convenience function that selects the narrowest

gopls/internal/server/definition.go

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010

1111
"golang.org/x/tools/gopls/internal/file"
12+
"golang.org/x/tools/gopls/internal/goasm"
1213
"golang.org/x/tools/gopls/internal/golang"
1314
"golang.org/x/tools/gopls/internal/label"
1415
"golang.org/x/tools/gopls/internal/protocol"
@@ -37,6 +38,8 @@ func (s *server) Definition(ctx context.Context, params *protocol.DefinitionPara
3738
return template.Definition(snapshot, fh, params.Position)
3839
case file.Go:
3940
return golang.Definition(ctx, snapshot, fh, params.Position)
41+
case file.Asm:
42+
return goasm.Definition(ctx, snapshot, fh, params.Position)
4043
default:
4144
return nil, fmt.Errorf("can't find definitions for file type %s", kind)
4245
}

0 commit comments

Comments
 (0)