blob: 04efa6114e44100085c0f94e1cdec93184da5d3b [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, pos protocol.Position) ([]protocol.Location, error) {
pkgPath, name := parseLinkname(ctx, snapshot, fh, pos)
if pkgPath == "" {
return nil, ErrNoLinkname
}
return findLinkname(ctx, snapshot, fh, pos, PackagePath(pkgPath), name)
}
// parseLinkname attempts to parse a go:linkname declaration at the given pos.
// If successful, it returns the package path and object name referenced by the second
// argument of the linkname directive.
//
// If the position is not in the second argument of a go:linkname directive, or parsing fails, it returns "", "".
func parseLinkname(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) (pkgPath, name string) {
// 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 "", ""
}
offset, err := pgf.Mapper.PositionOffset(pos)
if err != nil {
return "", ""
}
// 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 "", ""
}
// Inside 2nd arg [start, end]?
// (Assumes no trailing spaces.)
start := end - len(parts[2])
if !(start <= offset && offset <= end) {
return "", ""
}
linkname := parts[2]
// Split the pkg path from the name.
dot := strings.LastIndexByte(linkname, '.')
if dot < 0 {
return "", ""
}
return linkname[:dot], linkname[dot+1:]
}
// 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())
end := p.Offset + len(com.Text)
if p.Offset <= offset && offset < end {
return com.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, fh FileHandle, pos protocol.Position, pkgPath PackagePath, name string) ([]protocol.Location, 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, err
}
RemoveIntermediateTestVariants(&metas)
for _, meta := range metas {
if meta.PkgPath == pkgPath {
pkgMeta = meta
break
}
}
if pkgMeta == nil {
return nil, 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, err
}
pkg := pkgs[0]
obj := pkg.GetTypes().Scope().Lookup(name)
if obj == nil {
return nil, 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, err
}
loc, err := pgf.PosLocation(obj.Pos(), obj.Pos()+token.Pos(len(name)))
if err != nil {
return nil, err
}
return []protocol.Location{loc}, nil
}