blob: f20fe85f54196de434b61277c4f6d6048cb1eff3 [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 golang
import (
"context"
"errors"
"fmt"
"go/ast"
"go/parser"
"go/token"
"go/types"
"regexp"
"strings"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/cache/metadata"
"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/astutil"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/internal/event"
)
// Definition handles the textDocument/definition request for Go files.
func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.Location, error) {
ctx, done := event.Start(ctx, "golang.Definition")
defer done()
pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
if err != nil {
return nil, err
}
pos, err := pgf.PositionPos(position)
if err != nil {
return nil, err
}
// Handle the case where the cursor is in an import.
importLocations, err := importDefinition(ctx, snapshot, pkg, pgf, pos)
if err != nil {
return nil, err
}
if len(importLocations) > 0 {
return importLocations, nil
}
// Handle the case where the cursor is in the package name.
// We use "<= End" to accept a query immediately after the package name.
if pgf.File != nil && pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End() {
// If there's no package documentation, just use current file.
declFile := pgf
for _, pgf := range pkg.CompiledGoFiles() {
if pgf.File.Name != nil && pgf.File.Doc != nil {
declFile = pgf
break
}
}
loc, err := declFile.NodeLocation(declFile.File.Name)
if err != nil {
return nil, err
}
return []protocol.Location{loc}, nil
}
// Handle the case where the cursor is in a linkname directive.
locations, err := linknameDefinition(ctx, snapshot, pgf.Mapper, position)
if !errors.Is(err, ErrNoLinkname) {
return locations, err // may be success or failure
}
// Handle the case where the cursor is in an embed directive.
locations, err = embedDefinition(pgf.Mapper, position)
if !errors.Is(err, ErrNoEmbed) {
return locations, err // may be success or failure
}
// Handle the case where the cursor is in a doc link.
locations, err = docLinkDefinition(ctx, snapshot, pkg, pgf, pos)
if !errors.Is(err, errNoCommentReference) {
return locations, err // may be success or failure
}
// The general case: the cursor is on an identifier.
_, obj, _ := referencedObject(pkg, pgf, pos)
if obj == nil {
return nil, nil
}
// Built-ins have no position.
if isBuiltin(obj) {
return builtinDefinition(ctx, snapshot, obj)
}
// Non-go (e.g. assembly) symbols
//
// When already at the definition of a Go function without
// a body, we jump to its non-Go (C or assembly) definition.
for _, decl := range pgf.File.Decls {
if decl, ok := decl.(*ast.FuncDecl); ok &&
decl.Body == nil &&
astutil.NodeContains(decl.Name, pos) {
return nonGoDefinition(ctx, snapshot, pkg, decl.Name.Name)
}
}
// Finally, map the object position.
loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, obj.Pos(), adjustedObjEnd(obj))
if err != nil {
return nil, err
}
return []protocol.Location{loc}, nil
}
// builtinDefinition returns the location of the fake source
// declaration of a built-in in {builtin,unsafe}.go.
func builtinDefinition(ctx context.Context, snapshot *cache.Snapshot, obj types.Object) ([]protocol.Location, error) {
pgf, ident, err := builtinDecl(ctx, snapshot, obj)
if err != nil {
return nil, err
}
loc, err := pgf.NodeLocation(ident)
if err != nil {
return nil, err
}
return []protocol.Location{loc}, nil
}
// builtinDecl returns the parsed Go file and node corresponding to a builtin
// object, which may be a universe object or part of types.Unsafe, as well as
// its declaring identifier.
func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object) (*parsego.File, *ast.Ident, error) {
// declaringIdent returns the file-level declaration node (as reported by
// ast.Object) and declaring identifier of name using legacy (go/ast) object
// resolution.
declaringIdent := func(file *ast.File, name string) (ast.Node, *ast.Ident, error) {
astObj := file.Scope.Lookup(name)
if astObj == nil {
// Every built-in should have documentation syntax.
// However, it is possible to reach this statement by
// commenting out declarations in {builtin,unsafe}.go.
return nil, nil, fmt.Errorf("internal error: no object for %s", name)
}
decl, ok := astObj.Decl.(ast.Node)
if !ok {
return nil, nil, bug.Errorf("internal error: no declaration for %s", obj.Name())
}
var ident *ast.Ident
switch node := decl.(type) {
case *ast.Field:
for _, id := range node.Names {
if id.Name == name {
ident = id
}
}
case *ast.ValueSpec:
for _, id := range node.Names {
if id.Name == name {
ident = id
}
}
case *ast.TypeSpec:
ident = node.Name
case *ast.Ident:
ident = node
case *ast.FuncDecl:
ident = node.Name
case *ast.ImportSpec, *ast.LabeledStmt, *ast.AssignStmt:
// Not reachable for imported objects.
default:
return nil, nil, bug.Errorf("internal error: unexpected decl type %T", decl)
}
if ident == nil {
return nil, nil, bug.Errorf("internal error: no declaring identifier for %s", obj.Name())
}
return decl, ident, nil
}
var (
pgf *parsego.File
ident *ast.Ident
err error
)
if obj.Pkg() == types.Unsafe {
// package "unsafe":
// parse $GOROOT/src/unsafe/unsafe.go
//
// (Strictly, we shouldn't assume that the ID of a std
// package is its PkgPath, but no Bazel+gopackagesdriver
// users have complained about this yet.)
unsafe := snapshot.Metadata("unsafe")
if unsafe == nil {
// If the type checker somehow resolved 'unsafe', we must have metadata
// for it.
return nil, nil, bug.Errorf("no metadata for package 'unsafe'")
}
uri := unsafe.GoFiles[0]
fh, err := snapshot.ReadFile(ctx, uri)
if err != nil {
return nil, nil, err
}
// TODO(rfindley): treat unsafe symmetrically with the builtin file. Either
// pre-parse them both, or look up metadata for both.
pgf, err = snapshot.ParseGo(ctx, fh, parsego.Full&^parser.SkipObjectResolution)
if err != nil {
return nil, nil, err
}
_, ident, err = declaringIdent(pgf.File, obj.Name())
if err != nil {
return nil, nil, err
}
} else {
// pseudo-package "builtin":
// use parsed $GOROOT/src/builtin/builtin.go
pgf, err = snapshot.BuiltinFile(ctx)
if err != nil {
return nil, nil, err
}
if obj.Parent() == types.Universe {
// built-in function or type
_, ident, err = declaringIdent(pgf.File, obj.Name())
if err != nil {
return nil, nil, err
}
} else if obj.Name() == "Error" {
// error.Error method
decl, _, err := declaringIdent(pgf.File, "error")
if err != nil {
return nil, nil, err
}
field := decl.(*ast.TypeSpec).Type.(*ast.InterfaceType).Methods.List[0]
ident = field.Names[0]
} else {
return nil, nil, bug.Errorf("unknown built-in %v", obj)
}
}
return pgf, ident, nil
}
// referencedObject returns the identifier and object referenced at the
// specified position, which must be within the file pgf, for the purposes of
// definition/hover/call hierarchy operations. It returns a nil object if no
// object was found at the given position.
//
// If the returned identifier is a type-switch implicit (i.e. the x in x :=
// e.(type)), the third result will be the type of the expression being
// switched on (the type of e in the example). This facilitates workarounds for
// limitations of the go/types API, which does not report an object for the
// identifier x.
//
// For embedded fields, referencedObject returns the type name object rather
// than the var (field) object.
//
// TODO(rfindley): this function exists to preserve the pre-existing behavior
// of golang.Identifier. Eliminate this helper in favor of sharing
// functionality with objectsAt, after choosing suitable primitives.
func referencedObject(pkg *cache.Package, pgf *parsego.File, pos token.Pos) (*ast.Ident, types.Object, types.Type) {
path := pathEnclosingObjNode(pgf.File, pos)
if len(path) == 0 {
return nil, nil, nil
}
var obj types.Object
info := pkg.TypesInfo()
switch n := path[0].(type) {
case *ast.Ident:
obj = info.ObjectOf(n)
// If n is the var's declaring ident in a type switch
// [i.e. the x in x := foo.(type)], it will not have an object. In this
// case, set obj to the first implicit object (if any), and return the type
// of the expression being switched on.
//
// The type switch may have no case clauses and thus no
// implicit objects; this is a type error ("unused x"),
if obj == nil {
if implicits, typ := typeSwitchImplicits(info, path); len(implicits) > 0 {
return n, implicits[0], typ
}
}
// If the original position was an embedded field, we want to jump
// to the field's type definition, not the field's definition.
if v, ok := obj.(*types.Var); ok && v.Embedded() {
// types.Info.Uses contains the embedded field's *types.TypeName.
if typeName := info.Uses[n]; typeName != nil {
obj = typeName
}
}
return n, obj, nil
}
return nil, nil, nil
}
// importDefinition returns locations defining a package referenced by the
// import spec containing pos.
//
// If pos is not inside an import spec, it returns nil, nil.
func importDefinition(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, pos token.Pos) ([]protocol.Location, error) {
var imp *ast.ImportSpec
for _, spec := range pgf.File.Imports {
// We use "<= End" to accept a query immediately after an ImportSpec.
if spec.Path.Pos() <= pos && pos <= spec.Path.End() {
imp = spec
}
}
if imp == nil {
return nil, nil
}
importPath := metadata.UnquoteImportPath(imp)
impID := pkg.Metadata().DepsByImpPath[importPath]
if impID == "" {
return nil, fmt.Errorf("failed to resolve import %q", importPath)
}
impMetadata := s.Metadata(impID)
if impMetadata == nil {
return nil, fmt.Errorf("missing information for package %q", impID)
}
var locs []protocol.Location
for _, f := range impMetadata.CompiledGoFiles {
fh, err := s.ReadFile(ctx, f)
if err != nil {
if ctx.Err() != nil {
return nil, ctx.Err()
}
continue
}
pgf, err := s.ParseGo(ctx, fh, parsego.Header)
if err != nil {
if ctx.Err() != nil {
return nil, ctx.Err()
}
continue
}
loc, err := pgf.NodeLocation(pgf.File)
if err != nil {
return nil, err
}
locs = append(locs, loc)
}
if len(locs) == 0 {
return nil, fmt.Errorf("package %q has no readable files", impID) // incl. unsafe
}
return locs, nil
}
// TODO(rfindley): avoid the duplicate column mapping here, by associating a
// column mapper with each file handle.
func mapPosition(ctx context.Context, fset *token.FileSet, s file.Source, start, end token.Pos) (protocol.Location, error) {
file := fset.File(start)
uri := protocol.URIFromPath(file.Name())
fh, err := s.ReadFile(ctx, uri)
if err != nil {
return protocol.Location{}, err
}
content, err := fh.Content()
if err != nil {
return protocol.Location{}, err
}
m := protocol.NewMapper(fh.URI(), content)
return m.PosLocation(file, start, end)
}
// nonGoDefinition returns the location of the definition of a non-Go symbol.
// Only assembly is supported for now.
func nonGoDefinition(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, symbol string) ([]protocol.Location, error) {
// Examples:
// TEXT runtime·foo(SB)
// TEXT ·foo<ABIInternal>(SB)
// TODO(adonovan): why does ^TEXT cause it not to match?
pattern := regexp.MustCompile("TEXT\\b.*·(" + regexp.QuoteMeta(symbol) + ")[\\(<]")
for _, uri := range pkg.Metadata().OtherFiles {
if strings.HasSuffix(uri.Path(), ".s") {
fh, err := snapshot.ReadFile(ctx, uri)
if err != nil {
return nil, err // context cancelled
}
content, err := fh.Content()
if err != nil {
continue // can't read file
}
if match := pattern.FindSubmatchIndex(content); match != nil {
mapper := protocol.NewMapper(uri, content)
loc, err := mapper.OffsetLocation(match[2], match[3])
if err != nil {
return nil, err
}
return []protocol.Location{loc}, nil
}
}
}
// TODO(adonovan): try C files
// This may be reached for functions that aren't implemented
// in assembly (e.g. compiler intrinsics like getg).
return nil, fmt.Errorf("can't find non-Go definition of %s", symbol)
}