-
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathreference.go
More file actions
291 lines (263 loc) · 8.47 KB
/
Copy pathreference.go
File metadata and controls
291 lines (263 loc) · 8.47 KB
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
// Copyright 2025 TypeFox GmbH
// This program and the accompanying materials are made available under the
// terms of the MIT License, which is available in the project root.
package fastbelt
import (
"context"
"iter"
"reflect"
"slices"
"sync"
"sync/atomic"
"typefox.dev/fastbelt/util/collections"
"typefox.dev/fastbelt/util/extiter"
)
// Reference represents a reference to another AST node of type T.
//
// Resolving a reference is thread safe.
// The resolution is triggered when [Reference.Ref], [Reference.RefNode] or [Reference.Resolve]
// are called for the first time.
type Reference[T AstNode] struct {
unit StringUnit
owner AstNode
description *SymbolDescription
err *ReferenceError
ref T
mu sync.Mutex
getter ReferenceGetter[T]
resolved atomic.Bool
}
// Reset clears all cached resolution results so the reference can be resolved again.
func (r *Reference[T]) Reset() {
if r == nil {
return
}
r.mu.Lock()
defer r.mu.Unlock()
r.resolved.Store(false)
r.description = nil
r.err = nil
var zero T
r.ref = zero
}
// Unit returns the [StringUnit] that contains the textual reference.
func (r *Reference[T]) Unit() StringUnit {
if r == nil {
return nil
}
return r.unit
}
// Text returns the textual value of the reference, or an empty string if unavailable.
func (r *Reference[T]) Text() string {
if r == nil || r.unit == nil {
return ""
}
return r.unit.String()
}
// Owner returns the AST node that owns this reference.
func (r *Reference[T]) Owner() AstNode {
if r == nil {
return nil
}
return r.owner
}
// Description returns the resolved symbol description, or nil when unresolved.
func (r *Reference[T]) Description() *SymbolDescription {
if r == nil {
return nil
}
return r.description
}
// RefNode returns the resolved target node as [AstNode].
func (r *Reference[T]) RefNode(ctx context.Context) AstNode {
return r.Ref(ctx)
}
// Ref resolves the reference and returns the typed target node.
// It returns the zero value of T when resolution fails.
func (r *Reference[T]) Ref(ctx context.Context) T {
var zero T
if r == nil {
return zero
}
r.Resolve(ctx)
return r.ref
}
// Error returns the resolution error for this reference, if any.
func (r *Reference[T]) Error() *ReferenceError {
if r == nil {
return nil
}
return r.err
}
// Range returns the text range of the reference in the source document.
func (r *Reference[T]) TextRange() TextRange {
return r.unit.TextRange()
}
// Resolve resolves the reference exactly once for this instance.
// It is safe for concurrent use.
func (r *Reference[T]) Resolve(ctx context.Context) {
// Fast path: check if already resolved without locking
if r == nil || r.resolved.Load() {
return
}
// Slow path (outlined so that the fast path can be inlined)
r.resolveSlow(ctx)
}
func (r *Reference[T]) resolveSlow(ctx context.Context) {
// We can use the context to detect cyclic reference resolution attempts
// We are allowed to do this outside of the mutex lock because context is immutable
if ctx.Value(r) != nil {
// Note that we write directly to r.err without locking here
// This is safe, because the reference is already locked by the caller
// Attempting to lock it again would cause a deadlock anyway
r.err = NewReferenceError("Cyclic reference resolution detected")
// Return directly, do not set the resolved flag
return
}
r.mu.Lock()
defer r.mu.Unlock()
if r.resolved.Load() {
// Another goroutine might have resolved it while we were waiting for the lock
return
}
newCtx := context.WithValue(ctx, r, true)
desc, e := r.getter(newCtx, r)
r.description = desc
if r.err == nil {
// Do not overwrite existing errors
r.err = e
}
if desc != nil {
if node, ok := desc.Node.(T); ok {
r.ref = node
} else if r.err == nil {
expectedType := reflect.TypeFor[T]().String()
actualType := "nil"
if desc.Node != nil {
actualType = reflect.TypeOf(desc.Node).String()
}
r.err = NewReferenceError("Reference resolution type mismatch: expected " + expectedType + ", got " + actualType)
}
}
r.resolved.Store(true)
}
// NewReference creates a lazy, typed reference owned by owner and backed by getter.
func NewReference[T AstNode](owner AstNode, unit StringUnit, getter ReferenceGetter[T]) *Reference[T] {
return &Reference[T]{
owner: owner,
unit: unit,
getter: getter,
}
}
// UntypedReference is an untyped representation of a [Reference] that can be used when the target
// type is not known at compile time.
//
// Used throughout the fastbelt codebase to generically deal with different references.
type UntypedReference interface {
Owner() AstNode
Description() *SymbolDescription
RefNode(ctx context.Context) AstNode
Resolve(ctx context.Context)
Reset()
Error() *ReferenceError
Unit() StringUnit
TextRange() TextRange
Text() string
}
// ReferenceGetter resolves a [Reference] and returns its symbol description.
// The returned [ReferenceError] is stored on the reference when not nil.
type ReferenceGetter[T AstNode] func(context.Context, *Reference[T]) (*SymbolDescription, *ReferenceError)
// Computes the reference that this token represents.
// Returns nil if the token does not represent a reference.
func ReferenceOfToken(token *Token) UntypedReference {
owner := token.Element
rng := token.TextRange()
if composite, ok := owner.(CompositeNode); ok {
// If the token is part of a composite node,
// we first have to retrieve its parent node, which is the actual owner of the reference.
owner = composite.Container()
rng = composite.TextRange()
}
if owner == nil {
return nil
}
var ref UntypedReference = nil
// We don't have a direct reference from token -> reference, so we need to search for it
// We have to iterate over all references of the owner node
// This might seem inefficient, but in practice the number of references per node is usually very small
// Also, we only do this in select LSP requests, so the performance impact is negligible
for ur := range References(owner) {
// Simply compare the text indices to find the matching reference
if ur.TextRange() == rng {
ref = ur
break
}
}
return ref
}
// ReferenceDescription describes one concrete use of a symbol in source text.
type ReferenceDescription struct {
// SourceNode is the node that contains the reference.
SourceNode AstNode
// TargetNode is the node that is being referenced.
TargetNode AstNode
// Range is the text range of the reference in the source document (usually the symbol name).
Range TextRange
}
// NewReferenceDescription creates a [ReferenceDescription] for a source-to-target link.
func NewReferenceDescription(source, target AstNode, rng TextRange) *ReferenceDescription {
return &ReferenceDescription{
SourceNode: source,
TargetNode: target,
Range: rng,
}
}
// SourceURI returns the URI of the document containing the source node, or nil if not available.
func (d *ReferenceDescription) SourceURI() URI {
if d.SourceNode != nil {
doc := d.SourceNode.Document()
if doc != nil {
return doc.URI
}
}
return nil
}
// TargetURI returns the URI of the document containing the target node, or nil if not available.
func (d *ReferenceDescription) TargetURI() URI {
if d.TargetNode != nil {
doc := d.TargetNode.Document()
if doc != nil {
return doc.URI
}
}
return nil
}
// ReferenceDescriptions is a collection of reference descriptions for a document, indexed by target node.
// It allows efficient retrieval of all references to a given target node.
type ReferenceDescriptions interface {
// All returns an iterator over all reference descriptions in the document.
All() iter.Seq[*ReferenceDescription]
// ForTarget returns an iterator over all reference descriptions that point to the given target node.
ForTarget(target AstNode) iter.Seq[*ReferenceDescription]
}
type referenceDescriptions struct {
descriptions collections.MultiMap[AstNode, *ReferenceDescription]
}
func (d *referenceDescriptions) All() iter.Seq[*ReferenceDescription] {
if d == nil {
return extiter.Empty[*ReferenceDescription]()
}
return d.descriptions.Values()
}
func (d *referenceDescriptions) ForTarget(target AstNode) iter.Seq[*ReferenceDescription] {
if d == nil {
return extiter.Empty[*ReferenceDescription]()
}
return slices.Values(d.descriptions.Get(target))
}
// NewReferenceDescriptionsFromMap wraps precomputed descriptions keyed by target node.
func NewReferenceDescriptionsFromMap(descriptions collections.MultiMap[AstNode, *ReferenceDescription]) ReferenceDescriptions {
return &referenceDescriptions{
descriptions: descriptions,
}
}