internal/lsp/source: derive document symbols from syntax alone

The documentSymbols handler joined syntax information with type
information, meaning that it was only able to satisfy requests for files
in valid workspace packages. However, the value added by type
information was limited, and in many cases could be derived from syntax
alone. For example, while generating symbols for a const declaration, we
don't need the type checker to tell us that the symbol kind is const.

Refactor the documentSymbols handler to derive symbols from syntax
alone. This leads to some simplifications from the code, in addition to
eliminating the dependency on package data. Also, simplify symbol
details to just use types.ExprString, which includes some missing
information such as function return values. Also, update handling to
support Go 1.18 type embedding in interfaces.

Notably, this reverts decisions like golang/go#31202, in which we went to
effort to make the symbol kind more accurate. In my opinion (and the
opinion expressed in golang/go#52797), the cost of requiring type
information is not worth the minor improvement in accuracy of the symbol
kind, which (as far as I know) is only used for UI elements.

To facilitate testing (and start to clean up the test framework), make
several simplifications / improvements to the marker tests:
- simplify the collection of symbol data
- eliminate unused marks
- just use cmp.Diff for comparing results
- allow for arbitrary nesting of symbols.
- remove unnecessary @symbol annotations from workspace_symbol tests --
  their data is not used by workspace_symbol handlers
- remove Symbol and WorkspaceSymbol handlers from source_test.go. On
  inspection, these handlers were redundant with lsp_test.go.
Notably, the collection and assembly of @symbol annotations is still way
too complicated. It would be much simpler to just have a single golden
file summarizing the entire output, rather than weaving it together from
annotations. However, I realized this too late, and so it will have to
wait for a separate CL.

Fixes golang/go#52797
Fixes golang/vscode-go#2242
Updates golang/go#54845

Change-Id: I3a2c2d39f59f9d045a6cedf8023ff0c80a69d974
Reviewed-on: https://go-review.googlesource.com/c/tools/+/405254
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
Run-TryBot: Robert Findley <rfindley@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
This commit is contained in:
Robert Findley 2022-05-09 12:14:36 -04:00
parent 3dda4ba24c
commit b9adce94b7
24 changed files with 415 additions and 415 deletions

View File

@ -18,13 +18,13 @@ import (
"strconv"
"strings"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
"golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/diff/myers"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/safetoken"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/diff/myers"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
"golang.org/x/tools/internal/memoize"
)
@ -237,7 +237,7 @@ func (f *unexportedFilter) keep(ident *ast.Ident) bool {
func (f *unexportedFilter) filterDecl(decl ast.Decl) bool {
switch decl := decl.(type) {
case *ast.FuncDecl:
if ident := recvIdent(decl); ident != nil && !f.keep(ident) {
if ident := source.RecvIdent(decl.Recv); ident != nil && !f.keep(ident) {
return false
}
return f.keep(decl.Name)
@ -312,7 +312,7 @@ func (f *unexportedFilter) recordUses(file *ast.File) {
switch decl := decl.(type) {
case *ast.FuncDecl:
// Ignore methods on dropped types.
if ident := recvIdent(decl); ident != nil && !f.keep(ident) {
if ident := source.RecvIdent(decl.Recv); ident != nil && !f.keep(ident) {
break
}
// Ignore functions with dropped names.
@ -356,21 +356,6 @@ func (f *unexportedFilter) recordUses(file *ast.File) {
}
}
// recvIdent returns the identifier of a method receiver, e.g. *int.
func recvIdent(decl *ast.FuncDecl) *ast.Ident {
if decl.Recv == nil || len(decl.Recv.List) == 0 {
return nil
}
x := decl.Recv.List[0].Type
if star, ok := x.(*ast.StarExpr); ok {
x = star.X
}
if ident, ok := x.(*ast.Ident); ok {
return ident
}
return nil
}
// recordIdents records unexported identifiers in an Expr in uses.
// These may be types, e.g. in map[key]value, function names, e.g. in foo(),
// or simple variable references. References that will be discarded, such

View File

@ -8,6 +8,7 @@ import (
"testing"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/tests/compare"
"golang.org/x/tools/internal/span"
)
@ -17,7 +18,7 @@ func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.
expect := string(r.data.Golden(t, "symbols", filename, func() ([]byte, error) {
return []byte(got), nil
}))
if expect != got {
t.Errorf("symbols failed for %s expected:\n%s\ngot:\n%s", filename, expect, got)
if diff := compare.Text(expect, got); diff != "" {
t.Errorf("symbols differ from expected:\n%s", diff)
}
}

View File

@ -15,6 +15,8 @@ import (
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"golang.org/x/tools/gopls/internal/lsp/cache"
"golang.org/x/tools/gopls/internal/lsp/command"
"golang.org/x/tools/gopls/internal/lsp/protocol"
@ -1147,10 +1149,7 @@ func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.
if err != nil {
t.Fatal(err)
}
if len(got) != len(expectedSymbols) {
t.Errorf("want %d top-level symbols in %v, got %d", len(expectedSymbols), uri, len(got))
return
}
symbols := make([]protocol.DocumentSymbol, len(got))
for i, s := range got {
s, ok := s.(protocol.DocumentSymbol)
@ -1159,18 +1158,25 @@ func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.
}
symbols[i] = s
}
if diff := tests.DiffSymbols(t, uri, expectedSymbols, symbols); diff != "" {
t.Error(diff)
// Sort by position to make it easier to find errors.
sortSymbols := func(s []protocol.DocumentSymbol) {
sort.Slice(s, func(i, j int) bool {
return protocol.CompareRange(s[i].SelectionRange, s[j].SelectionRange) < 0
})
}
sortSymbols(expectedSymbols)
sortSymbols(symbols)
// Ignore 'Range' here as it is difficult (impossible?) to express
// multi-line ranges in the packagestest framework.
ignoreRange := cmpopts.IgnoreFields(protocol.DocumentSymbol{}, "Range")
if diff := cmp.Diff(expectedSymbols, symbols, ignoreRange); diff != "" {
t.Errorf("mismatching symbols (-want +got)\n%s", diff)
}
}
func (r *runner) WorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) {
r.callWorkspaceSymbols(t, uri, query, typ)
}
func (r *runner) callWorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) {
t.Helper()
matcher := tests.WorkspaceSymbolsTestTypeToMatcher(typ)
original := r.server.session.Options()

View File

@ -6,6 +6,7 @@ package lsppos
import (
"errors"
"go/ast"
"go/token"
"golang.org/x/tools/gopls/internal/lsp/protocol"
@ -58,3 +59,9 @@ func (m *TokenMapper) Range(start, end token.Pos) (protocol.Range, error) {
return protocol.Range{Start: startPos, End: endPos}, nil
}
// NodeRange returns the protocol range corresponding to the span of the given
// node.
func (m *TokenMapper) NodeRange(n ast.Node) (protocol.Range, error) {
return m.Range(n.Pos(), n.End())
}

View File

@ -148,6 +148,11 @@ func IsPoint(r Range) bool {
return r.Start.Line == r.End.Line && r.Start.Character == r.End.Character
}
// CompareRange returns -1 if a is before b, 0 if a == b, and 1 if a is after
// b.
//
// A range a is defined to be 'before' b if a.Start is before b.Start, or
// a.Start == b.Start and a.End is before b.End.
func CompareRange(a, b Range) int {
if r := ComparePosition(a.Start, b.Start); r != 0 {
return r
@ -155,6 +160,8 @@ func CompareRange(a, b Range) int {
return ComparePosition(a.End, b.End)
}
// ComparePosition returns -1 if a is before b, 0 if a == b, and 1 if a is
// after b.
func ComparePosition(a, b Position) int {
if a.Line < b.Line {
return -1

View File

@ -879,46 +879,11 @@ func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.Prepare
}
func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) {
fh, err := r.snapshot.GetFile(r.ctx, uri)
if err != nil {
t.Fatal(err)
}
symbols, err := source.DocumentSymbols(r.ctx, r.snapshot, fh)
if err != nil {
t.Errorf("symbols failed for %s: %v", uri, err)
}
if len(symbols) != len(expectedSymbols) {
t.Errorf("want %d top-level symbols in %v, got %d", len(expectedSymbols), uri, len(symbols))
return
}
if diff := tests.DiffSymbols(t, uri, expectedSymbols, symbols); diff != "" {
t.Error(diff)
}
// Removed in favor of just using the lsp_test implementation. See ../lsp_test.go
}
func (r *runner) WorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) {
r.callWorkspaceSymbols(t, uri, query, typ)
}
func (r *runner) callWorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) {
t.Helper()
matcher := tests.WorkspaceSymbolsTestTypeToMatcher(typ)
gotSymbols, err := source.WorkspaceSymbols(r.ctx, matcher, r.view.Options().SymbolStyle, []source.View{r.view}, query)
if err != nil {
t.Fatal(err)
}
got, err := tests.WorkspaceSymbolsString(r.ctx, r.data, uri, gotSymbols)
if err != nil {
t.Fatal(err)
}
got = filepath.ToSlash(tests.Normalize(got, r.normalizers))
want := string(r.data.Golden(t, fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) {
return []byte(got), nil
}))
if d := compare.Text(want, got); d != "" {
t.Error(d)
}
// Removed in favor of just using the lsp_test implementation. See ../lsp_test.go
}
func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.SignatureHelp) {

View File

@ -8,25 +8,34 @@ import (
"context"
"fmt"
"go/ast"
"go/token"
"go/types"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/gopls/internal/lsp/lsppos"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/internal/event"
)
func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.DocumentSymbol, error) {
ctx, done := event.Start(ctx, "source.DocumentSymbols")
defer done()
pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
content, err := fh.Read()
if err != nil {
return nil, err
}
pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
if err != nil {
return nil, fmt.Errorf("getting file for DocumentSymbols: %w", err)
}
info := pkg.GetTypesInfo()
q := Qualifier(pgf.File, pkg.GetTypes(), info)
m := lsppos.NewTokenMapper(content, pgf.Tok)
symbolsToReceiver := make(map[types.Type]int)
// Build symbols for file declarations. When encountering a declaration with
// errors (typically because positions are invalid), we skip the declaration
// entirely. VS Code fails to show any symbols if one of the top-level
// symbols is missing position information.
var symbols []protocol.DocumentSymbol
for _, decl := range pgf.File.Decls {
switch decl := decl.(type) {
@ -34,15 +43,11 @@ func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p
if decl.Name.Name == "_" {
continue
}
if obj := info.ObjectOf(decl.Name); obj != nil {
fs, err := funcSymbol(snapshot, pkg, decl, obj, q)
if err != nil {
return nil, err
}
fs, err := funcSymbol(m, decl)
if err == nil {
// If function is a method, prepend the type of the method.
if fs.Kind == protocol.Method {
rtype := obj.Type().(*types.Signature).Recv().Type()
fs.Name = fmt.Sprintf("(%s).%s", types.TypeString(rtype, q), fs.Name)
if decl.Recv != nil && len(decl.Recv.List) > 0 {
fs.Name = fmt.Sprintf("(%s).%s", types.ExprString(decl.Recv.List[0].Type), fs.Name)
}
symbols = append(symbols, fs)
}
@ -53,24 +58,17 @@ func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p
if spec.Name.Name == "_" {
continue
}
if obj := info.ObjectOf(spec.Name); obj != nil {
ts, err := typeSymbol(snapshot, pkg, info, spec, obj, q)
if err != nil {
return nil, err
}
ts, err := typeSymbol(m, spec)
if err == nil {
symbols = append(symbols, ts)
symbolsToReceiver[obj.Type()] = len(symbols) - 1
}
case *ast.ValueSpec:
for _, name := range spec.Names {
if name.Name == "_" {
continue
}
if obj := info.ObjectOf(name); obj != nil {
vs, err := varSymbol(snapshot, pkg, decl, name, obj, q)
if err != nil {
return nil, err
}
vs, err := varSymbol(m, spec, name, decl.Tok == token.CONST)
if err == nil {
symbols = append(symbols, vs)
}
}
@ -81,185 +79,157 @@ func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p
return symbols, nil
}
func funcSymbol(snapshot Snapshot, pkg Package, decl *ast.FuncDecl, obj types.Object, q types.Qualifier) (protocol.DocumentSymbol, error) {
func funcSymbol(m *lsppos.TokenMapper, decl *ast.FuncDecl) (protocol.DocumentSymbol, error) {
s := protocol.DocumentSymbol{
Name: obj.Name(),
Name: decl.Name.Name,
Kind: protocol.Function,
}
if decl.Recv != nil {
s.Kind = protocol.Method
}
var err error
s.Range, err = nodeToProtocolRange(snapshot, pkg, decl)
s.Range, err = m.Range(decl.Pos(), decl.End())
if err != nil {
return protocol.DocumentSymbol{}, err
}
s.SelectionRange, err = nodeToProtocolRange(snapshot, pkg, decl.Name)
s.SelectionRange, err = m.Range(decl.Name.Pos(), decl.Name.End())
if err != nil {
return protocol.DocumentSymbol{}, err
}
sig, _ := obj.Type().(*types.Signature)
if sig != nil {
if sig.Recv() != nil {
s.Kind = protocol.Method
}
s.Detail += "("
for i := 0; i < sig.Params().Len(); i++ {
if i > 0 {
s.Detail += ", "
}
param := sig.Params().At(i)
label := types.TypeString(param.Type(), q)
if param.Name() != "" {
label = fmt.Sprintf("%s %s", param.Name(), label)
}
s.Detail += label
}
s.Detail += ")"
}
s.Detail = types.ExprString(decl.Type)
return s, nil
}
func typeSymbol(snapshot Snapshot, pkg Package, info *types.Info, spec *ast.TypeSpec, obj types.Object, qf types.Qualifier) (protocol.DocumentSymbol, error) {
func typeSymbol(m *lsppos.TokenMapper, spec *ast.TypeSpec) (protocol.DocumentSymbol, error) {
s := protocol.DocumentSymbol{
Name: obj.Name(),
Name: spec.Name.Name,
}
s.Detail, _ = FormatType(obj.Type(), qf)
s.Kind = typeToKind(obj.Type())
var err error
s.Range, err = nodeToProtocolRange(snapshot, pkg, spec)
s.Range, err = m.NodeRange(spec)
if err != nil {
return protocol.DocumentSymbol{}, err
}
s.SelectionRange, err = nodeToProtocolRange(snapshot, pkg, spec.Name)
s.SelectionRange, err = m.NodeRange(spec.Name)
if err != nil {
return protocol.DocumentSymbol{}, err
}
t, objIsStruct := obj.Type().Underlying().(*types.Struct)
st, specIsStruct := spec.Type.(*ast.StructType)
if objIsStruct && specIsStruct {
for i := 0; i < t.NumFields(); i++ {
f := t.Field(i)
child := protocol.DocumentSymbol{
Name: f.Name(),
Kind: protocol.Field,
}
child.Detail, _ = FormatType(f.Type(), qf)
spanNode, selectionNode := nodesForStructField(i, st)
if span, err := nodeToProtocolRange(snapshot, pkg, spanNode); err == nil {
child.Range = span
}
if span, err := nodeToProtocolRange(snapshot, pkg, selectionNode); err == nil {
child.SelectionRange = span
}
s.Children = append(s.Children, child)
}
}
ti, objIsInterface := obj.Type().Underlying().(*types.Interface)
ai, specIsInterface := spec.Type.(*ast.InterfaceType)
if objIsInterface && specIsInterface {
for i := 0; i < ti.NumExplicitMethods(); i++ {
method := ti.ExplicitMethod(i)
child := protocol.DocumentSymbol{
Name: method.Name(),
Kind: protocol.Method,
}
var spanNode, selectionNode ast.Node
Methods:
for _, f := range ai.Methods.List {
for _, id := range f.Names {
if id.Name == method.Name() {
spanNode, selectionNode = f, id
break Methods
}
}
}
child.Range, err = nodeToProtocolRange(snapshot, pkg, spanNode)
if err != nil {
return protocol.DocumentSymbol{}, err
}
child.SelectionRange, err = nodeToProtocolRange(snapshot, pkg, selectionNode)
if err != nil {
return protocol.DocumentSymbol{}, err
}
s.Children = append(s.Children, child)
}
for i := 0; i < ti.NumEmbeddeds(); i++ {
embedded := ti.EmbeddedType(i)
nt, isNamed := embedded.(*types.Named)
if !isNamed {
continue
}
child := protocol.DocumentSymbol{
Name: types.TypeString(embedded, qf),
}
child.Kind = typeToKind(embedded)
var spanNode, selectionNode ast.Node
Embeddeds:
for _, f := range ai.Methods.List {
if len(f.Names) > 0 {
continue
}
if t := info.TypeOf(f.Type); types.Identical(nt, t) {
spanNode, selectionNode = f, f.Type
break Embeddeds
}
}
child.Range, err = nodeToProtocolRange(snapshot, pkg, spanNode)
if err != nil {
return protocol.DocumentSymbol{}, err
}
child.SelectionRange, err = nodeToProtocolRange(snapshot, pkg, selectionNode)
if err != nil {
return protocol.DocumentSymbol{}, err
}
s.Children = append(s.Children, child)
}
}
s.Kind, s.Detail, s.Children = typeDetails(m, spec.Type)
return s, nil
}
func nodesForStructField(i int, st *ast.StructType) (span, selection ast.Node) {
j := 0
for _, field := range st.Fields.List {
if len(field.Names) == 0 {
if i == j {
return field, field.Type
}
j++
continue
func typeDetails(m *lsppos.TokenMapper, typExpr ast.Expr) (kind protocol.SymbolKind, detail string, children []protocol.DocumentSymbol) {
switch typExpr := typExpr.(type) {
case *ast.StructType:
kind = protocol.Struct
children = fieldListSymbols(m, typExpr.Fields, protocol.Field)
if len(children) > 0 {
detail = "struct{...}"
} else {
detail = "struct{}"
}
for _, name := range field.Names {
if i == j {
return field, name
}
j++
// Find interface methods and embedded types.
case *ast.InterfaceType:
kind = protocol.Interface
children = fieldListSymbols(m, typExpr.Methods, protocol.Method)
if len(children) > 0 {
detail = "interface{...}"
} else {
detail = "interface{}"
}
case *ast.FuncType:
kind = protocol.Function
detail = types.ExprString(typExpr)
default:
kind = protocol.Class // catch-all, for cases where we don't know the kind syntactically
detail = types.ExprString(typExpr)
}
return nil, nil
return
}
func varSymbol(snapshot Snapshot, pkg Package, decl ast.Node, name *ast.Ident, obj types.Object, q types.Qualifier) (protocol.DocumentSymbol, error) {
func fieldListSymbols(m *lsppos.TokenMapper, fields *ast.FieldList, fieldKind protocol.SymbolKind) []protocol.DocumentSymbol {
if fields == nil {
return nil
}
var symbols []protocol.DocumentSymbol
for _, field := range fields.List {
detail, children := "", []protocol.DocumentSymbol(nil)
if field.Type != nil {
_, detail, children = typeDetails(m, field.Type)
}
if len(field.Names) == 0 { // embedded interface or struct field
// By default, use the formatted type details as the name of this field.
// This handles potentially invalid syntax, as well as type embeddings in
// interfaces.
child := protocol.DocumentSymbol{
Name: detail,
Kind: protocol.Field, // consider all embeddings to be fields
Children: children,
}
// If the field is a valid embedding, promote the type name to field
// name.
selection := field.Type
if id := embeddedIdent(field.Type); id != nil {
child.Name = id.Name
child.Detail = detail
selection = id
}
if rng, err := m.NodeRange(field.Type); err == nil {
child.Range = rng
}
if rng, err := m.NodeRange(selection); err == nil {
child.SelectionRange = rng
}
symbols = append(symbols, child)
} else {
for _, name := range field.Names {
child := protocol.DocumentSymbol{
Name: name.Name,
Kind: fieldKind,
Detail: detail,
Children: children,
}
if rng, err := m.NodeRange(field); err == nil {
child.Range = rng
}
if rng, err := m.NodeRange(name); err == nil {
child.SelectionRange = rng
}
symbols = append(symbols, child)
}
}
}
return symbols
}
func varSymbol(m *lsppos.TokenMapper, spec *ast.ValueSpec, name *ast.Ident, isConst bool) (protocol.DocumentSymbol, error) {
s := protocol.DocumentSymbol{
Name: obj.Name(),
Name: name.Name,
Kind: protocol.Variable,
}
if _, ok := obj.(*types.Const); ok {
if isConst {
s.Kind = protocol.Constant
}
var err error
s.Range, err = nodeToProtocolRange(snapshot, pkg, decl)
s.Range, err = m.NodeRange(spec)
if err != nil {
return protocol.DocumentSymbol{}, err
}
s.SelectionRange, err = nodeToProtocolRange(snapshot, pkg, name)
s.SelectionRange, err = m.NodeRange(name)
if err != nil {
return protocol.DocumentSymbol{}, err
}
s.Detail = types.TypeString(obj.Type(), q)
if spec.Type != nil { // type may be missing from the syntax
_, s.Detail, s.Children = typeDetails(m, spec.Type)
}
return s, nil
}

View File

@ -18,9 +18,10 @@ import (
"strings"
"golang.org/x/mod/modfile"
"golang.org/x/tools/internal/bug"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/internal/bug"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/typeparams"
)
// MappedRange provides mapped protocol.Range for a span.Range, accounting for
@ -122,16 +123,6 @@ func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool {
return false
}
func nodeToProtocolRange(snapshot Snapshot, pkg Package, n ast.Node) (protocol.Range, error) {
mrng, err := posToMappedRange(snapshot, pkg, n.Pos(), n.End())
if err != nil {
return protocol.Range{}, err
}
return mrng.Range()
}
// objToMappedRange returns the MappedRange for the object's declaring
// identifier (or string literal, for an import).
func objToMappedRange(snapshot Snapshot, pkg Package, obj types.Object) (MappedRange, error) {
nameLen := len(obj.Name())
if pkgName, ok := obj.(*types.PkgName); ok {
@ -589,3 +580,51 @@ func ByteOffsetsToRange(m *protocol.ColumnMapper, uri span.URI, start, end int)
e := span.NewPoint(line, col, end)
return m.Range(span.New(uri, s, e))
}
// RecvIdent returns the type identifier of a method receiver.
// e.g. A for all of A, *A, A[T], *A[T], etc.
func RecvIdent(recv *ast.FieldList) *ast.Ident {
if recv == nil || len(recv.List) == 0 {
return nil
}
x := recv.List[0].Type
if star, ok := x.(*ast.StarExpr); ok {
x = star.X
}
switch ix := x.(type) { // check for instantiated receivers
case *ast.IndexExpr:
x = ix.X
case *typeparams.IndexListExpr:
x = ix.X
}
if ident, ok := x.(*ast.Ident); ok {
return ident
}
return nil
}
// embeddedIdent returns the type name identifier for an embedding x, if x in a
// valid embedding. Otherwise, it returns nil.
//
// Spec: An embedded field must be specified as a type name T or as a pointer
// to a non-interface type name *T
func embeddedIdent(x ast.Expr) *ast.Ident {
if star, ok := x.(*ast.StarExpr); ok {
x = star.X
}
switch ix := x.(type) { // check for instantiated receivers
case *ast.IndexExpr:
x = ix.X
case *typeparams.IndexListExpr:
x = ix.X
}
switch x := x.(type) {
case *ast.Ident:
return x
case *ast.SelectorExpr:
if _, ok := x.X.(*ast.Ident); ok {
return x.Sel
}
}
return nil
}

View File

@ -23,7 +23,7 @@ InlayHintsCount = 4
ReferencesCount = 27
RenamesCount = 41
PrepareRenamesCount = 7
SymbolsCount = 5
SymbolsCount = 1
WorkspaceSymbolsCount = 20
SignaturesCount = 33
LinksCount = 7

View File

@ -23,7 +23,7 @@ InlayHintsCount = 5
ReferencesCount = 27
RenamesCount = 48
PrepareRenamesCount = 7
SymbolsCount = 5
SymbolsCount = 2
WorkspaceSymbolsCount = 20
SignaturesCount = 33
LinksCount = 7

View File

@ -0,0 +1,16 @@
//go:build go1.18
// +build go1.18
package main
type T[P any] struct { //@symbol("T", "T", "Struct", "struct{...}", "T", "")
F P //@symbol("F", "F", "Field", "P", "", "T")
}
type Constraint interface { //@symbol("Constraint", "Constraint", "Interface", "interface{...}", "Constraint", "")
~int | struct{ int } //@symbol("~int | struct{int}", "~int | struct{ int }", "Field", "", "", "Constraint")
// TODO(rfindley): the selection range below is the entire interface field.
// Can we reduce it?
interface{ M() } //@symbol("interface{...}", "interface{ M() }", "Field", "", "iFaceField", "Constraint"), symbol("M", "M", "Method", "func()", "", "iFaceField")
}

View File

@ -0,0 +1,7 @@
-- symbols --
T Struct 6:6-6:7
F Field 7:2-7:3
Constraint Interface 10:6-10:16
interface{...} Field 15:2-15:18
~int | struct{int} Field 11:2-11:22

View File

@ -4,61 +4,88 @@ import (
"io"
)
// Each symbol marker in this file defines the following information:
// symbol(name, selectionSpan, kind, detail, id, parentID)
// - name: DocumentSymbol.Name
// - selectionSpan: DocumentSymbol.SelectionRange
// - kind: DocumentSymbol.Kind
// - detail: DocumentSymbol.Detail
// - id: if non-empty, a unique identifier for this symbol
// - parentID: if non-empty, the id of the parent of this symbol
//
// This data in aggregate defines a set of document symbols and their
// parent-child relationships, which is compared against the DocummentSymbols
// response from gopls for the current file.
//
// TODO(rfindley): the symbol annotations here are complicated and difficult to
// maintain. It would be simpler to just write out the full expected response
// in the golden file, perhaps as raw JSON.
var _ = 1
var x = 42 //@mark(symbolsx, "x"), symbol("x", "x", "Variable", "", "main.x")
var x = 42 //@symbol("x", "x", "Variable", "", "", "")
const y = 43 //@symbol("y", "y", "Constant", "", "main.y")
var nested struct { //@symbol("nested", "nested", "Variable", "struct{...}", "nested", "")
nestedField struct { //@symbol("nestedField", "nestedField", "Field", "struct{...}", "nestedField", "nested")
f int //@symbol("f", "f", "Field", "int", "", "nestedField")
}
}
type Number int //@symbol("Number", "Number", "Number", "", "main.Number")
const y = 43 //@symbol("y", "y", "Constant", "", "", "")
type Alias = string //@symbol("Alias", "Alias", "String", "", "main.Alias")
type Number int //@symbol("Number", "Number", "Class", "int", "", "")
type NumberAlias = Number //@symbol("NumberAlias", "NumberAlias", "Number", "", "main.NumberAlias")
type Alias = string //@symbol("Alias", "Alias", "Class", "string", "", "")
type NumberAlias = Number //@symbol("NumberAlias", "NumberAlias", "Class", "Number", "", "")
type (
Boolean bool //@symbol("Boolean", "Boolean", "Boolean", "", "main.Boolean")
BoolAlias = bool //@symbol("BoolAlias", "BoolAlias", "Boolean", "", "main.BoolAlias")
Boolean bool //@symbol("Boolean", "Boolean", "Class", "bool", "", "")
BoolAlias = bool //@symbol("BoolAlias", "BoolAlias", "Class", "bool", "", "")
)
type Foo struct { //@mark(symbolsFoo, "Foo"), symbol("Foo", "Foo", "Struct", "", "main.Foo")
Quux //@mark(fQuux, "Quux"), symbol("Quux", "Quux", "Field", "Foo", "main.Foo.Quux")
W io.Writer //@symbol("W" , "W", "Field", "Foo", "main.Foo.W")
Bar int //@mark(fBar, "Bar"), symbol("Bar", "Bar", "Field", "Foo", "main.Foo.Bar")
baz string //@symbol("baz", "baz", "Field", "Foo", "main.Foo.baz")
type Foo struct { //@symbol("Foo", "Foo", "Struct", "struct{...}", "Foo", "")
Quux //@symbol("Quux", "Quux", "Field", "Quux", "", "Foo")
W io.Writer //@symbol("W", "W", "Field", "io.Writer", "", "Foo")
Bar int //@symbol("Bar", "Bar", "Field", "int", "", "Foo")
baz string //@symbol("baz", "baz", "Field", "string", "", "Foo")
funcField func(int) int //@symbol("funcField", "funcField", "Field", "func(int) int", "", "Foo")
}
type Quux struct { //@symbol("Quux", "Quux", "Struct", "", "main.Quux")
X, Y float64 //@mark(qX, "X"), symbol("X", "X", "Field", "Quux", "main.X"), symbol("Y", "Y", "Field", "Quux", "main.Y")
type Quux struct { //@symbol("Quux", "Quux", "Struct", "struct{...}", "Quux", "")
X, Y float64 //@symbol("X", "X", "Field", "float64", "", "Quux"), symbol("Y", "Y", "Field", "float64", "", "Quux")
}
func (f Foo) Baz() string { //@symbol("(Foo).Baz", "Baz", "Method", "", "main.Foo.Baz")
type EmptyStruct struct{} //@symbol("EmptyStruct", "EmptyStruct", "Struct", "struct{}", "", "")
func (f Foo) Baz() string { //@symbol("(Foo).Baz", "Baz", "Method", "func() string", "", "")
return f.baz
}
func _() {}
func (q *Quux) Do() {} //@mark(qDo, "Do"), symbol("(*Quux).Do", "Do", "Method", "", "main.Quux.Do")
func main() { //@symbol("main", "main", "Function", "", "main.main")
func (q *Quux) Do() {} //@symbol("(*Quux).Do", "Do", "Method", "func()", "", "")
func main() { //@symbol("main", "main", "Function", "func()", "", "")
}
type Stringer interface { //@symbol("Stringer", "Stringer", "Interface", "", "main.Stringer")
String() string //@symbol("String", "String", "Method", "Stringer", "main.Stringer.String")
type Stringer interface { //@symbol("Stringer", "Stringer", "Interface", "interface{...}", "Stringer", "")
String() string //@symbol("String", "String", "Method", "func() string", "", "Stringer")
}
type ABer interface { //@mark(ABerInterface, "ABer"), symbol("ABer", "ABer", "Interface", "", "main.ABer")
B() //@symbol("B", "B", "Method", "ABer", "main.ABer.B")
A() string //@mark(ABerA, "A"), symbol("A", "A", "Method", "ABer", "main.ABer.A")
type ABer interface { //@symbol("ABer", "ABer", "Interface", "interface{...}", "ABer", "")
B() //@symbol("B", "B", "Method", "func()", "", "ABer")
A() string //@symbol("A", "A", "Method", "func() string", "", "ABer")
}
type WithEmbeddeds interface { //@symbol("WithEmbeddeds", "WithEmbeddeds", "Interface", "", "main.WithEmbeddeds")
Do() //@symbol("Do", "Do", "Method", "WithEmbeddeds", "main.WithEmbeddeds.Do")
ABer //@symbol("ABer", "ABer", "Interface", "WithEmbeddeds", "main.WithEmbeddeds.ABer")
io.Writer //@mark(ioWriter, "io.Writer"), symbol("io.Writer", "io.Writer", "Interface", "WithEmbeddeds", "main.WithEmbeddeds.Writer")
type WithEmbeddeds interface { //@symbol("WithEmbeddeds", "WithEmbeddeds", "Interface", "interface{...}", "WithEmbeddeds", "")
Do() //@symbol("Do", "Do", "Method", "func()", "", "WithEmbeddeds")
ABer //@symbol("ABer", "ABer", "Field", "ABer", "", "WithEmbeddeds")
io.Writer //@symbol("Writer", "Writer", "Field", "io.Writer", "", "WithEmbeddeds")
}
func Dunk() int { return 0 } //@symbol("Dunk", "Dunk", "Function", "", "main.Dunk")
type EmptyInterface interface{} //@symbol("EmptyInterface", "EmptyInterface", "Interface", "interface{}", "", "")
func dunk() {} //@symbol("dunk", "dunk", "Function", "", "main.dunk")
func Dunk() int { return 0 } //@symbol("Dunk", "Dunk", "Function", "func() int", "", "")
func dunk() {} //@symbol("dunk", "dunk", "Function", "func()", "", "")

View File

@ -1,31 +1,36 @@
-- symbols --
x Variable 9:5-9:6
y Constant 11:7-11:8
Number Number 13:6-13:12
Alias String 15:6-15:11
NumberAlias Number 17:6-17:17
Boolean Boolean 20:2-20:9
BoolAlias Boolean 21:2-21:11
Foo Struct 24:6-24:9
Bar Field 27:2-27:5
Quux Field 25:2-25:6
W Field 26:2-26:3
baz Field 28:2-28:5
Quux Struct 31:6-31:10
X Field 32:2-32:3
Y Field 32:5-32:6
(Foo).Baz Method 35:14-35:17
(*Quux).Do Method 41:16-41:18
main Function 43:6-43:10
Stringer Interface 47:6-47:14
String Method 48:2-48:8
ABer Interface 51:6-51:10
A Method 53:2-53:3
B Method 52:2-52:3
WithEmbeddeds Interface 56:6-56:19
ABer Interface 58:2-58:6
Do Method 57:2-57:4
io.Writer Interface 59:2-59:11
Dunk Function 62:6-62:10
dunk Function 64:6-64:10
x Variable 26:5-26:6
nested Variable 28:5-28:11
nestedField Field 29:2-29:13
y Constant 34:7-34:8
Number Class 36:6-36:12
Alias Class 38:6-38:11
NumberAlias Class 40:6-40:17
Boolean Class 43:2-43:9
BoolAlias Class 44:2-44:11
Foo Struct 47:6-47:9
Bar Field 50:2-50:5
Quux Field 48:2-48:6
W Field 49:2-49:3
baz Field 51:2-51:5
funcField Field 52:2-52:11
Quux Struct 55:6-55:10
X Field 56:2-56:3
Y Field 56:5-56:6
EmptyStruct Struct 59:6-59:17
(Foo).Baz Method 61:14-61:17
(*Quux).Do Method 67:16-67:18
main Function 69:6-69:10
Stringer Interface 72:6-72:14
String Method 73:2-73:8
ABer Interface 76:6-76:10
A Method 78:2-78:3
B Method 77:2-77:3
WithEmbeddeds Interface 81:6-81:19
ABer Field 83:2-83:6
Do Method 82:2-82:4
Writer Field 84:5-84:11
EmptyInterface Interface 87:6-87:20
Dunk Function 89:6-89:10
dunk Function 91:6-91:10

View File

@ -1,9 +1,9 @@
package a
var RandomGopherVariableA = "a" //@symbol("RandomGopherVariableA", "RandomGopherVariableA", "Variable", "", "a.RandomGopherVariableA")
var RandomGopherVariableA = "a"
const RandomGopherConstantA = "a" //@symbol("RandomGopherConstantA", "RandomGopherConstantA", "Constant", "", "a.RandomGopherConstantA")
const RandomGopherConstantA = "a"
const (
randomgopherinvariable = iota //@symbol("randomgopherinvariable", "randomgopherinvariable", "Constant", "", "a.randomgopherinvariable")
randomgopherinvariable = iota
)

View File

@ -1,5 +0,0 @@
-- symbols --
RandomGopherVariableA Variable 3:5-3:26
RandomGopherConstantA Constant 5:7-5:28
randomgopherinvariable Constant 8:2-8:24

View File

@ -1,3 +1,3 @@
package a
var RandomGopherTestVariableA = "a" //@symbol("RandomGopherTestVariableA", "RandomGopherTestVariableA", "Variable", "", "a.RandomGopherTestVariableA")
var RandomGopherTestVariableA = "a"

View File

@ -1,3 +0,0 @@
-- symbols --
RandomGopherTestVariableA Variable 3:5-3:30

View File

@ -1,3 +1,3 @@
package a_test
var RandomGopherXTestVariableA = "a" //@symbol("RandomGopherXTestVariableA", "RandomGopherXTestVariableA", "Variable", "", "a_test.RandomGopherXTestVariableA")
var RandomGopherXTestVariableA = "a"

View File

@ -1,3 +0,0 @@
-- symbols --
RandomGopherXTestVariableA Variable 3:5-3:31

View File

@ -1,7 +1,7 @@
package b
var RandomGopherVariableB = "b" //@symbol("RandomGopherVariableB", "RandomGopherVariableB", "Variable", "", "b.RandomGopherVariableB")
var RandomGopherVariableB = "b"
type RandomGopherStructB struct { //@symbol("RandomGopherStructB", "RandomGopherStructB", "Struct", "", "b.RandomGopherStructB")
Bar int //@mark(bBar, "Bar"), symbol("Bar", "Bar", "Field", "RandomGopherStructB", "b.RandomGopherStructB.Bar")
type RandomGopherStructB struct {
Bar int
}

View File

@ -1,5 +0,0 @@
-- symbols --
RandomGopherVariableB Variable 3:5-3:26
RandomGopherStructB Struct 5:6-5:25
Bar Field 6:2-6:5

View File

@ -86,8 +86,7 @@ type Highlights = map[span.Span][]span.Span
type References = map[span.Span][]span.Span
type Renames = map[span.Span]string
type PrepareRenames = map[span.Span]*source.PrepareItem
type Symbols = map[span.URI][]protocol.DocumentSymbol
type SymbolsChildren = map[string][]protocol.DocumentSymbol
type Symbols = map[span.URI][]*symbol
type SymbolInformation = map[span.Span]protocol.SymbolInformation
type InlayHints = []span.Span
type WorkspaceSymbols = map[WorkspaceSymbolsTestType]map[span.URI][]string
@ -125,8 +124,6 @@ type Data struct {
InlayHints InlayHints
PrepareRenames PrepareRenames
Symbols Symbols
symbolsChildren SymbolsChildren
symbolInformation SymbolInformation
WorkspaceSymbols WorkspaceSymbols
Signatures Signatures
Links Links
@ -250,6 +247,12 @@ type SuggestedFix struct {
ActionKind, Title string
}
// A symbol holds a DocumentSymbol along with its parent-child edge.
type symbol struct {
pSymbol protocol.DocumentSymbol
id, parentID string
}
type Golden struct {
Filename string
Archive *txtar.Archive
@ -328,8 +331,6 @@ func load(t testing.TB, mode string, dir string) *Data {
FunctionExtractions: make(FunctionExtractions),
MethodExtractions: make(MethodExtractions),
Symbols: make(Symbols),
symbolsChildren: make(SymbolsChildren),
symbolInformation: make(SymbolInformation),
WorkspaceSymbols: make(WorkspaceSymbols),
Signatures: make(Signatures),
Links: make(Links),
@ -504,12 +505,7 @@ func load(t testing.TB, mode string, dir string) *Data {
}); err != nil {
t.Fatal(err)
}
for _, symbols := range datum.Symbols {
for i := range symbols {
children := datum.symbolsChildren[symbols[i].Name]
symbols[i].Children = children
}
}
// Collect names for the entries that require golden files.
if err := datum.Exported.Expect(map[string]interface{}{
"godef": datum.collectDefinitionNames,
@ -847,10 +843,44 @@ func Run(t *testing.T, tests Tests, data *Data) {
t.Run("Symbols", func(t *testing.T) {
t.Helper()
for uri, expectedSymbols := range data.Symbols {
for uri, allSymbols := range data.Symbols {
byParent := make(map[string][]*symbol)
for _, sym := range allSymbols {
if sym.parentID != "" {
byParent[sym.parentID] = append(byParent[sym.parentID], sym)
}
}
// collectChildren does a depth-first traversal of the symbol tree,
// computing children of child nodes before returning to their parent.
// This is necessary as the Children field is slice of non-pointer types,
// and therefore we need to be careful to mutate children first before
// assigning them to their parent.
var collectChildren func(id string) []protocol.DocumentSymbol
collectChildren = func(id string) []protocol.DocumentSymbol {
children := byParent[id]
// delete from byParent before recursing, to ensure that
// collectChildren terminates even in the presence of cycles.
delete(byParent, id)
var result []protocol.DocumentSymbol
for _, child := range children {
child.pSymbol.Children = collectChildren(child.id)
result = append(result, child.pSymbol)
}
return result
}
var topLevel []protocol.DocumentSymbol
for _, sym := range allSymbols {
if sym.parentID == "" {
sym.pSymbol.Children = collectChildren(sym.id)
topLevel = append(topLevel, sym.pSymbol)
}
}
t.Run(uriName(uri), func(t *testing.T) {
t.Helper()
tests.Symbols(t, uri, expectedSymbols)
tests.Symbols(t, uri, topLevel)
})
}
})
@ -1356,36 +1386,32 @@ func (data *Data) collectPrepareRenames(src span.Span, rng span.Range, placehold
}
// collectSymbols is responsible for collecting @symbol annotations.
func (data *Data) collectSymbols(name string, spn span.Span, kind string, parentName string, siName string) {
func (data *Data) collectSymbols(name string, selectionRng span.Span, kind, detail, id, parentID string) {
// We don't set 'Range' here as it is difficult (impossible?) to express
// multi-line ranges in the packagestest framework.
uri := selectionRng.URI()
data.Symbols[uri] = append(data.Symbols[uri], &symbol{
pSymbol: protocol.DocumentSymbol{
Name: name,
Kind: protocol.ParseSymbolKind(kind),
SelectionRange: data.mustRange(selectionRng),
Detail: detail,
},
id: id,
parentID: parentID,
})
}
// mustRange converts spn into a protocol.Range, calling t.Fatal on any error.
func (data *Data) mustRange(spn span.Span) protocol.Range {
m, err := data.Mapper(spn.URI())
if err != nil {
data.t.Fatal(err)
}
rng, err := m.Range(spn)
if err != nil {
// TODO(rfindley): this can probably just be a panic, at which point we
// don't need to close over t.
data.t.Fatal(err)
}
sym := protocol.DocumentSymbol{
Name: name,
Kind: protocol.ParseSymbolKind(kind),
SelectionRange: rng,
}
if parentName == "" {
data.Symbols[spn.URI()] = append(data.Symbols[spn.URI()], sym)
} else {
data.symbolsChildren[parentName] = append(data.symbolsChildren[parentName], sym)
}
// Reuse @symbol in the workspace symbols tests.
si := protocol.SymbolInformation{
Name: siName,
Kind: sym.Kind,
Location: protocol.Location{
URI: protocol.URIFromSpanURI(spn.URI()),
Range: sym.SelectionRange,
},
}
data.symbolInformation[spn] = si
return rng
}
func (data *Data) collectWorkspaceSymbols(typ WorkspaceSymbolsTestType) func(*expect.Note, string) {

View File

@ -14,13 +14,12 @@ import (
"sort"
"strconv"
"strings"
"testing"
"golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/diff/myers"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/lsp/source/completion"
"golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/diff/myers"
"golang.org/x/tools/internal/span"
)
@ -66,50 +65,6 @@ func DiffLinks(mapper *protocol.ColumnMapper, wantLinks []Link, gotLinks []proto
return ""
}
// DiffSymbols prints the diff between expected and actual symbols test results.
func DiffSymbols(t *testing.T, uri span.URI, want, got []protocol.DocumentSymbol) string {
sort.Slice(want, func(i, j int) bool { return want[i].Name < want[j].Name })
sort.Slice(got, func(i, j int) bool { return got[i].Name < got[j].Name })
if len(got) != len(want) {
return summarizeSymbols(-1, want, got, "different lengths got %v want %v", len(got), len(want))
}
for i, w := range want {
g := got[i]
if w.Name != g.Name {
return summarizeSymbols(i, want, got, "incorrect name got %v want %v", g.Name, w.Name)
}
if w.Kind != g.Kind {
return summarizeSymbols(i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind)
}
if protocol.CompareRange(w.SelectionRange, g.SelectionRange) != 0 {
return summarizeSymbols(i, want, got, "incorrect span got %v want %v", g.SelectionRange, w.SelectionRange)
}
if msg := DiffSymbols(t, uri, w.Children, g.Children); msg != "" {
return fmt.Sprintf("children of %s: %s", w.Name, msg)
}
}
return ""
}
func summarizeSymbols(i int, want, got []protocol.DocumentSymbol, reason string, args ...interface{}) string {
msg := &bytes.Buffer{}
fmt.Fprint(msg, "document symbols failed")
if i >= 0 {
fmt.Fprintf(msg, " at %d", i)
}
fmt.Fprint(msg, " because of ")
fmt.Fprintf(msg, reason, args...)
fmt.Fprint(msg, ":\nexpected:\n")
for _, s := range want {
fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange)
}
fmt.Fprintf(msg, "got:\n")
for _, s := range got {
fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange)
}
return msg.String()
}
// DiffDiagnostics prints the diff between expected and actual diagnostics test
// results. If the sole expectation is "no_diagnostics", the check is suppressed.
// The Message field of each want element must be a regular expression.