prism 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -17,7 +17,7 @@ module Prism
17
17
 
18
18
  # Formats the errors in a human-readable way and return them as a string.
19
19
  def format
20
- error_lines = {}
20
+ error_lines = {} #: Hash[Integer, Array[ParseError]]
21
21
  parse_result.errors.each do |error|
22
22
  location = error.location
23
23
  (location.start_line..location.end_line).each do |line|
@@ -204,8 +204,8 @@ module Prism
204
204
  LengthCounter.new(source, encoding)
205
205
  end
206
206
 
207
- @cache = {}
208
- @offsets = []
207
+ @cache = {} #: Hash[Integer, Integer]
208
+ @offsets = [] #: Array[Integer]
209
209
  end
210
210
 
211
211
  # Retrieve the code units offset from the given byte offset.
@@ -396,11 +396,11 @@ module Prism
396
396
  when :unless_node
397
397
  [LocationField.new(:keyword_loc), NodeField.new(:predicate), OptionalLocationField.new(:then_keyword_loc), OptionalNodeField.new(:statements), OptionalNodeField.new(:else_clause), OptionalLocationField.new(:end_keyword_loc)]
398
398
  when :until_node
399
- [FlagsField.new(:flags, [:begin_modifier?]), LocationField.new(:keyword_loc), OptionalLocationField.new(:closing_loc), NodeField.new(:predicate), OptionalNodeField.new(:statements)]
399
+ [FlagsField.new(:flags, [:begin_modifier?]), LocationField.new(:keyword_loc), OptionalLocationField.new(:do_keyword_loc), OptionalLocationField.new(:closing_loc), NodeField.new(:predicate), OptionalNodeField.new(:statements)]
400
400
  when :when_node
401
401
  [LocationField.new(:keyword_loc), NodeListField.new(:conditions), OptionalLocationField.new(:then_keyword_loc), OptionalNodeField.new(:statements)]
402
402
  when :while_node
403
- [FlagsField.new(:flags, [:begin_modifier?]), LocationField.new(:keyword_loc), OptionalLocationField.new(:closing_loc), NodeField.new(:predicate), OptionalNodeField.new(:statements)]
403
+ [FlagsField.new(:flags, [:begin_modifier?]), LocationField.new(:keyword_loc), OptionalLocationField.new(:do_keyword_loc), OptionalLocationField.new(:closing_loc), NodeField.new(:predicate), OptionalNodeField.new(:statements)]
404
404
  when :x_string_node
405
405
  [FlagsField.new(:flags, [:forced_utf8_encoding?, :forced_binary_encoding?]), LocationField.new(:opening_loc), LocationField.new(:content_loc), LocationField.new(:closing_loc), StringField.new(:unescaped)]
406
406
  when :yield_node
@@ -0,0 +1,504 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prism
4
+ # Prism parses deterministically for the same input. This provides a nice
5
+ # property that is exposed through the #node_id API on nodes. Effectively this
6
+ # means that for the same input, these values will remain consistent every
7
+ # time the source is parsed. This means we can reparse the source same with a
8
+ # #node_id value and find the exact same node again.
9
+ #
10
+ # The Relocation module provides an API around this property. It allows you to
11
+ # "save" nodes and locations using a minimal amount of memory (just the
12
+ # node_id and a field identifier) and then reify them later.
13
+ module Relocation
14
+ # An entry in a repository that will lazily reify its values when they are
15
+ # first accessed.
16
+ class Entry
17
+ # Raised if a value that could potentially be on an entry is missing
18
+ # because it was either not configured on the repository or it has not yet
19
+ # been fetched.
20
+ class MissingValueError < StandardError
21
+ end
22
+
23
+ # Initialize a new entry with the given repository.
24
+ def initialize(repository)
25
+ @repository = repository
26
+ @values = nil
27
+ end
28
+
29
+ # Fetch the filepath of the value.
30
+ def filepath
31
+ fetch_value(:filepath)
32
+ end
33
+
34
+ # Fetch the start line of the value.
35
+ def start_line
36
+ fetch_value(:start_line)
37
+ end
38
+
39
+ # Fetch the end line of the value.
40
+ def end_line
41
+ fetch_value(:end_line)
42
+ end
43
+
44
+ # Fetch the start byte offset of the value.
45
+ def start_offset
46
+ fetch_value(:start_offset)
47
+ end
48
+
49
+ # Fetch the end byte offset of the value.
50
+ def end_offset
51
+ fetch_value(:end_offset)
52
+ end
53
+
54
+ # Fetch the start character offset of the value.
55
+ def start_character_offset
56
+ fetch_value(:start_character_offset)
57
+ end
58
+
59
+ # Fetch the end character offset of the value.
60
+ def end_character_offset
61
+ fetch_value(:end_character_offset)
62
+ end
63
+
64
+ # Fetch the start code units offset of the value, for the encoding that
65
+ # was configured on the repository.
66
+ def start_code_units_offset
67
+ fetch_value(:start_code_units_offset)
68
+ end
69
+
70
+ # Fetch the end code units offset of the value, for the encoding that was
71
+ # configured on the repository.
72
+ def end_code_units_offset
73
+ fetch_value(:end_code_units_offset)
74
+ end
75
+
76
+ # Fetch the start byte column of the value.
77
+ def start_column
78
+ fetch_value(:start_column)
79
+ end
80
+
81
+ # Fetch the end byte column of the value.
82
+ def end_column
83
+ fetch_value(:end_column)
84
+ end
85
+
86
+ # Fetch the start character column of the value.
87
+ def start_character_column
88
+ fetch_value(:start_character_column)
89
+ end
90
+
91
+ # Fetch the end character column of the value.
92
+ def end_character_column
93
+ fetch_value(:end_character_column)
94
+ end
95
+
96
+ # Fetch the start code units column of the value, for the encoding that
97
+ # was configured on the repository.
98
+ def start_code_units_column
99
+ fetch_value(:start_code_units_column)
100
+ end
101
+
102
+ # Fetch the end code units column of the value, for the encoding that was
103
+ # configured on the repository.
104
+ def end_code_units_column
105
+ fetch_value(:end_code_units_column)
106
+ end
107
+
108
+ # Fetch the leading comments of the value.
109
+ def leading_comments
110
+ fetch_value(:leading_comments)
111
+ end
112
+
113
+ # Fetch the trailing comments of the value.
114
+ def trailing_comments
115
+ fetch_value(:trailing_comments)
116
+ end
117
+
118
+ # Fetch the leading and trailing comments of the value.
119
+ def comments
120
+ leading_comments.concat(trailing_comments)
121
+ end
122
+
123
+ # Reify the values on this entry with the given values. This is an
124
+ # internal-only API that is called from the repository when it is time to
125
+ # reify the values.
126
+ def reify!(values) # :nodoc:
127
+ @repository = nil
128
+ @values = values
129
+ end
130
+
131
+ private
132
+
133
+ # Fetch a value from the entry, raising an error if it is missing.
134
+ def fetch_value(name)
135
+ values.fetch(name) do
136
+ raise MissingValueError, "No value for #{name}, make sure the " \
137
+ "repository has been properly configured"
138
+ end
139
+ end
140
+
141
+ # Return the values from the repository, reifying them if necessary.
142
+ def values
143
+ @values || (@repository.reify!; @values)
144
+ end
145
+ end
146
+
147
+ # Represents the source of a repository that will be reparsed.
148
+ class Source
149
+ # The value that will need to be reparsed.
150
+ attr_reader :value
151
+
152
+ # Initialize the source with the given value.
153
+ def initialize(value)
154
+ @value = value
155
+ end
156
+
157
+ # Reparse the value and return the parse result.
158
+ def result
159
+ raise NotImplementedError, "Subclasses must implement #result"
160
+ end
161
+
162
+ # Create a code units cache for the given encoding.
163
+ def code_units_cache(encoding)
164
+ result.code_units_cache(encoding)
165
+ end
166
+ end
167
+
168
+ # A source that is represented by a file path.
169
+ class SourceFilepath < Source
170
+ # Reparse the file and return the parse result.
171
+ def result
172
+ Prism.parse_file(value)
173
+ end
174
+ end
175
+
176
+ # A source that is represented by a string.
177
+ class SourceString < Source
178
+ # Reparse the string and return the parse result.
179
+ def result
180
+ Prism.parse(value)
181
+ end
182
+ end
183
+
184
+ # A field that represents the file path.
185
+ class FilepathField
186
+ # The file path that this field represents.
187
+ attr_reader :value
188
+
189
+ # Initialize a new field with the given file path.
190
+ def initialize(value)
191
+ @value = value
192
+ end
193
+
194
+ # Fetch the file path.
195
+ def fields(_value)
196
+ { filepath: value }
197
+ end
198
+ end
199
+
200
+ # A field representing the start and end lines.
201
+ class LinesField
202
+ # Fetches the start and end line of a value.
203
+ def fields(value)
204
+ { start_line: value.start_line, end_line: value.end_line }
205
+ end
206
+ end
207
+
208
+ # A field representing the start and end byte offsets.
209
+ class OffsetsField
210
+ # Fetches the start and end byte offset of a value.
211
+ def fields(value)
212
+ { start_offset: value.start_offset, end_offset: value.end_offset }
213
+ end
214
+ end
215
+
216
+ # A field representing the start and end character offsets.
217
+ class CharacterOffsetsField
218
+ # Fetches the start and end character offset of a value.
219
+ def fields(value)
220
+ {
221
+ start_character_offset: value.start_character_offset,
222
+ end_character_offset: value.end_character_offset
223
+ }
224
+ end
225
+ end
226
+
227
+ # A field representing the start and end code unit offsets.
228
+ class CodeUnitOffsetsField
229
+ # A pointer to the repository object that is used for lazily creating a
230
+ # code units cache.
231
+ attr_reader :repository
232
+
233
+ # The associated encoding for the code units.
234
+ attr_reader :encoding
235
+
236
+ # Initialize a new field with the associated repository and encoding.
237
+ def initialize(repository, encoding)
238
+ @repository = repository
239
+ @encoding = encoding
240
+ @cache = nil
241
+ end
242
+
243
+ # Fetches the start and end code units offset of a value for a particular
244
+ # encoding.
245
+ def fields(value)
246
+ {
247
+ start_code_units_offset: value.cached_start_code_units_offset(cache),
248
+ end_code_units_offset: value.cached_end_code_units_offset(cache)
249
+ }
250
+ end
251
+
252
+ private
253
+
254
+ # Lazily create a code units cache for the associated encoding.
255
+ def cache
256
+ @cache ||= repository.code_units_cache(encoding)
257
+ end
258
+ end
259
+
260
+ # A field representing the start and end byte columns.
261
+ class ColumnsField
262
+ # Fetches the start and end byte column of a value.
263
+ def fields(value)
264
+ { start_column: value.start_column, end_column: value.end_column }
265
+ end
266
+ end
267
+
268
+ # A field representing the start and end character columns.
269
+ class CharacterColumnsField
270
+ # Fetches the start and end character column of a value.
271
+ def fields(value)
272
+ {
273
+ start_character_column: value.start_character_column,
274
+ end_character_column: value.end_character_column
275
+ }
276
+ end
277
+ end
278
+
279
+ # A field representing the start and end code unit columns for a specific
280
+ # encoding.
281
+ class CodeUnitColumnsField
282
+ # The repository object that is used for lazily creating a code units
283
+ # cache.
284
+ attr_reader :repository
285
+
286
+ # The associated encoding for the code units.
287
+ attr_reader :encoding
288
+
289
+ # Initialize a new field with the associated repository and encoding.
290
+ def initialize(repository, encoding)
291
+ @repository = repository
292
+ @encoding = encoding
293
+ @cache = nil
294
+ end
295
+
296
+ # Fetches the start and end code units column of a value for a particular
297
+ # encoding.
298
+ def fields(value)
299
+ {
300
+ start_code_units_column: value.cached_start_code_units_column(cache),
301
+ end_code_units_column: value.cached_end_code_units_column(cache)
302
+ }
303
+ end
304
+
305
+ private
306
+
307
+ # Lazily create a code units cache for the associated encoding.
308
+ def cache
309
+ @cache ||= repository.code_units_cache(encoding)
310
+ end
311
+ end
312
+
313
+ # An abstract field used as the parent class of the two comments fields.
314
+ class CommentsField
315
+ # An object that represents a slice of a comment.
316
+ class Comment
317
+ # The slice of the comment.
318
+ attr_reader :slice
319
+
320
+ # Initialize a new comment with the given slice.
321
+ def initialize(slice)
322
+ @slice = slice
323
+ end
324
+ end
325
+
326
+ private
327
+
328
+ # Create comment objects from the given values.
329
+ def comments(values)
330
+ values.map { |value| Comment.new(value.slice) }
331
+ end
332
+ end
333
+
334
+ # A field representing the leading comments.
335
+ class LeadingCommentsField < CommentsField
336
+ # Fetches the leading comments of a value.
337
+ def fields(value)
338
+ { leading_comments: comments(value.leading_comments) }
339
+ end
340
+ end
341
+
342
+ # A field representing the trailing comments.
343
+ class TrailingCommentsField < CommentsField
344
+ # Fetches the trailing comments of a value.
345
+ def fields(value)
346
+ { trailing_comments: comments(value.trailing_comments) }
347
+ end
348
+ end
349
+
350
+ # A repository is a configured collection of fields and a set of entries
351
+ # that knows how to reparse a source and reify the values.
352
+ class Repository
353
+ # Raised when multiple fields of the same type are configured on the same
354
+ # repository.
355
+ class ConfigurationError < StandardError
356
+ end
357
+
358
+ # The source associated with this repository. This will be either a
359
+ # SourceFilepath (the most common use case) or a SourceString.
360
+ attr_reader :source
361
+
362
+ # The fields that have been configured on this repository.
363
+ attr_reader :fields
364
+
365
+ # The entries that have been saved on this repository.
366
+ attr_reader :entries
367
+
368
+ # Initialize a new repository with the given source.
369
+ def initialize(source)
370
+ @source = source
371
+ @fields = {}
372
+ @entries = Hash.new { |hash, node_id| hash[node_id] = {} }
373
+ end
374
+
375
+ # Create a code units cache for the given encoding from the source.
376
+ def code_units_cache(encoding)
377
+ source.code_units_cache(encoding)
378
+ end
379
+
380
+ # Configure the filepath field for this repository and return self.
381
+ def filepath
382
+ raise ConfigurationError, "Can only specify filepath for a filepath source" unless source.is_a?(SourceFilepath)
383
+ field(:filepath, FilepathField.new(source.value))
384
+ end
385
+
386
+ # Configure the lines field for this repository and return self.
387
+ def lines
388
+ field(:lines, LinesField.new)
389
+ end
390
+
391
+ # Configure the offsets field for this repository and return self.
392
+ def offsets
393
+ field(:offsets, OffsetsField.new)
394
+ end
395
+
396
+ # Configure the character offsets field for this repository and return
397
+ # self.
398
+ def character_offsets
399
+ field(:character_offsets, CharacterOffsetsField.new)
400
+ end
401
+
402
+ # Configure the code unit offsets field for this repository for a specific
403
+ # encoding and return self.
404
+ def code_unit_offsets(encoding)
405
+ field(:code_unit_offsets, CodeUnitOffsetsField.new(self, encoding))
406
+ end
407
+
408
+ # Configure the columns field for this repository and return self.
409
+ def columns
410
+ field(:columns, ColumnsField.new)
411
+ end
412
+
413
+ # Configure the character columns field for this repository and return
414
+ # self.
415
+ def character_columns
416
+ field(:character_columns, CharacterColumnsField.new)
417
+ end
418
+
419
+ # Configure the code unit columns field for this repository for a specific
420
+ # encoding and return self.
421
+ def code_unit_columns(encoding)
422
+ field(:code_unit_columns, CodeUnitColumnsField.new(self, encoding))
423
+ end
424
+
425
+ # Configure the leading comments field for this repository and return
426
+ # self.
427
+ def leading_comments
428
+ field(:leading_comments, LeadingCommentsField.new)
429
+ end
430
+
431
+ # Configure the trailing comments field for this repository and return
432
+ # self.
433
+ def trailing_comments
434
+ field(:trailing_comments, TrailingCommentsField.new)
435
+ end
436
+
437
+ # Configure both the leading and trailing comment fields for this
438
+ # repository and return self.
439
+ def comments
440
+ leading_comments.trailing_comments
441
+ end
442
+
443
+ # This method is called from nodes and locations when they want to enter
444
+ # themselves into the repository. It it internal-only and meant to be
445
+ # called from the #save* APIs.
446
+ def enter(node_id, field_name) # :nodoc:
447
+ entry = Entry.new(self)
448
+ @entries[node_id][field_name] = entry
449
+ entry
450
+ end
451
+
452
+ # This method is called from the entries in the repository when they need
453
+ # to reify their values. It is internal-only and meant to be called from
454
+ # the various value APIs.
455
+ def reify! # :nodoc:
456
+ result = source.result
457
+
458
+ # Attach the comments if they have been requested as part of the
459
+ # configuration of this repository.
460
+ if fields.key?(:leading_comments) || fields.key?(:trailing_comments)
461
+ result.attach_comments!
462
+ end
463
+
464
+ queue = [result.value] #: Array[Prism::node]
465
+ while (node = queue.shift)
466
+ @entries[node.node_id].each do |field_name, entry|
467
+ value = node.public_send(field_name)
468
+ values = {} #: Hash[Symbol, untyped]
469
+
470
+ fields.each_value do |field|
471
+ values.merge!(field.fields(value))
472
+ end
473
+
474
+ entry.reify!(values)
475
+ end
476
+
477
+ queue.concat(node.compact_child_nodes)
478
+ end
479
+
480
+ @entries.clear
481
+ end
482
+
483
+ private
484
+
485
+ # Append the given field to the repository and return the repository so
486
+ # that these calls can be chained.
487
+ def field(name, value)
488
+ raise ConfigurationError, "Cannot specify multiple #{name} fields" if @fields.key?(name)
489
+ @fields[name] = value
490
+ self
491
+ end
492
+ end
493
+
494
+ # Create a new repository for the given filepath.
495
+ def self.filepath(value)
496
+ Repository.new(SourceFilepath.new(value))
497
+ end
498
+
499
+ # Create a new repository for the given string.
500
+ def self.string(value)
501
+ Repository.new(SourceString.new(value))
502
+ end
503
+ end
504
+ end
@@ -18,7 +18,7 @@ module Prism
18
18
 
19
19
  # The minor version of prism that we are expecting to find in the serialized
20
20
  # strings.
21
- MINOR_VERSION = 2
21
+ MINOR_VERSION = 3
22
22
 
23
23
  # The patch version of prism that we are expecting to find in the serialized
24
24
  # strings.
@@ -952,11 +952,11 @@ module Prism
952
952
  when 146 then
953
953
  UnlessNode.new(source, node_id, location, load_varuint, load_location, load_node, load_optional_location, load_optional_node, load_optional_node, load_optional_location)
954
954
  when 147 then
955
- UntilNode.new(source, node_id, location, load_varuint, load_location, load_optional_location, load_node, load_optional_node)
955
+ UntilNode.new(source, node_id, location, load_varuint, load_location, load_optional_location, load_optional_location, load_node, load_optional_node)
956
956
  when 148 then
957
957
  WhenNode.new(source, node_id, location, load_varuint, load_location, Array.new(load_varuint) { load_node }, load_optional_location, load_optional_node)
958
958
  when 149 then
959
- WhileNode.new(source, node_id, location, load_varuint, load_location, load_optional_location, load_node, load_optional_node)
959
+ WhileNode.new(source, node_id, location, load_varuint, load_location, load_optional_location, load_optional_location, load_node, load_optional_node)
960
960
  when 150 then
961
961
  XStringNode.new(source, node_id, location, load_varuint, load_location, load_location, load_location, load_string)
962
962
  when 151 then
@@ -1706,7 +1706,7 @@ module Prism
1706
1706
  -> {
1707
1707
  node_id = load_varuint
1708
1708
  location = load_location
1709
- UntilNode.new(source, node_id, location, load_varuint, load_location, load_optional_location, load_node, load_optional_node)
1709
+ UntilNode.new(source, node_id, location, load_varuint, load_location, load_optional_location, load_optional_location, load_node, load_optional_node)
1710
1710
  },
1711
1711
  -> {
1712
1712
  node_id = load_varuint
@@ -1716,7 +1716,7 @@ module Prism
1716
1716
  -> {
1717
1717
  node_id = load_varuint
1718
1718
  location = load_location
1719
- WhileNode.new(source, node_id, location, load_varuint, load_location, load_optional_location, load_node, load_optional_node)
1719
+ WhileNode.new(source, node_id, location, load_varuint, load_location, load_optional_location, load_optional_location, load_node, load_optional_node)
1720
1720
  },
1721
1721
  -> {
1722
1722
  node_id = load_varuint
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prism
4
+ # Query methods that allow categorizing strings based on their context for
5
+ # where they could be valid in a Ruby syntax tree.
6
+ class StringQuery
7
+ # The string that this query is wrapping.
8
+ attr_reader :string
9
+
10
+ # Initialize a new query with the given string.
11
+ def initialize(string)
12
+ @string = string
13
+ end
14
+
15
+ # Whether or not this string is a valid local variable name.
16
+ def local?
17
+ StringQuery.local?(string)
18
+ end
19
+
20
+ # Whether or not this string is a valid constant name.
21
+ def constant?
22
+ StringQuery.constant?(string)
23
+ end
24
+
25
+ # Whether or not this string is a valid method name.
26
+ def method_name?
27
+ StringQuery.method_name?(string)
28
+ end
29
+ end
30
+ end