-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrelocation.rb
504 lines (419 loc) · 15.1 KB
/
relocation.rb
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
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
# frozen_string_literal: true
module Prism
# Prism parses deterministically for the same input. This provides a nice
# property that is exposed through the #node_id API on nodes. Effectively this
# means that for the same input, these values will remain consistent every
# time the source is parsed. This means we can reparse the source same with a
# #node_id value and find the exact same node again.
#
# The Relocation module provides an API around this property. It allows you to
# "save" nodes and locations using a minimal amount of memory (just the
# node_id and a field identifier) and then reify them later.
module Relocation
# An entry in a repository that will lazily reify its values when they are
# first accessed.
class Entry
# Raised if a value that could potentially be on an entry is missing
# because it was either not configured on the repository or it has not yet
# been fetched.
class MissingValueError < StandardError
end
# Initialize a new entry with the given repository.
def initialize(repository)
@repository = repository
@values = nil
end
# Fetch the filepath of the value.
def filepath
fetch_value(:filepath)
end
# Fetch the start line of the value.
def start_line
fetch_value(:start_line)
end
# Fetch the end line of the value.
def end_line
fetch_value(:end_line)
end
# Fetch the start byte offset of the value.
def start_offset
fetch_value(:start_offset)
end
# Fetch the end byte offset of the value.
def end_offset
fetch_value(:end_offset)
end
# Fetch the start character offset of the value.
def start_character_offset
fetch_value(:start_character_offset)
end
# Fetch the end character offset of the value.
def end_character_offset
fetch_value(:end_character_offset)
end
# Fetch the start code units offset of the value, for the encoding that
# was configured on the repository.
def start_code_units_offset
fetch_value(:start_code_units_offset)
end
# Fetch the end code units offset of the value, for the encoding that was
# configured on the repository.
def end_code_units_offset
fetch_value(:end_code_units_offset)
end
# Fetch the start byte column of the value.
def start_column
fetch_value(:start_column)
end
# Fetch the end byte column of the value.
def end_column
fetch_value(:end_column)
end
# Fetch the start character column of the value.
def start_character_column
fetch_value(:start_character_column)
end
# Fetch the end character column of the value.
def end_character_column
fetch_value(:end_character_column)
end
# Fetch the start code units column of the value, for the encoding that
# was configured on the repository.
def start_code_units_column
fetch_value(:start_code_units_column)
end
# Fetch the end code units column of the value, for the encoding that was
# configured on the repository.
def end_code_units_column
fetch_value(:end_code_units_column)
end
# Fetch the leading comments of the value.
def leading_comments
fetch_value(:leading_comments)
end
# Fetch the trailing comments of the value.
def trailing_comments
fetch_value(:trailing_comments)
end
# Fetch the leading and trailing comments of the value.
def comments
leading_comments.concat(trailing_comments)
end
# Reify the values on this entry with the given values. This is an
# internal-only API that is called from the repository when it is time to
# reify the values.
def reify!(values) # :nodoc:
@repository = nil
@values = values
end
private
# Fetch a value from the entry, raising an error if it is missing.
def fetch_value(name)
values.fetch(name) do
raise MissingValueError, "No value for #{name}, make sure the " \
"repository has been properly configured"
end
end
# Return the values from the repository, reifying them if necessary.
def values
@values || (@repository.reify!; @values)
end
end
# Represents the source of a repository that will be reparsed.
class Source
# The value that will need to be reparsed.
attr_reader :value
# Initialize the source with the given value.
def initialize(value)
@value = value
end
# Reparse the value and return the parse result.
def result
raise NotImplementedError, "Subclasses must implement #result"
end
# Create a code units cache for the given encoding.
def code_units_cache(encoding)
result.code_units_cache(encoding)
end
end
# A source that is represented by a file path.
class SourceFilepath < Source
# Reparse the file and return the parse result.
def result
Prism.parse_file(value)
end
end
# A source that is represented by a string.
class SourceString < Source
# Reparse the string and return the parse result.
def result
Prism.parse(value)
end
end
# A field that represents the file path.
class FilepathField
# The file path that this field represents.
attr_reader :value
# Initialize a new field with the given file path.
def initialize(value)
@value = value
end
# Fetch the file path.
def fields(_value)
{ filepath: value }
end
end
# A field representing the start and end lines.
class LinesField
# Fetches the start and end line of a value.
def fields(value)
{ start_line: value.start_line, end_line: value.end_line }
end
end
# A field representing the start and end byte offsets.
class OffsetsField
# Fetches the start and end byte offset of a value.
def fields(value)
{ start_offset: value.start_offset, end_offset: value.end_offset }
end
end
# A field representing the start and end character offsets.
class CharacterOffsetsField
# Fetches the start and end character offset of a value.
def fields(value)
{
start_character_offset: value.start_character_offset,
end_character_offset: value.end_character_offset
}
end
end
# A field representing the start and end code unit offsets.
class CodeUnitOffsetsField
# A pointer to the repository object that is used for lazily creating a
# code units cache.
attr_reader :repository
# The associated encoding for the code units.
attr_reader :encoding
# Initialize a new field with the associated repository and encoding.
def initialize(repository, encoding)
@repository = repository
@encoding = encoding
@cache = nil
end
# Fetches the start and end code units offset of a value for a particular
# encoding.
def fields(value)
{
start_code_units_offset: value.cached_start_code_units_offset(cache),
end_code_units_offset: value.cached_end_code_units_offset(cache)
}
end
private
# Lazily create a code units cache for the associated encoding.
def cache
@cache ||= repository.code_units_cache(encoding)
end
end
# A field representing the start and end byte columns.
class ColumnsField
# Fetches the start and end byte column of a value.
def fields(value)
{ start_column: value.start_column, end_column: value.end_column }
end
end
# A field representing the start and end character columns.
class CharacterColumnsField
# Fetches the start and end character column of a value.
def fields(value)
{
start_character_column: value.start_character_column,
end_character_column: value.end_character_column
}
end
end
# A field representing the start and end code unit columns for a specific
# encoding.
class CodeUnitColumnsField
# The repository object that is used for lazily creating a code units
# cache.
attr_reader :repository
# The associated encoding for the code units.
attr_reader :encoding
# Initialize a new field with the associated repository and encoding.
def initialize(repository, encoding)
@repository = repository
@encoding = encoding
@cache = nil
end
# Fetches the start and end code units column of a value for a particular
# encoding.
def fields(value)
{
start_code_units_column: value.cached_start_code_units_column(cache),
end_code_units_column: value.cached_end_code_units_column(cache)
}
end
private
# Lazily create a code units cache for the associated encoding.
def cache
@cache ||= repository.code_units_cache(encoding)
end
end
# An abstract field used as the parent class of the two comments fields.
class CommentsField
# An object that represents a slice of a comment.
class Comment
# The slice of the comment.
attr_reader :slice
# Initialize a new comment with the given slice.
def initialize(slice)
@slice = slice
end
end
private
# Create comment objects from the given values.
def comments(values)
values.map { |value| Comment.new(value.slice) }
end
end
# A field representing the leading comments.
class LeadingCommentsField < CommentsField
# Fetches the leading comments of a value.
def fields(value)
{ leading_comments: comments(value.leading_comments) }
end
end
# A field representing the trailing comments.
class TrailingCommentsField < CommentsField
# Fetches the trailing comments of a value.
def fields(value)
{ trailing_comments: comments(value.trailing_comments) }
end
end
# A repository is a configured collection of fields and a set of entries
# that knows how to reparse a source and reify the values.
class Repository
# Raised when multiple fields of the same type are configured on the same
# repository.
class ConfigurationError < StandardError
end
# The source associated with this repository. This will be either a
# SourceFilepath (the most common use case) or a SourceString.
attr_reader :source
# The fields that have been configured on this repository.
attr_reader :fields
# The entries that have been saved on this repository.
attr_reader :entries
# Initialize a new repository with the given source.
def initialize(source)
@source = source
@fields = {}
@entries = Hash.new { |hash, node_id| hash[node_id] = {} }
end
# Create a code units cache for the given encoding from the source.
def code_units_cache(encoding)
source.code_units_cache(encoding)
end
# Configure the filepath field for this repository and return self.
def filepath
raise ConfigurationError, "Can only specify filepath for a filepath source" unless source.is_a?(SourceFilepath)
field(:filepath, FilepathField.new(source.value))
end
# Configure the lines field for this repository and return self.
def lines
field(:lines, LinesField.new)
end
# Configure the offsets field for this repository and return self.
def offsets
field(:offsets, OffsetsField.new)
end
# Configure the character offsets field for this repository and return
# self.
def character_offsets
field(:character_offsets, CharacterOffsetsField.new)
end
# Configure the code unit offsets field for this repository for a specific
# encoding and return self.
def code_unit_offsets(encoding)
field(:code_unit_offsets, CodeUnitOffsetsField.new(self, encoding))
end
# Configure the columns field for this repository and return self.
def columns
field(:columns, ColumnsField.new)
end
# Configure the character columns field for this repository and return
# self.
def character_columns
field(:character_columns, CharacterColumnsField.new)
end
# Configure the code unit columns field for this repository for a specific
# encoding and return self.
def code_unit_columns(encoding)
field(:code_unit_columns, CodeUnitColumnsField.new(self, encoding))
end
# Configure the leading comments field for this repository and return
# self.
def leading_comments
field(:leading_comments, LeadingCommentsField.new)
end
# Configure the trailing comments field for this repository and return
# self.
def trailing_comments
field(:trailing_comments, TrailingCommentsField.new)
end
# Configure both the leading and trailing comment fields for this
# repository and return self.
def comments
leading_comments.trailing_comments
end
# This method is called from nodes and locations when they want to enter
# themselves into the repository. It it internal-only and meant to be
# called from the #save* APIs.
def enter(node_id, field_name) # :nodoc:
entry = Entry.new(self)
@entries[node_id][field_name] = entry
entry
end
# This method is called from the entries in the repository when they need
# to reify their values. It is internal-only and meant to be called from
# the various value APIs.
def reify! # :nodoc:
result = source.result
# Attach the comments if they have been requested as part of the
# configuration of this repository.
if fields.key?(:leading_comments) || fields.key?(:trailing_comments)
result.attach_comments!
end
queue = [result.value] #: Array[Prism::node]
while (node = queue.shift)
@entries[node.node_id].each do |field_name, entry|
value = node.public_send(field_name)
values = {}
fields.each_value do |field|
values.merge!(field.fields(value))
end
entry.reify!(values)
end
queue.concat(node.compact_child_nodes)
end
@entries.clear
end
private
# Append the given field to the repository and return the repository so
# that these calls can be chained.
def field(name, value)
raise ConfigurationError, "Cannot specify multiple #{name} fields" if @fields.key?(name)
@fields[name] = value
self
end
end
# Create a new repository for the given filepath.
def self.filepath(value)
Repository.new(SourceFilepath.new(value))
end
# Create a new repository for the given string.
def self.string(value)
Repository.new(SourceString.new(value))
end
end
end