blob: fe9fb8efc7f24d0ce8735aecc0fc9433b00b3d99 [file]
// 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
import (
"context"
"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"
)
// A resolution is the result of resolving an assembly identifier to its
// definition, shared by Definition and Hover.
type resolution struct {
// file is the parsed assembly file.
file *asm.File
// found is the identifier under the cursor, or nil if the cursor is
// not on an identifier.
found *asm.Ident
// obj is the Go object for a package-qualified symbol (including a
// current-package symbol such as ·foo), or nil if the symbol has no
// Go declaration.
obj types.Object
// pkg is the type-checked package that declares obj, or nil.
pkg *cache.Package
// localDef is the defining identifier in the assembly file for a local
// symbol — a label, a bare TEXT/GLOBL symbol, or a current-package
// symbol without a Go declaration. It is nil if none was found. For a
// label, only the enclosing TEXT function is searched.
localDef *asm.Ident
}
// resolve resolves the assembly identifier at rng to its definition.
//
// For a package-qualified symbol (including a current-package symbol such
// as ·foo, which is rewritten to pkgpath.foo), resolve type-checks the
// declaring package and returns the Go object in res.obj. For a local
// symbol, resolve returns the defining identifier in the assembly file in
// res.localDef.
//
// res.found is nil if the cursor is not on an identifier.
func resolve(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range) (res resolution, err error) {
// Package metadata is needed only to resolve package-qualified symbols
// to Go declarations. An assembly-only file with no Go package has
// none; tolerate the error and fall back to local assembly definitions.
mp, err := snapshot.NarrowestMetadataForFile(ctx, fh.URI())
if err != nil {
mp = nil
}
content, err := fh.Content()
if err != nil {
return res, err
}
// TODO(adonovan): make this just another
// attribute of the type-checked cache.Package.
res.file = asm.Parse(fh.URI(), content)
start, end, err := res.file.Mapper.RangeOffsets(rng)
if err != nil {
return res, err
}
// Find the identifier under the cursor.
// Use the selection range so that haphazard selections that
// happen to start in an identifier don't produce spurious matches.
res.found = res.file.IdentAt(start, end)
if res.found == nil {
return res, nil
}
// Resolve a package-qualified symbol (including a current-package
// symbol such as ·foo) to its Go declaration.
if mp != nil {
sym := res.found.Name
if sym != "" && sym[0] == '.' {
sym = string(mp.PkgPath) + sym
}
if pkgpath, name, ok := morestrings.CutLast(sym, "."); ok {
// Find declaring package among dependencies.
//
// TODO(adonovan): assembly may legally reference
// non-dependencies. For example, sync/atomic calls
// internal/runtime/atomic. Perhaps we should search
// the entire metadata graph, but that's path-dependent.
var declaring *metadata.Package
for pkg := range snapshot.MetadataGraph().ForwardReflexiveTransitiveClosure(mp.ID) {
if pkg.PkgPath == metadata.PackagePath(pkgpath) {
declaring = pkg
break
}
}
if declaring != nil {
pkgs, err := snapshot.TypeCheck(ctx, declaring.ID)
if err != nil {
return res, err
}
res.pkg = pkgs[0]
res.obj = res.pkg.Types().Scope().Lookup(name)
}
// If obj is nil (no Go declaration, e.g. an asm-only
// symbol), fall through to the local-definition search.
}
}
// Find the definition of a local symbol — a label, a bare TEXT/GLOBL
// symbol, or a package-qualified symbol without a Go declaration — in
// the assembly file.
if res.obj == nil {
// Labels are function-scoped: a label definition matches only
// within the enclosing TEXT function, so that a jump doesn't land
// on a same-named label in another function.
lo, hi := res.file.FunctionRange(res.found.Offset)
for _, id := range res.file.Idents {
if id.Name != res.found.Name {
continue
}
switch id.Kind {
case asm.Text, asm.Global:
res.localDef = &id
case asm.Label:
if lo <= id.Offset && id.Offset < hi {
res.localDef = &id
}
}
if res.localDef != nil {
break
}
}
}
return res, nil
}