Skip to content

Commit edff653

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/server: TypeHierarchy support
This CL adds basic support for the LSP Type Hierarchy feature. There are three new RPCs: - PrepareTypeHierarchy asks for a description of the currently selected text, which must be a reference to a type name. - Supertypes and Subtypes ask for the items related by the subtyping relation, using the same machinery as Implementations by method sets, which has been factored to deliver a concurrent stream of results at a higher level then just protocol.Location. Unlike Implementations, Type Hierarchy does not report relationships between func types and FuncDecl/FuncLit/RangeStmt. The names of types are now saved in the methodsets index. The marker test framework has been extended with @{super,sub}types markers. This CL also sets us up to start reporting interface/interface relationships (golang/go#68641), which are especially desirable in the Type Hierarchy viewer; but that behavior change will be left for a follow-up. + tests, docs, relnotes Fixes golang/go#72142 Change-Id: Id60c9f447e938ac7e34262522ccd79bd54d90fc5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/663055 Auto-Submit: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Commit-Queue: Alan Donovan <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent a99a1c3 commit edff653

File tree

13 files changed

+573
-137
lines changed

13 files changed

+573
-137
lines changed

gopls/doc/features/navigation.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ Interfaces and concrete types are matched using method sets:
9494
location of the declaration of each type that implements
9595
the interface.
9696
- When invoked on a **concrete type**,
97-
it returns the locations of the matching interface types.
97+
it returns the locations of the matching interface types.
9898
- When invoked on an **interface method**, it returns the corresponding
9999
methods of the types that satisfy the interface.
100100
- When invoked on a **concrete method**,
@@ -282,3 +282,38 @@ Client support:
282282
- **VS Code**: `Show Call Hierarchy` menu item (`⌥⇧H`) opens [Call hierarchy view](https://code.visualstudio.com/docs/cpp/cpp-ide#_call-hierarchy) (note: docs refer to C++ but the idea is the same for Go).
283283
- **Emacs + eglot**: Not standard; install with `(package-vc-install "https://github.com/dolmens/eglot-hierarchy")`. Use `M-x eglot-hierarchy-call-hierarchy` to show the direct incoming calls to the selected function; use a prefix argument (`C-u`) to show the direct outgoing calls. There is no way to expand the tree.
284284
- **CLI**: `gopls call_hierarchy file.go:#offset` shows outgoing and incoming calls.
285+
286+
287+
## Type Hierarchy
288+
289+
The LSP TypeHierarchy mechanism consists of three queries that
290+
together enable clients to present a hierarchical view of a portion of
291+
the subtyping relation over named types.
292+
293+
- [`textDocument/prepareTypeHierarchy`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#textDocument_prepareTypeHierarchy) returns an [item](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#typeHierarchyItem) describing the named type at the current position;
294+
- [`typeHierarchyItem/subtypes`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#typeHierarchy_subtypes) returns the set of subtypes of the selected (interface) type; and
295+
- [`typeHierarchy/supertypes`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#typeHierarchy_supertypes) returns the set of supertypes (interface types) of the selected type.
296+
297+
Invoke the command while selecting the name of a type.
298+
299+
As with an Implementation query, a type hierarchy query reports
300+
function-local types only within the same package as the query type.
301+
Also the result does not include alias types, only defined types.
302+
303+
<!--
304+
The screenshot below shows ...
305+
TODO: screenshot, but wait till #68641 is fixed.
306+
<img title="Subtypes of io.Reader" src="../assets/subtypes.png" width="640">
307+
-->
308+
309+
Caveats:
310+
311+
- The type hierarchy supports only named types and their assignability
312+
relation. By contrast, the Implementations request also reports the
313+
relation between unnamed `func` types and function declarations,
314+
function literals, and dynamic calls of values of those types.
315+
316+
Client support:
317+
- **VS Code**: `Show Type Hierarchy` menu item opens [Type hierarchy view](https://code.visualstudio.com/docs/java/java-editing#_type-hierarchy) (note: docs refer to Java but the idea is the same for Go).
318+
- **Emacs + eglot**: Support added in March 2025. Use `M-x eglot-show-call-hierarchy`.
319+
- **CLI**: not yet supported.

gopls/doc/release/v0.19.0.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ and queries using signatures should be invoked on a `func` or `(` token.
3333
Only the local (same-package) algorithm is currently supported.
3434
TODO: implement global.
3535

36+
## Support for Type Hierarchy
37+
38+
<!-- golang/go#72142 -->
39+
40+
Gopls now implements the three LSP methods related to the Type
41+
Hierarchy viewer: `textDocument/prepareTypeHierarchy`,
42+
`typeHierarchy/supertypes`, `typeHierarchy/subtypes`.
43+
44+
In VS Code, select "Show Type Hierarchy" from the context menu
45+
to see a tree widget displaying all the supertypes or subtypes
46+
of the selected named type.
47+
48+
TODO: screenshot, but wait till #68641 is fixed.
49+
3650

3751
## "Eliminate dot import" code action
3852

gopls/internal/cache/methodsets/methodsets.go

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import (
5151
"sync/atomic"
5252

5353
"golang.org/x/tools/go/types/objectpath"
54+
"golang.org/x/tools/gopls/internal/cache/metadata"
5455
"golang.org/x/tools/gopls/internal/util/bug"
5556
"golang.org/x/tools/gopls/internal/util/fingerprint"
5657
"golang.org/x/tools/gopls/internal/util/frob"
@@ -62,14 +63,15 @@ import (
6263
// types in a package in a form that permits assignability queries
6364
// without the type checker.
6465
type Index struct {
65-
pkg gobPackage
66+
pkg gobPackage
67+
PkgPath metadata.PackagePath
6668
}
6769

6870
// Decode decodes the given gob-encoded data as an Index.
69-
func Decode(data []byte) *Index {
71+
func Decode(pkgpath metadata.PackagePath, data []byte) *Index {
7072
var pkg gobPackage
7173
packageCodec.Decode(data, &pkg)
72-
return &Index{pkg}
74+
return &Index{pkg: pkg, PkgPath: pkgpath}
7375
}
7476

7577
// Encode encodes the receiver as gob-encoded data.
@@ -110,36 +112,75 @@ func KeyOf(t types.Type) (Key, bool) {
110112

111113
// A Result reports a matching type or method in a method-set search.
112114
type Result struct {
113-
Location Location // location of the type or method
115+
TypeName string // name of the named type
116+
IsInterface bool // matched type (or method) is abstract
117+
Location Location // location of the type or method
114118

115119
// methods only:
116120
PkgPath string // path of declaring package (may differ due to embedding)
117121
ObjectPath objectpath.Path // path of method within declaring package
118122
}
119123

120-
// Search reports each type that implements (or is implemented by) the
121-
// type that produced the search key. If methodID is nonempty, only
122-
// that method of each type is reported.
124+
// TypeRelation indicates the direction of subtyping relation,
125+
// if any, between two types.
126+
//
127+
// It is a bitset, so that clients of Implementations may use
128+
// Supertype|Subtype to request an undirected match.
129+
type TypeRelation int8
130+
131+
const (
132+
Supertype TypeRelation = 0x1
133+
Subtype TypeRelation = 0x2
134+
)
135+
136+
// Search reports each type that implements (Supertype ∈ want) or is
137+
// implemented by (Subtype ∈ want) the type that produced the search key.
138+
//
139+
// If method is non-nil, only that method of each type is reported.
123140
//
124141
// The result does not include the error.Error method.
125142
// TODO(adonovan): give this special case a more systematic treatment.
126-
func (index *Index) Search(key Key, method *types.Func) []Result {
143+
func (index *Index) Search(key Key, want TypeRelation, method *types.Func) []Result {
127144
var results []Result
128145
for _, candidate := range index.pkg.MethodSets {
129-
// Traditionally this feature doesn't report
130-
// interface/interface elements of the relation.
131-
// I think that's a mistake.
132-
// TODO(adonovan): UX: change it, here and in the local implementation.
146+
// The historical behavior enshrined by this
147+
// function rejects cases where both are
148+
// (nontrivial) interface types, but this is
149+
// useful information; see #68641 and CL 619719.
150+
// TODO(adonovan): rescind this policy choice,
151+
// and report I/I relationships,
152+
// by deleting this continue statement.
153+
// (It is also necessary to remove self-matches.)
154+
//
155+
// The same question appears in the local algorithm (implementations).
133156
if candidate.IsInterface && key.mset.IsInterface {
134157
continue
135158
}
136159

137-
if !implements(candidate, key.mset) && !implements(key.mset, candidate) {
160+
// Test the direction of the relation.
161+
// The client may request either direction or both
162+
// (e.g. when the client is References),
163+
// and the Result reports each test independently;
164+
// both tests succeed when comparing identical
165+
// interface types.
166+
var got TypeRelation
167+
if want&Subtype != 0 && implements(candidate, key.mset) {
168+
got |= Subtype
169+
}
170+
if want&Supertype != 0 && implements(key.mset, candidate) {
171+
got |= Supertype
172+
}
173+
if got == 0 {
138174
continue
139175
}
140176

177+
typeName := index.pkg.Strings[candidate.TypeName]
141178
if method == nil {
142-
results = append(results, Result{Location: index.location(candidate.Posn)})
179+
results = append(results, Result{
180+
TypeName: typeName,
181+
IsInterface: candidate.IsInterface,
182+
Location: index.location(candidate.Posn),
183+
})
143184
} else {
144185
for _, m := range candidate.Methods {
145186
if m.ID == method.Id() {
@@ -154,9 +195,11 @@ func (index *Index) Search(key Key, method *types.Func) []Result {
154195
}
155196

156197
results = append(results, Result{
157-
Location: index.location(m.Posn),
158-
PkgPath: index.pkg.Strings[m.PkgPath],
159-
ObjectPath: objectpath.Path(index.pkg.Strings[m.ObjectPath]),
198+
TypeName: typeName,
199+
IsInterface: candidate.IsInterface,
200+
Location: index.location(m.Posn),
201+
PkgPath: index.pkg.Strings[m.PkgPath],
202+
ObjectPath: objectpath.Path(index.pkg.Strings[m.ObjectPath]),
160203
})
161204
break
162205
}
@@ -285,14 +328,18 @@ func (b *indexBuilder) build(fset *token.FileSet, pkg *types.Package) *Index {
285328
for _, name := range scope.Names() {
286329
if tname, ok := scope.Lookup(name).(*types.TypeName); ok && !tname.IsAlias() {
287330
if mset := methodSetInfo(tname.Type(), setIndexInfo); mset.Mask != 0 {
331+
mset.TypeName = b.string(name)
288332
mset.Posn = objectPos(tname)
289333
// Only record types with non-trivial method sets.
290334
b.MethodSets = append(b.MethodSets, mset)
291335
}
292336
}
293337
}
294338

295-
return &Index{pkg: b.gobPackage}
339+
return &Index{
340+
pkg: b.gobPackage,
341+
PkgPath: metadata.PackagePath(pkg.Path()),
342+
}
296343
}
297344

298345
// string returns a small integer that encodes the string.
@@ -370,6 +417,7 @@ type gobPackage struct {
370417

371418
// A gobMethodSet records the method set of a single type.
372419
type gobMethodSet struct {
420+
TypeName int // name (string index) of the package-level type
373421
Posn gobPosition
374422
IsInterface bool
375423
Tricky bool // at least one method is tricky; fingerprint must be parsed + unified

gopls/internal/cache/snapshot.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ func (s *Snapshot) MethodSets(ctx context.Context, ids ...PackageID) ([]*methods
608608
pre := func(i int, ph *packageHandle) bool {
609609
data, err := filecache.Get(methodSetsKind, ph.key)
610610
if err == nil { // hit
611-
indexes[i] = methodsets.Decode(data)
611+
indexes[i] = methodsets.Decode(ph.mp.PkgPath, data)
612612
return false
613613
} else if err != filecache.ErrNotFound {
614614
event.Error(ctx, "reading methodsets from filecache", err)

0 commit comments

Comments
 (0)