blob: 961e8f25139b85f6cf999891a23a90d363d7a388 [file] [log] [blame]
// Copyright 2023 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 source
import (
"context"
"errors"
"fmt"
"go/token"
"strings"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/safetoken"
"golang.org/x/tools/gopls/internal/span"
)
// ErrNoLinkname is returned by LinknameDefinition when no linkname
// directive is found at a particular position.
// As such it indicates that other definitions could be worth checking.
var ErrNoLinkname = errors.New("no linkname directive found")
// LinknameDefinition finds the definition of the linkname directive in fh at pos.
// If there is no linkname directive at pos, returns ErrNoLinkname.
func LinknameDefinition(ctx context.Context, snapshot Snapshot, fh FileHandle, from protocol.Position) ([]protocol.Location, error) {
pkgPath, name, _ := parseLinkname(ctx, snapshot, fh, from)
if pkgPath == "" {
return nil, ErrNoLinkname
}
_, pgf, pos, err := findLinkname(ctx, snapshot, PackagePath(pkgPath), name)
if err != nil {
return nil, fmt.Errorf("find linkname: %w", err)
}
loc, err := pgf.PosLocation(pos, pos+token.Pos(len(name)))
if err != nil {
return nil, fmt.Errorf("location of linkname: %w", err)
}
return []protocol.Location{loc}, nil
}
// parseLinkname attempts to parse a go:linkname declaration at the given pos.
// If successful, it returns
// - package path referenced
// - object name referenced
// - byte offset in fh of the start of the link target
// of the linkname directives 2nd argument.
//
// If the position is not in the second argument of a go:linkname directive,
// or parsing fails, it returns "", "", 0.
func parseLinkname(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) (pkgPath, name string, targetOffset int) {
// TODO(adonovan): opt: parsing isn't necessary here.
// We're only looking for a line comment.
pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
if err != nil {
return "", "", 0
}
offset, err := pgf.Mapper.PositionOffset(pos)
if err != nil {
return "", "", 0
}
// Looking for pkgpath in '//go:linkname f pkgpath.g'.
// (We ignore 1-arg linkname directives.)
directive, end := findLinknameAtOffset(pgf, offset)
parts := strings.Fields(directive)
if len(parts) != 3 {
return "", "", 0
}
// Inside 2nd arg [start, end]?
// (Assumes no trailing spaces.)
start := end - len(parts[2])
if !(start <= offset && offset <= end) {
return "", "", 0
}
linkname := parts[2]
// Split the pkg path from the name.
dot := strings.LastIndexByte(linkname, '.')
if dot < 0 {
return "", "", 0
}
return linkname[:dot], linkname[dot+1:], start
}
// findLinknameAtOffset returns the first linkname directive on line and its end offset.
// Returns "", 0 if the offset is not in a linkname directive.
func findLinknameAtOffset(pgf *ParsedGoFile, offset int) (string, int) {
for _, grp := range pgf.File.Comments {
for _, com := range grp.List {
if strings.HasPrefix(com.Text, "//go:linkname") {
p := safetoken.Position(pgf.Tok, com.Pos())
// Sometimes source code (typically tests) has another
// comment after the directive, trim that away.
text := com.Text
if i := strings.LastIndex(text, "//"); i != 0 {
text = strings.TrimSpace(text[:i])
}
end := p.Offset + len(text)
if p.Offset <= offset && offset < end {
return text, end
}
}
}
}
return "", 0
}
// findLinkname searches dependencies of packages containing fh for an object
// with linker name matching the given package path and name.
func findLinkname(ctx context.Context, snapshot Snapshot, pkgPath PackagePath, name string) (Package, *ParsedGoFile, token.Pos, error) {
// Typically the linkname refers to a forward dependency
// or a reverse dependency, but in general it may refer
// to any package in the workspace.
var pkgMeta *Metadata
metas, err := snapshot.AllMetadata(ctx)
if err != nil {
return nil, nil, token.NoPos, err
}
RemoveIntermediateTestVariants(&metas)
for _, meta := range metas {
if meta.PkgPath == pkgPath {
pkgMeta = meta
break
}
}
if pkgMeta == nil {
return nil, nil, token.NoPos, fmt.Errorf("cannot find package %q", pkgPath)
}
// When found, type check the desired package (snapshot.TypeCheck in TypecheckFull mode),
pkgs, err := snapshot.TypeCheck(ctx, pkgMeta.ID)
if err != nil {
return nil, nil, token.NoPos, err
}
pkg := pkgs[0]
obj := pkg.GetTypes().Scope().Lookup(name)
if obj == nil {
return nil, nil, token.NoPos, fmt.Errorf("package %q does not define %s", pkgPath, name)
}
objURI := safetoken.StartPosition(pkg.FileSet(), obj.Pos())
pgf, err := pkg.File(span.URIFromPath(objURI.Filename))
if err != nil {
return nil, nil, token.NoPos, err
}
return pkg, pgf, obj.Pos(), nil
}