This repository has been archived by the owner on Sep 26, 2023. It is now read-only.
/
package_data_cache.go
312 lines (271 loc) · 10 KB
/
package_data_cache.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
package indexer
import (
"go/ast"
"go/token"
"go/types"
"strings"
"sync"
"golang.org/x/tools/go/packages"
)
// PackageDataCache is a cache of hover text and enclosing type identifiers by file and token position.
type PackageDataCache struct {
m sync.RWMutex
packageData map[*packages.Package]*PackageData
}
// NewPackageDataCache creates a new empty PackageDataCache.
func NewPackageDataCache() *PackageDataCache {
return &PackageDataCache{
packageData: map[*packages.Package]*PackageData{},
}
}
// Text will return the hover text extracted from the given package for the symbol at the given position.
// This method will parse the package if the package results haven't been previously calculated or have been
// evicted from the cache.
func (l *PackageDataCache) Text(p *packages.Package, position token.Pos) string {
return extractHoverText(l.getPackageData(p).HoverText[position])
}
// MonikerPath will return the names of enclosing nodes extracted form the given package for the symbol at
// the given position. This method will parse the package if the package results haven't been previously
// calculated or have been evicted from the cache.
func (l *PackageDataCache) MonikerPath(p *packages.Package, position token.Pos) []string {
return l.getPackageData(p).MonikerPaths[position]
}
// Stats returns a PackageDataCacheStats object with the number of unique packages traversed.
func (l *PackageDataCache) Stats() PackageDataCacheStats {
return PackageDataCacheStats{
NumPks: uint(len(l.packageData)),
}
}
// getPackageData will return a package data value for the given package. If the data for this package has not
// already been loaded, it will be loaded immediately. This method will block until the package data has been
// completely loaded before returning to the caller.
func (l *PackageDataCache) getPackageData(p *packages.Package) *PackageData {
data := l.getPackageDataRaw(p)
data.load(p)
return data
}
// getPackageDataRaw will return the package data value for the given package or create one of it doesn't exist.
// It is not guaranteed that the value has bene loaded, so load (which is idempotent) should be called before use.
func (l *PackageDataCache) getPackageDataRaw(p *packages.Package) *PackageData {
l.m.RLock()
data, ok := l.packageData[p]
l.m.RUnlock()
if ok {
return data
}
l.m.Lock()
defer l.m.Unlock()
if data, ok = l.packageData[p]; ok {
return data
}
data = &PackageData{
HoverText: map[token.Pos]ast.Node{},
MonikerPaths: map[token.Pos][]string{},
}
l.packageData[p] = data
return data
}
// PackageData is a cache of hover text and moniker paths by token position within a package.
type PackageData struct {
once sync.Once
HoverText map[token.Pos]ast.Node
MonikerPaths map[token.Pos][]string
}
// load will parse the package and populate the maps of hover text and moniker paths. This method is
// idempotent. All calls to this method will block until the first call has completed.
func (data *PackageData) load(p *packages.Package) {
data.once.Do(func() {
definitionPositions, fieldPositions := interestingPositions(p)
for _, root := range p.Syntax {
visit(root, definitionPositions, fieldPositions, data.HoverText, data.MonikerPaths, nil, nil)
}
})
}
// interestingPositions returns a pair of maps whose keys are token positions for which we want values
// in the package data cache's hoverText and monikerPaths maps. Determining which types of types we will
// query for this data and populating values only for those nodes saves a lot of resident memory.
func interestingPositions(p *packages.Package) (map[token.Pos]struct{}, map[token.Pos]struct{}) {
hoverTextPositions := map[token.Pos]struct{}{}
monikerPathPositions := map[token.Pos]struct{}{}
for _, obj := range p.TypesInfo.Defs {
if shouldHaveHoverText(obj) {
hoverTextPositions[obj.Pos()] = struct{}{}
}
if isField(obj) {
monikerPathPositions[obj.Pos()] = struct{}{}
}
}
for _, obj := range p.TypesInfo.Uses {
if isField(obj) {
monikerPathPositions[obj.Pos()] = struct{}{}
}
}
return hoverTextPositions, monikerPathPositions
}
// visit walks the AST for a file and assigns hover text and a moniker path to interesting positions.
// A position's hover text is the comment associated with the deepest node that encloses the position.
// A position's moniker path is the name of the object prefixed with the names of the containers that
// enclose that position.
func visit(
node ast.Node, // Current node
hoverTextPositions map[token.Pos]struct{}, // Positions for hover text assignment
monikerPathPositions map[token.Pos]struct{}, // Positions for moniker paths assignment
hoverTextMap map[token.Pos]ast.Node, // Target hover text map
monikerPathMap map[token.Pos][]string, // Target moniker path map
nodeWithHoverText ast.Node, // The ancestor node with non-empty hover text (if any)
monikerPath []string, // The moniker path constructed up to this node
) {
if canExtractHoverText(node) {
// If we have hover text replace whatever ancestor node we might
// have. We have more relevant text on this node, so just use that.
nodeWithHoverText = node
}
// If we're a field or type, update our moniker path
newMonikerPath := updateMonikerPath(monikerPath, node)
for _, child := range childrenOf(node) {
visit(
child,
hoverTextPositions,
monikerPathPositions,
hoverTextMap,
monikerPathMap,
chooseNodeWithHoverText(node, child),
newMonikerPath,
)
}
if _, ok := hoverTextPositions[node.Pos()]; ok {
hoverTextMap[node.Pos()] = nodeWithHoverText
}
if _, ok := monikerPathPositions[node.Pos()]; ok {
monikerPathMap[node.Pos()] = newMonikerPath
}
}
// updateMonikerPath returns the given slice plus the name of the given node if it has a name that
// can uniquely identify it along a path of nodes to the root of the file (an enclosing type).
// Otherwise, the given slice is returned unchanged. This function does not modify the input slice.
func updateMonikerPath(monikerPath []string, node ast.Node) []string {
switch q := node.(type) {
case *ast.Field:
// Handle field name/names
if len(q.Names) > 0 {
// Handle things like `a, b, c T`. If there are multiple names we just default to the first
// one as each field must belong on at most one moniker path. This is sub-optimal and
// should be addressed in https://github.com/sourcegraph/lsif-go/issues/154.
return addString(monikerPath, q.Names[0].String())
}
// Handle embedded types
if name, ok := q.Type.(*ast.Ident); ok {
return addString(monikerPath, name.Name)
}
// Handle embedded types that are selectors, like http.Client
if selector, ok := q.Type.(*ast.SelectorExpr); ok {
return addString(monikerPath, selector.Sel.Name)
}
case *ast.TypeSpec:
// Add the top-level type spec (e.g. `type X struct` and `type Y interface`)
return addString(monikerPath, q.Name.String())
}
return monikerPath
}
// addString creates a new slice composed of the element of slice plus the given value.
// This function does not modify the input slice.
func addString(slice []string, value string) []string {
newSlice := make([]string, len(slice), len(slice)+1)
copy(newSlice, slice)
newSlice = append(newSlice, value)
return newSlice
}
// childrenOf returns the direct non-nil children of ast.Node n.
func childrenOf(n ast.Node) (children []ast.Node) {
ast.Inspect(n, func(node ast.Node) bool {
if node == n {
return true
}
if node != nil {
children = append(children, node)
}
return false
})
return children
}
// isField returns true if the given object is a field.
func isField(obj ObjectLike) bool {
if v, ok := obj.(*types.Var); ok && v.IsField() {
return true
}
return false
}
// shouldHaveHoverText returns true if the object is a type for which we should store hover text. This
// is similar but distinct from the set of types from which we _extract_ hover text. See canExtractHoverText
// for those types. This function returns true for the set of objects for which we actually call the methods
// findHoverContents or findExternalHoverContents (see hover.go).
func shouldHaveHoverText(obj ObjectLike) bool {
switch obj.(type) {
case *types.Const:
return true
case *types.Func:
return true
case *types.Label:
return true
case *types.TypeName:
return true
case *types.Var:
return true
}
return false
}
// extractHoverText returns the comments attached to the given node.
func extractHoverText(node ast.Node) string {
switch v := node.(type) {
case *ast.FuncDecl:
return v.Doc.Text()
case *ast.GenDecl:
return v.Doc.Text()
case *ast.TypeSpec:
return v.Doc.Text()
case *ast.ValueSpec:
return v.Doc.Text()
case *ast.Field:
return strings.TrimSpace(v.Doc.Text() + "\n" + v.Comment.Text())
}
return ""
}
// canExtractHoverText returns true if the node has non-empty comments extractable by extractHoverText.
func canExtractHoverText(node ast.Node) bool {
switch v := node.(type) {
case *ast.FuncDecl:
return !commentGroupsEmpty(v.Doc)
case *ast.GenDecl:
return !commentGroupsEmpty(v.Doc)
case *ast.TypeSpec:
return !commentGroupsEmpty(v.Doc)
case *ast.ValueSpec:
return !commentGroupsEmpty(v.Doc)
case *ast.Field:
return !commentGroupsEmpty(v.Doc, v.Comment)
}
return false
}
// commentGroupsEmpty returns true if all of the given comments groups are empty.
func commentGroupsEmpty(gs ...*ast.CommentGroup) bool {
for _, g := range gs {
if g != nil && len(g.List) > 0 {
return false
}
}
return true
}
// chooseNodeWithHoverText returns the parent node if the relationship between the parent and child is
// one in which comments can be reasonably shared. This will return a nil node for most relationships,
// except things like (1) FuncDecl -> Ident, in which case we want to store the function's comment
// in the ident, or (2) GenDecl -> TypeSpec, in which case we want to store the generic declaration's
// comments if the type node doesn't have any directly attached to it.
func chooseNodeWithHoverText(parent, child ast.Node) ast.Node {
if _, ok := parent.(*ast.GenDecl); ok {
return parent
}
if _, ok := child.(*ast.Ident); ok {
return parent
}
return nil
}