diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index 909003288bc..93518886c7e 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go @@ -1456,12 +1456,12 @@ type typeCheckInputs struct { id PackageID // Used for type checking: - pkgPath PackagePath - name PackageName - goFiles, compiledGoFiles []file.Handle - sizes types.Sizes - depsByImpPath map[ImportPath]PackageID - goVersion string // packages.Module.GoVersion, e.g. "1.18" + pkgPath PackagePath + name PackageName + goFiles, compiledGoFiles, asmFiles []file.Handle + sizes types.Sizes + depsByImpPath map[ImportPath]PackageID + goVersion string // packages.Module.GoVersion, e.g. "1.18" // Used for type check diagnostics: // TODO(rfindley): consider storing less data in gobDiagnostics, and @@ -1491,6 +1491,10 @@ func (s *Snapshot) typeCheckInputs(ctx context.Context, mp *metadata.Package) (* if err != nil { return nil, err } + asmFiles, err := readFiles(ctx, s, mp.AsmFiles) + if err != nil { + return nil, err + } goVersion := "" if mp.Module != nil && mp.Module.GoVersion != "" { @@ -1503,6 +1507,7 @@ func (s *Snapshot) typeCheckInputs(ctx context.Context, mp *metadata.Package) (* name: mp.Name, goFiles: goFiles, compiledGoFiles: compiledGoFiles, + asmFiles: asmFiles, sizes: mp.TypesSizes, depsByImpPath: mp.DepsByImpPath, goVersion: goVersion, @@ -1555,6 +1560,10 @@ func localPackageKey(inputs *typeCheckInputs) file.Hash { for _, fh := range inputs.goFiles { fmt.Fprintln(hasher, fh.Identity()) } + fmt.Fprintf(hasher, "asmFiles:%d\n", len(inputs.asmFiles)) + for _, fh := range inputs.asmFiles { + fmt.Fprintln(hasher, fh.Identity()) + } // types sizes wordSize := inputs.sizes.Sizeof(types.Typ[types.Int]) @@ -1611,6 +1620,10 @@ func (b *typeCheckBatch) checkPackage(ctx context.Context, fset *token.FileSet, pkg.parseErrors = append(pkg.parseErrors, pgf.ParseErr) } } + pkg.asmFiles, err = parseAsmFiles(ctx, inputs.asmFiles...) + if err != nil { + return nil, err + } // Use the default type information for the unsafe package. if inputs.pkgPath == "unsafe" { diff --git a/gopls/internal/cache/load.go b/gopls/internal/cache/load.go index e15e0cef0b6..b7d8d671168 100644 --- a/gopls/internal/cache/load.go +++ b/gopls/internal/cache/load.go @@ -454,6 +454,13 @@ func buildMetadata(updates map[PackageID]*metadata.Package, loadDir string, stan *dst = append(*dst, protocol.URIFromPath(filename)) } } + // Copy SFiles to AsmFiles. + for _, filename := range pkg.OtherFiles { + if !strings.HasSuffix(filename, ".s") { + continue + } + mp.AsmFiles = append(mp.AsmFiles, protocol.URIFromPath(filename)) + } copyURIs(&mp.CompiledGoFiles, pkg.CompiledGoFiles) copyURIs(&mp.GoFiles, pkg.GoFiles) copyURIs(&mp.IgnoredFiles, pkg.IgnoredFiles) diff --git a/gopls/internal/cache/metadata/metadata.go b/gopls/internal/cache/metadata/metadata.go index 81b6dc57e1f..2e0eb82b81a 100644 --- a/gopls/internal/cache/metadata/metadata.go +++ b/gopls/internal/cache/metadata/metadata.go @@ -51,6 +51,8 @@ type Package struct { IgnoredFiles []protocol.DocumentURI OtherFiles []protocol.DocumentURI + AsmFiles []protocol.DocumentURI // *.s subset of OtherFiles + ForTest PackagePath // q in a "p [q.test]" package, else "" TypesSizes types.Sizes Errors []packages.Error // must be set for packages in import cycles diff --git a/gopls/internal/cache/package.go b/gopls/internal/cache/package.go index 3477d522cee..da12ec7989a 100644 --- a/gopls/internal/cache/package.go +++ b/gopls/internal/cache/package.go @@ -19,6 +19,7 @@ import ( "golang.org/x/tools/gopls/internal/cache/testfuncs" "golang.org/x/tools/gopls/internal/cache/xrefs" "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/util/asm" ) // Convenient aliases for very heavily used types. @@ -49,6 +50,7 @@ type syntaxPackage struct { fset *token.FileSet // for now, same as the snapshot's FileSet goFiles []*parsego.File compiledGoFiles []*parsego.File + asmFiles []*asm.File diagnostics []*Diagnostic parseErrors []scanner.ErrorList typeErrors []types.Error @@ -69,7 +71,7 @@ type syntaxPackage struct { func (p *syntaxPackage) xrefs() []byte { p.xrefsOnce.Do(func() { - p._xrefs = xrefs.Index(p.compiledGoFiles, p.types, p.typesInfo) + p._xrefs = xrefs.Index(p.compiledGoFiles, p.types, p.typesInfo, p.asmFiles) }) return p._xrefs } @@ -200,3 +202,21 @@ func (p *Package) ParseErrors() []scanner.ErrorList { func (p *Package) TypeErrors() []types.Error { return p.pkg.typeErrors } + +func (p *Package) AsmFiles() []*asm.File { + return p.pkg.asmFiles +} + +func (p *Package) AsmFile(uri protocol.DocumentURI) (*asm.File, error) { + return p.pkg.AsmFile(uri) +} + +func (pkg *syntaxPackage) AsmFile(uri protocol.DocumentURI) (*asm.File, error) { + for _, af := range pkg.asmFiles { + if af.URI == uri { + return af, nil + } + } + + return nil, fmt.Errorf("no parsed file for %s in %v", uri, pkg.id) +} diff --git a/gopls/internal/cache/parse.go b/gopls/internal/cache/parse.go index d733ca76799..ec94b182997 100644 --- a/gopls/internal/cache/parse.go +++ b/gopls/internal/cache/parse.go @@ -13,6 +13,7 @@ import ( "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/util/asm" ) // ParseGo parses the file whose contents are provided by fh. @@ -43,3 +44,21 @@ func parseGoImpl(ctx context.Context, fset *token.FileSet, fh file.Handle, mode pgf, _ := parsego.Parse(ctx, fset, fh.URI(), content, mode, purgeFuncBodies) // ignore 'fixes' return pgf, nil } + +// parseAsmFiles parses the assembly files whose contents are provided by fhs. +func parseAsmFiles(ctx context.Context, fhs ...file.Handle) ([]*asm.File, error) { + pafs := make([]*asm.File, len(fhs)) + for i, fh := range fhs { + var err error + content, err := fh.Content() + if err != nil { + return nil, err + } + // Check for context cancellation before actually doing the parse. + if ctx.Err() != nil { + return nil, ctx.Err() + } + pafs[i] = asm.Parse(fh.URI(), content) + } + return pafs, nil +} diff --git a/gopls/internal/cache/xrefs/xrefs.go b/gopls/internal/cache/xrefs/xrefs.go index d9b7051737a..6b1791b24a8 100644 --- a/gopls/internal/cache/xrefs/xrefs.go +++ b/gopls/internal/cache/xrefs/xrefs.go @@ -11,19 +11,22 @@ package xrefs import ( "go/ast" "go/types" + "slices" "sort" "golang.org/x/tools/go/types/objectpath" "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/util/asm" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/frob" + "golang.org/x/tools/gopls/internal/util/morestrings" ) // Index constructs a serializable index of outbound cross-references // for the specified type-checked package. -func Index(files []*parsego.File, pkg *types.Package, info *types.Info) []byte { +func Index(files []*parsego.File, pkg *types.Package, info *types.Info, asmFiles []*asm.File) []byte { // pkgObjects maps each referenced package Q to a mapping: // from each referenced symbol in Q to the ordered list // of references to that symbol from this package. @@ -112,6 +115,34 @@ func Index(files []*parsego.File, pkg *types.Package, info *types.Info) []byte { } } + // For each asm file, record references to identifiers. + for fileIndex, af := range asmFiles { + for _, id := range af.Idents { + _, name, ok := morestrings.CutLast(id.Name, ".") + if !ok { + continue + } + obj := pkg.Scope().Lookup(name) + if obj == nil { + // TODO(grootguo): If the object is not found in the current package, + // consider handling cross-package references. + continue + } + objects := getObjects(pkg) + gobObj, ok := objects[obj] + if !ok { + gobObj = &gobObject{Path: objectpath.Path(obj.Name())} + objects[obj] = gobObj + } + if rng, err := af.NodeRange(id); err == nil { + gobObj.Refs = append(gobObj.Refs, gobRef{ + FileIndex: fileIndex, + Range: rng, + }) + } + } + } + // Flatten the maps into slices, and sort for determinism. var packages []*gobPackage for p := range pkgObjects { @@ -147,7 +178,7 @@ func Lookup(mp *metadata.Package, data []byte, targets map[metadata.PackagePath] for _, gobObj := range gp.Objects { if _, ok := objectSet[gobObj.Path]; ok { for _, ref := range gobObj.Refs { - uri := mp.CompiledGoFiles[ref.FileIndex] + uri := slices.Concat(mp.CompiledGoFiles, mp.AsmFiles)[ref.FileIndex] locs = append(locs, protocol.Location{ URI: uri, Range: ref.Range, diff --git a/gopls/internal/goasm/definition.go b/gopls/internal/goasm/definition.go index 903916d265d..3eeb0bd7b9c 100644 --- a/gopls/internal/goasm/definition.go +++ b/gopls/internal/goasm/definition.go @@ -45,7 +45,7 @@ func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p // // TODO(adonovan): make this just another // attribute of the type-checked cache.Package. - file := asm.Parse(content) + file := asm.Parse(fh.URI(), content) // Figure out the selected symbol. // For now, just find the identifier around the cursor. diff --git a/gopls/internal/goasm/references.go b/gopls/internal/goasm/references.go new file mode 100644 index 00000000000..f7a09ca0fd3 --- /dev/null +++ b/gopls/internal/goasm/references.go @@ -0,0 +1,125 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package goasm provides language-server features for files in Go +// assembly language (https://go.dev/doc/asm). +package goasm + +import ( + "context" + "fmt" + "go/ast" + "go/types" + + "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/util/asm" + "golang.org/x/tools/gopls/internal/util/morestrings" + "golang.org/x/tools/internal/event" +) + +// References returns a list of locations (file and position) where the symbol under the cursor in an assembly file is referenced, +// including both Go source files and assembly files within the same package. +func References(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position, includeDeclaration bool) ([]protocol.Location, error) { + ctx, done := event.Start(ctx, "goasm.References") + defer done() + + mps, err := snapshot.MetadataForFile(ctx, fh.URI()) + if err != nil { + return nil, err + } + metadata.RemoveIntermediateTestVariants(&mps) + if len(mps) == 0 { + return nil, fmt.Errorf("no package metadata for file %s", fh.URI()) + } + mp := mps[0] + pkgs, err := snapshot.TypeCheck(ctx, mp.ID) + if err != nil { + return nil, err + } + pkg := pkgs[0] + asmFile, err := pkg.AsmFile(fh.URI()) + if err != nil { + return nil, err // "can't happen" + } + + offset, err := asmFile.Mapper.PositionOffset(position) + if err != nil { + return nil, err + } + + // Figure out the selected symbol. + // For now, just find the identifier around the cursor. + var found *asm.Ident + for _, id := range asmFile.Idents { + if id.Offset <= offset && offset <= id.End() { + found = &id + break + } + } + if found == nil { + return nil, fmt.Errorf("not an identifier") + } + + sym := found.Name + var locations []protocol.Location + _, name, ok := morestrings.CutLast(sym, ".") + if !ok { + return nil, fmt.Errorf("not found") + } + + // TODO(grootguo): Currently, only references to the symbol within the package are found (i.e., only Idents in this package's Go files are searched). + // It is still necessary to implement cross-package reference lookup: that is, to find all references to this symbol in other packages that import the current package. + // Refer to the global search logic in golang.References, and add corresponding test cases for verification. + obj := pkg.Types().Scope().Lookup(name) + matches := func(curObj types.Object) bool { + if curObj == nil { + return false + } + if curObj.Name() != obj.Name() { + return false + } + return true + } + for _, pgf := range pkg.CompiledGoFiles() { + for curId := range pgf.Cursor.Preorder((*ast.Ident)(nil)) { + id := curId.Node().(*ast.Ident) + curObj, ok := pkg.TypesInfo().Defs[id] + if !ok { + curObj, ok = pkg.TypesInfo().Uses[id] + if !ok { + continue + } + } + if !matches(curObj) { + continue + } + loc, err := pgf.NodeLocation(id) + if err != nil { + return nil, err + } + locations = append(locations, loc) + } + } + + // If includeDeclaration is false, return only reference locations (exclude declarations). + if !includeDeclaration { + return locations, nil + } + + for _, asmFile := range pkg.AsmFiles() { + for _, id := range asmFile.Idents { + if id.Name != sym { + continue + } + if loc, err := asmFile.NodeLocation(id); err == nil { + locations = append(locations, loc) + } + } + } + + return locations, nil +} diff --git a/gopls/internal/golang/references.go b/gopls/internal/golang/references.go index cf24685ca91..ae6747ed801 100644 --- a/gopls/internal/golang/references.go +++ b/gopls/internal/golang/references.go @@ -32,6 +32,7 @@ import ( "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/morestrings" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/event" ) @@ -610,6 +611,30 @@ func localReferences(pkg *cache.Package, targets map[types.Object]bool, correspo } } } + + // Iterate over all assembly files and find all references to the target object. + for _, pgf := range pkg.AsmFiles() { + for _, id := range pgf.Idents { + _, name, ok := morestrings.CutLast(id.Name, ".") + if !ok { + continue + } + obj := pkg.Types().Scope().Lookup(name) + if obj == nil { + continue + } + if !matches(obj) { + continue + } + if rng, err := pgf.NodeRange(id); err == nil { + asmLocation := protocol.Location{ + URI: pgf.URI, + Range: rng, + } + report(asmLocation, false) + } + } + } return nil } diff --git a/gopls/internal/server/references.go b/gopls/internal/server/references.go index 8a01e96498b..3a852ae7c05 100644 --- a/gopls/internal/server/references.go +++ b/gopls/internal/server/references.go @@ -8,6 +8,7 @@ import ( "context" "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/goasm" "golang.org/x/tools/gopls/internal/golang" "golang.org/x/tools/gopls/internal/label" "golang.org/x/tools/gopls/internal/protocol" @@ -35,6 +36,8 @@ func (s *server) References(ctx context.Context, params *protocol.ReferenceParam return template.References(ctx, snapshot, fh, params) case file.Go: return golang.References(ctx, snapshot, fh, params.Position, params.Context.IncludeDeclaration) + case file.Asm: + return goasm.References(ctx, snapshot, fh, params.Position, params.Context.IncludeDeclaration) } return nil, nil // empty result } diff --git a/gopls/internal/test/marker/testdata/references/asm.txt b/gopls/internal/test/marker/testdata/references/asm.txt new file mode 100644 index 00000000000..1bdfad56cb8 --- /dev/null +++ b/gopls/internal/test/marker/testdata/references/asm.txt @@ -0,0 +1,36 @@ +This test validates the References request functionality in Go assembly files. + +It ensures that references to both exported (`Add`) and unexported (`sub`) functions are correctly identified across Go and assembly files. The test covers: +- Locating the definition of functions in both Go and assembly files. +- Identifying all references to the functions (`Add` and `sub`) within the Go and assembly files. + +The test includes: +- `Add`: An exported function with references in both Go and assembly files. +- `sub`: An unexported function with references in both Go and assembly files, including a usage in Go code (`var _ = sub`). + +The assembly file demonstrates portable assembly syntax and verifies cross-file reference handling. + +-- go.mod -- +module example.com +go 1.24 + +-- foo/foo.go -- +package foo + +func Add(a, b int) int //@ loc(use, "Add"), refs("Add", use, def) +func sub(a, b int) int //@ loc(useSub, "sub"), refs("sub", useSub, defSub, refSub) +var _ = sub //@loc(refSub, "sub"), refs("sub", useSub, defSub, refSub) + +-- foo/foo.s -- +// portable assembly +#include "textflag.h" + +TEXT ·Add(SB), NOSPLIT, $0-24 //@ loc(def, "Add"), refs("Add", def, use) + MOVQ a+0(FP), AX + ADDQ b+8(FP), AX + RET + +TEXT ·sub(SB), NOSPLIT, $0-24 //@ loc(defSub, "sub"), refs("sub", defSub, useSub, refSub) + MOVQ a+0(FP), AX + SUBQ b+8(FP), AX + RET diff --git a/gopls/internal/util/asm/parse.go b/gopls/internal/util/asm/parse.go index 11c59a7cc3d..dc43c5f783a 100644 --- a/gopls/internal/util/asm/parse.go +++ b/gopls/internal/util/asm/parse.go @@ -11,6 +11,8 @@ import ( "fmt" "strings" "unicode" + + "golang.org/x/tools/gopls/internal/protocol" ) // Kind describes the nature of an identifier in an assembly file. @@ -43,12 +45,24 @@ var kindString = [...]string{ // A file represents a parsed file of Go assembly language. type File struct { + URI protocol.DocumentURI Idents []Ident + Mapper *protocol.Mapper + // TODO(adonovan): use token.File? This may be important in a // future in which analyzers can report diagnostics in .s files. } +func (f *File) NodeRange(ident Ident) (protocol.Range, error) { + return f.Mapper.OffsetRange(ident.Offset+2, ident.End()+1) +} + +// NodeLocation returns a protocol Location for the ast.Node interval in this file. +func (f *File) NodeLocation(ident Ident) (protocol.Location, error) { + return f.Mapper.OffsetLocation(ident.Offset+2, ident.End()+1) +} + // Ident represents an identifier in an assembly file. type Ident struct { Name string // symbol name (after correcting [·∕]); Name[0]='.' => current package @@ -61,7 +75,7 @@ func (id Ident) End() int { return id.Offset + len(id.Name) } // Parse extracts identifiers from Go assembly files. // Since it is a best-effort parser, it never returns an error. -func Parse(content []byte) *File { +func Parse(uri protocol.DocumentURI, content []byte) *File { var idents []Ident offset := 0 // byte offset of start of current line @@ -192,7 +206,7 @@ func Parse(content []byte) *File { _ = scan.Err() // ignore scan errors - return &File{Idents: idents} + return &File{Idents: idents, Mapper: protocol.NewMapper(uri, content), URI: uri} } // isIdent reports whether s is a valid Go assembly identifier. diff --git a/gopls/internal/util/asm/parse_test.go b/gopls/internal/util/asm/parse_test.go index 67a1286d28b..6d9bd7b8b93 100644 --- a/gopls/internal/util/asm/parse_test.go +++ b/gopls/internal/util/asm/parse_test.go @@ -39,7 +39,7 @@ TEXT ·g(SB),NOSPLIT,$0 `[1:]) const filename = "asm.s" m := protocol.NewMapper(protocol.URIFromPath(filename), src) - file := asm.Parse(src) + file := asm.Parse("", src) want := ` asm.s:5:6-11: data "hello"