syntax_tree 1.2.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3223 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxTree
4
+ class Parser < Ripper
5
+ # A special parser error so that we can get nice syntax displays on the
6
+ # error message when prettier prints out the results.
7
+ class ParseError < StandardError
8
+ attr_reader :lineno, :column
9
+
10
+ def initialize(error, lineno, column)
11
+ super(error)
12
+ @lineno = lineno
13
+ @column = column
14
+ end
15
+ end
16
+
17
+ # Represents a line in the source. If this class is being used, it means
18
+ # that every character in the string is 1 byte in length, so we can just
19
+ # return the start of the line + the index.
20
+ class SingleByteString
21
+ attr_reader :start
22
+
23
+ def initialize(start)
24
+ @start = start
25
+ end
26
+
27
+ def [](byteindex)
28
+ start + byteindex
29
+ end
30
+ end
31
+
32
+ # Represents a line in the source. If this class is being used, it means
33
+ # that there are characters in the string that are multi-byte, so we will
34
+ # build up an array of indices, such that array[byteindex] will be equal to
35
+ # the index of the character within the string.
36
+ class MultiByteString
37
+ attr_reader :start, :indices
38
+
39
+ def initialize(start, line)
40
+ @start = start
41
+ @indices = []
42
+
43
+ line.each_char.with_index(start) do |char, index|
44
+ char.bytesize.times { @indices << index }
45
+ end
46
+ end
47
+
48
+ # Technically it's possible for the column index to be a negative value if
49
+ # there's a BOM at the beginning of the file, which is the reason we need
50
+ # to compare it to 0 here.
51
+ def [](byteindex)
52
+ indices[byteindex < 0 ? 0 : byteindex]
53
+ end
54
+ end
55
+
56
+ # [String] the source being parsed
57
+ attr_reader :source
58
+
59
+ # [Array[ String ]] the list of lines in the source
60
+ attr_reader :lines
61
+
62
+ # [Array[ SingleByteString | MultiByteString ]] the list of objects that
63
+ # represent the start of each line in character offsets
64
+ attr_reader :line_counts
65
+
66
+ # [Array[ untyped ]] a running list of tokens that have been found in the
67
+ # source. This list changes a lot as certain nodes will "consume" these
68
+ # tokens to determine their bounds.
69
+ attr_reader :tokens
70
+
71
+ # [Array[ Comment | EmbDoc ]] the list of comments that have been found
72
+ # while parsing the source.
73
+ attr_reader :comments
74
+
75
+ def initialize(source, *)
76
+ super
77
+
78
+ # We keep the source around so that we can refer back to it when we're
79
+ # generating the AST. Sometimes it's easier to just reference the source
80
+ # string when you want to check if it contains a certain character, for
81
+ # example.
82
+ @source = source
83
+
84
+ # Similarly, we keep the lines of the source string around to be able to
85
+ # check if certain lines contain certain characters. For example, we'll
86
+ # use this to generate the content that goes after the __END__ keyword.
87
+ # Or we'll use this to check if a comment has other content on its line.
88
+ @lines = source.split(/\r?\n/)
89
+
90
+ # This is the full set of comments that have been found by the parser.
91
+ # It's a running list. At the end of every block of statements, they will
92
+ # go in and attempt to grab any comments that are on their own line and
93
+ # turn them into regular statements. So at the end of parsing the only
94
+ # comments left in here will be comments on lines that also contain code.
95
+ @comments = []
96
+
97
+ # This is the current embdoc (comments that start with =begin and end with
98
+ # =end). Since they can't be nested, there's no need for a stack here, as
99
+ # there can only be one active. These end up getting dumped into the
100
+ # comments list before getting picked up by the statements that surround
101
+ # them.
102
+ @embdoc = nil
103
+
104
+ # This is an optional node that can be present if the __END__ keyword is
105
+ # used in the file. In that case, this will represent the content after
106
+ # that keyword.
107
+ @__end__ = nil
108
+
109
+ # Heredocs can actually be nested together if you're using interpolation,
110
+ # so this is a stack of heredoc nodes that are currently being created.
111
+ # When we get to the token that finishes off a heredoc node, we pop the
112
+ # top one off. If there are others surrounding it, then the body events
113
+ # will now be added to the correct nodes.
114
+ @heredocs = []
115
+
116
+ # This is a running list of tokens that have fired. It's useful mostly for
117
+ # maintaining location information. For example, if you're inside the
118
+ # handle of a def event, then in order to determine where the AST node
119
+ # started, you need to look backward in the tokens to find a def keyword.
120
+ # Most of the time, when a parser event consumes one of these events, it
121
+ # will be deleted from the list. So ideally, this list stays pretty short
122
+ # over the course of parsing a source string.
123
+ @tokens = []
124
+
125
+ # Here we're going to build up a list of SingleByteString or
126
+ # MultiByteString objects. They're each going to represent a string in the
127
+ # source. They are used by the `char_pos` method to determine where we are
128
+ # in the source string.
129
+ @line_counts = []
130
+ last_index = 0
131
+
132
+ @source.lines.each do |line|
133
+ if line.size == line.bytesize
134
+ @line_counts << SingleByteString.new(last_index)
135
+ else
136
+ @line_counts << MultiByteString.new(last_index, line)
137
+ end
138
+
139
+ last_index += line.size
140
+ end
141
+
142
+ # Make sure line counts is filled out with the first and last line at
143
+ # minimum so that it has something to compare against if the parser is in
144
+ # a lineno=2 state for an empty file.
145
+ @line_counts << SingleByteString.new(0) if @line_counts.empty?
146
+ @line_counts << SingleByteString.new(last_index)
147
+ end
148
+
149
+ private
150
+
151
+ # --------------------------------------------------------------------------
152
+ # :section: Helper methods
153
+ # The following methods are used by the ripper event handlers to either
154
+ # determine their bounds or query other nodes.
155
+ # --------------------------------------------------------------------------
156
+
157
+ # This represents the current place in the source string that we've gotten
158
+ # to so far. We have a memoized line_counts object that we can use to get
159
+ # the number of characters that we've had to go through to get to the
160
+ # beginning of this line, then we add the number of columns into this line
161
+ # that we've gone through.
162
+ def char_pos
163
+ line_counts[lineno - 1][column]
164
+ end
165
+
166
+ # This represents the current column we're in relative to the beginning of
167
+ # the current line.
168
+ def current_column
169
+ line = line_counts[lineno - 1]
170
+ line[column].to_i - line.start
171
+ end
172
+
173
+ # As we build up a list of tokens, we'll periodically need to go backwards
174
+ # and find the ones that we've already hit in order to determine the
175
+ # location information for nodes that use them. For example, if you have a
176
+ # module node then you'll look backward for a kw token to determine your
177
+ # start location.
178
+ #
179
+ # This works with nesting since we're deleting tokens from the list once
180
+ # they've been used up. For example if you had nested module declarations
181
+ # then the innermost declaration would grab the last kw node that matches
182
+ # "module" (which would happen to be the innermost keyword). Then the outer
183
+ # one would only be able to grab the first one. In this way all of the
184
+ # tokens act as their own stack.
185
+ def find_token(type, value = :any, consume: true, location: nil)
186
+ index =
187
+ tokens.rindex do |token|
188
+ token.is_a?(type) && (value == :any || (token.value == value))
189
+ end
190
+
191
+ if consume
192
+ # If we're expecting to be able to find a token and consume it, but
193
+ # can't actually find it, then we need to raise an error. This is
194
+ # _usually_ caused by a syntax error in the source that we're printing.
195
+ # It could also be caused by accidentally attempting to consume a token
196
+ # twice by two different parser event handlers.
197
+ unless index
198
+ token = value == :any ? type.name.split("::", 2).last : value
199
+ message = "Cannot find expected #{token}"
200
+
201
+ if location
202
+ lineno = location.start_line
203
+ column = location.start_char - line_counts[lineno - 1].start
204
+ raise ParseError.new(message, lineno, column)
205
+ else
206
+ raise ParseError.new(message, lineno, column)
207
+ end
208
+ end
209
+
210
+ tokens.delete_at(index)
211
+ elsif index
212
+ tokens[index]
213
+ end
214
+ end
215
+
216
+ # A helper function to find a :: operator. We do special handling instead of
217
+ # using find_token here because we don't pop off all of the :: operators so
218
+ # you could end up getting the wrong information if you have for instance
219
+ # ::X::Y::Z.
220
+ def find_colon2_before(const)
221
+ index =
222
+ tokens.rindex do |token|
223
+ token.is_a?(Op) && token.value == "::" &&
224
+ token.location.start_char < const.location.start_char
225
+ end
226
+
227
+ tokens[index]
228
+ end
229
+
230
+ # Finds the next position in the source string that begins a statement. This
231
+ # is used to bind statements lists and make sure they don't include a
232
+ # preceding comment. For example, we want the following comment to be
233
+ # attached to the class node and not the statement node:
234
+ #
235
+ # class Foo # :nodoc:
236
+ # ...
237
+ # end
238
+ #
239
+ # By finding the next non-space character, we can make sure that the bounds
240
+ # of the statement list are correct.
241
+ def find_next_statement_start(position)
242
+ remaining = source[position..-1]
243
+
244
+ if remaining.sub(/\A +/, "")[0] == "#"
245
+ return position + remaining.index("\n")
246
+ end
247
+
248
+ position
249
+ end
250
+
251
+ # --------------------------------------------------------------------------
252
+ # :section: Ripper event handlers
253
+ # The following methods all handle a dispatched ripper event.
254
+ # --------------------------------------------------------------------------
255
+
256
+ # :call-seq:
257
+ # on_BEGIN: (Statements statements) -> BEGINBlock
258
+ def on_BEGIN(statements)
259
+ lbrace = find_token(LBrace)
260
+ rbrace = find_token(RBrace)
261
+ start_char = find_next_statement_start(lbrace.location.end_char)
262
+
263
+ statements.bind(
264
+ start_char,
265
+ start_char - line_counts[lbrace.location.start_line - 1].start,
266
+ rbrace.location.start_char,
267
+ rbrace.location.start_column,
268
+ )
269
+
270
+ keyword = find_token(Kw, "BEGIN")
271
+
272
+ BEGINBlock.new(
273
+ lbrace: lbrace,
274
+ statements: statements,
275
+ location: keyword.location.to(rbrace.location)
276
+ )
277
+ end
278
+
279
+ # :call-seq:
280
+ # on_CHAR: (String value) -> CHAR
281
+ def on_CHAR(value)
282
+ CHAR.new(
283
+ value: value,
284
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
285
+ )
286
+ end
287
+
288
+ # :call-seq:
289
+ # on_END: (Statements statements) -> ENDBlock
290
+ def on_END(statements)
291
+ lbrace = find_token(LBrace)
292
+ rbrace = find_token(RBrace)
293
+ start_char = find_next_statement_start(lbrace.location.end_char)
294
+
295
+ statements.bind(
296
+ start_char,
297
+ start_char - line_counts[lbrace.location.start_line - 1].start,
298
+ rbrace.location.start_char,
299
+ rbrace.location.start_column
300
+ )
301
+
302
+ keyword = find_token(Kw, "END")
303
+
304
+ ENDBlock.new(
305
+ lbrace: lbrace,
306
+ statements: statements,
307
+ location: keyword.location.to(rbrace.location)
308
+ )
309
+ end
310
+
311
+ # :call-seq:
312
+ # on___end__: (String value) -> EndContent
313
+ def on___end__(value)
314
+ @__end__ =
315
+ EndContent.new(
316
+ value: source[(char_pos + value.length)..-1],
317
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
318
+ )
319
+ end
320
+
321
+ # :call-seq:
322
+ # on_alias: (
323
+ # (DynaSymbol | SymbolLiteral) left,
324
+ # (DynaSymbol | SymbolLiteral) right
325
+ # ) -> Alias
326
+ def on_alias(left, right)
327
+ keyword = find_token(Kw, "alias")
328
+
329
+ Alias.new(
330
+ left: left,
331
+ right: right,
332
+ location: keyword.location.to(right.location)
333
+ )
334
+ end
335
+
336
+ # :call-seq:
337
+ # on_aref: (untyped collection, (nil | Args) index) -> ARef
338
+ def on_aref(collection, index)
339
+ find_token(LBracket)
340
+ rbracket = find_token(RBracket)
341
+
342
+ ARef.new(
343
+ collection: collection,
344
+ index: index,
345
+ location: collection.location.to(rbracket.location)
346
+ )
347
+ end
348
+
349
+ # :call-seq:
350
+ # on_aref_field: (
351
+ # untyped collection,
352
+ # (nil | Args) index
353
+ # ) -> ARefField
354
+ def on_aref_field(collection, index)
355
+ find_token(LBracket)
356
+ rbracket = find_token(RBracket)
357
+
358
+ ARefField.new(
359
+ collection: collection,
360
+ index: index,
361
+ location: collection.location.to(rbracket.location)
362
+ )
363
+ end
364
+
365
+ # def on_arg_ambiguous(value)
366
+ # value
367
+ # end
368
+
369
+ # :call-seq:
370
+ # on_arg_paren: (
371
+ # (nil | Args | ArgsForward) arguments
372
+ # ) -> ArgParen
373
+ def on_arg_paren(arguments)
374
+ lparen = find_token(LParen)
375
+ rparen = find_token(RParen)
376
+
377
+ # If the arguments exceed the ending of the parentheses, then we know we
378
+ # have a heredoc in the arguments, and we need to use the bounds of the
379
+ # arguments to determine how large the arg_paren is.
380
+ ending =
381
+ if arguments && arguments.location.end_line > rparen.location.end_line
382
+ arguments
383
+ else
384
+ rparen
385
+ end
386
+
387
+ ArgParen.new(
388
+ arguments: arguments,
389
+ location: lparen.location.to(ending.location)
390
+ )
391
+ end
392
+
393
+ # :call-seq:
394
+ # on_args_add: (Args arguments, untyped argument) -> Args
395
+ def on_args_add(arguments, argument)
396
+ if arguments.parts.empty?
397
+ # If this is the first argument being passed into the list of arguments,
398
+ # then we're going to use the bounds of the argument to override the
399
+ # parent node's location since this will be more accurate.
400
+ Args.new(parts: [argument], location: argument.location)
401
+ else
402
+ # Otherwise we're going to update the existing list with the argument
403
+ # being added as well as the new end bounds.
404
+ Args.new(
405
+ parts: arguments.parts << argument,
406
+ location: arguments.location.to(argument.location)
407
+ )
408
+ end
409
+ end
410
+
411
+ # :call-seq:
412
+ # on_args_add_block: (
413
+ # Args arguments,
414
+ # (false | untyped) block
415
+ # ) -> Args
416
+ def on_args_add_block(arguments, block)
417
+ operator = find_token(Op, "&", consume: false)
418
+
419
+ # If we can't find the & operator, then there's no block to add to the
420
+ # list, so we're just going to return the arguments as-is.
421
+ return arguments unless operator
422
+
423
+ # Now we know we have an & operator, so we're going to delete it from the
424
+ # list of tokens to make sure it doesn't get confused with anything else.
425
+ tokens.delete(operator)
426
+
427
+ # Construct the location that represents the block argument.
428
+ location = operator.location
429
+ location = operator.location.to(block.location) if block
430
+
431
+ # If there are any arguments and the operator we found from the list is
432
+ # not after them, then we're going to return the arguments as-is because
433
+ # we're looking at an & that occurs before the arguments are done.
434
+ if arguments.parts.any? && location.start_char < arguments.location.end_char
435
+ return arguments
436
+ end
437
+
438
+ # Otherwise, we're looking at an actual block argument (with or without a
439
+ # block, which could be missing because it could be a bare & since 3.1.0).
440
+ arg_block = ArgBlock.new(value: block, location: location)
441
+
442
+ Args.new(
443
+ parts: arguments.parts << arg_block,
444
+ location: arguments.location.to(location)
445
+ )
446
+ end
447
+
448
+ # :call-seq:
449
+ # on_args_add_star: (Args arguments, untyped star) -> Args
450
+ def on_args_add_star(arguments, argument)
451
+ beginning = find_token(Op, "*")
452
+ ending = argument || beginning
453
+
454
+ location =
455
+ if arguments.parts.empty?
456
+ ending.location
457
+ else
458
+ arguments.location.to(ending.location)
459
+ end
460
+
461
+ arg_star =
462
+ ArgStar.new(
463
+ value: argument,
464
+ location: beginning.location.to(ending.location)
465
+ )
466
+
467
+ Args.new(parts: arguments.parts << arg_star, location: location)
468
+ end
469
+
470
+ # :call-seq:
471
+ # on_args_forward: () -> ArgsForward
472
+ def on_args_forward
473
+ op = find_token(Op, "...")
474
+
475
+ ArgsForward.new(value: op.value, location: op.location)
476
+ end
477
+
478
+ # :call-seq:
479
+ # on_args_new: () -> Args
480
+ def on_args_new
481
+ Args.new(parts: [], location: Location.fixed(line: lineno, column: current_column, char: char_pos))
482
+ end
483
+
484
+ # :call-seq:
485
+ # on_array: ((nil | Args) contents) ->
486
+ # ArrayLiteral | QSymbols | QWords | Symbols | Words
487
+ def on_array(contents)
488
+ if !contents || contents.is_a?(Args)
489
+ lbracket = find_token(LBracket)
490
+ rbracket = find_token(RBracket)
491
+
492
+ ArrayLiteral.new(
493
+ lbracket: lbracket,
494
+ contents: contents,
495
+ location: lbracket.location.to(rbracket.location)
496
+ )
497
+ else
498
+ tstring_end =
499
+ find_token(TStringEnd, location: contents.beginning.location)
500
+
501
+ contents.class.new(
502
+ beginning: contents.beginning,
503
+ elements: contents.elements,
504
+ location: contents.location.to(tstring_end.location)
505
+ )
506
+ end
507
+ end
508
+
509
+ # :call-seq:
510
+ # on_aryptn: (
511
+ # (nil | VarRef) constant,
512
+ # (nil | Array[untyped]) requireds,
513
+ # (nil | VarField) rest,
514
+ # (nil | Array[untyped]) posts
515
+ # ) -> AryPtn
516
+ def on_aryptn(constant, requireds, rest, posts)
517
+ parts = [constant, *requireds, rest, *posts].compact
518
+
519
+ AryPtn.new(
520
+ constant: constant,
521
+ requireds: requireds || [],
522
+ rest: rest,
523
+ posts: posts || [],
524
+ location: parts[0].location.to(parts[-1].location)
525
+ )
526
+ end
527
+
528
+ # :call-seq:
529
+ # on_assign: (
530
+ # (ARefField | ConstPathField | Field | TopConstField | VarField) target,
531
+ # untyped value
532
+ # ) -> Assign
533
+ def on_assign(target, value)
534
+ Assign.new(
535
+ target: target,
536
+ value: value,
537
+ location: target.location.to(value.location)
538
+ )
539
+ end
540
+
541
+ # :call-seq:
542
+ # on_assoc_new: (untyped key, untyped value) -> Assoc
543
+ def on_assoc_new(key, value)
544
+ location = key.location
545
+ location = location.to(value.location) if value
546
+
547
+ Assoc.new(key: key, value: value, location: location)
548
+ end
549
+
550
+ # :call-seq:
551
+ # on_assoc_splat: (untyped value) -> AssocSplat
552
+ def on_assoc_splat(value)
553
+ operator = find_token(Op, "**")
554
+
555
+ AssocSplat.new(value: value, location: operator.location.to(value.location))
556
+ end
557
+
558
+ # def on_assoclist_from_args(assocs)
559
+ # assocs
560
+ # end
561
+
562
+ # :call-seq:
563
+ # on_backref: (String value) -> Backref
564
+ def on_backref(value)
565
+ Backref.new(
566
+ value: value,
567
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
568
+ )
569
+ end
570
+
571
+ # :call-seq:
572
+ # on_backtick: (String value) -> Backtick
573
+ def on_backtick(value)
574
+ node =
575
+ Backtick.new(
576
+ value: value,
577
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
578
+ )
579
+
580
+ tokens << node
581
+ node
582
+ end
583
+
584
+ # :call-seq:
585
+ # on_bare_assoc_hash: (Array[AssocNew | AssocSplat] assocs) -> BareAssocHash
586
+ def on_bare_assoc_hash(assocs)
587
+ BareAssocHash.new(
588
+ assocs: assocs,
589
+ location: assocs[0].location.to(assocs[-1].location)
590
+ )
591
+ end
592
+
593
+ # :call-seq:
594
+ # on_begin: (untyped bodystmt) -> Begin | PinnedBegin
595
+ def on_begin(bodystmt)
596
+ pin = find_token(Op, "^", consume: false)
597
+
598
+ if pin && pin.location.start_char < bodystmt.location.start_char
599
+ tokens.delete(pin)
600
+ find_token(LParen)
601
+
602
+ rparen = find_token(RParen)
603
+ location = pin.location.to(rparen.location)
604
+
605
+ PinnedBegin.new(statement: bodystmt, location: location)
606
+ else
607
+ keyword = find_token(Kw, "begin")
608
+ end_location =
609
+ if bodystmt.rescue_clause || bodystmt.ensure_clause ||
610
+ bodystmt.else_clause
611
+ bodystmt.location
612
+ else
613
+ find_token(Kw, "end").location
614
+ end
615
+
616
+ bodystmt.bind(
617
+ keyword.location.end_char,
618
+ keyword.location.end_column,
619
+ end_location.end_char,
620
+ end_location.end_column
621
+ )
622
+ location = keyword.location.to(bodystmt.location)
623
+
624
+ Begin.new(bodystmt: bodystmt, location: location)
625
+ end
626
+ end
627
+
628
+ # :call-seq:
629
+ # on_binary: (untyped left, (Op | Symbol) operator, untyped right) -> Binary
630
+ def on_binary(left, operator, right)
631
+ if operator.is_a?(Symbol)
632
+ # Here, we're going to search backward for the token that's between the
633
+ # two operands that matches the operator so we can delete it from the
634
+ # list.
635
+ index =
636
+ tokens.rindex do |token|
637
+ location = token.location
638
+
639
+ token.is_a?(Op) && token.value == operator.to_s &&
640
+ location.start_char > left.location.end_char &&
641
+ location.end_char < right.location.start_char
642
+ end
643
+
644
+ tokens.delete_at(index) if index
645
+ else
646
+ # On most Ruby implementations, operator is a Symbol that represents
647
+ # that operation being performed. For instance in the example `1 < 2`,
648
+ # the `operator` object would be `:<`. However, on JRuby, it's an `@op`
649
+ # node, so here we're going to explicitly convert it into the same
650
+ # normalized form.
651
+ operator = tokens.delete(operator).value
652
+ end
653
+
654
+ Binary.new(
655
+ left: left,
656
+ operator: operator,
657
+ right: right,
658
+ location: left.location.to(right.location)
659
+ )
660
+ end
661
+
662
+ # :call-seq:
663
+ # on_block_var: (Params params, (nil | Array[Ident]) locals) -> BlockVar
664
+ def on_block_var(params, locals)
665
+ index =
666
+ tokens.rindex do |node|
667
+ node.is_a?(Op) && %w[| ||].include?(node.value) &&
668
+ node.location.start_char < params.location.start_char
669
+ end
670
+
671
+ beginning = tokens[index]
672
+ ending = tokens[-1]
673
+
674
+ BlockVar.new(
675
+ params: params,
676
+ locals: locals || [],
677
+ location: beginning.location.to(ending.location)
678
+ )
679
+ end
680
+
681
+ # :call-seq:
682
+ # on_blockarg: (Ident name) -> BlockArg
683
+ def on_blockarg(name)
684
+ operator = find_token(Op, "&")
685
+
686
+ location = operator.location
687
+ location = location.to(name.location) if name
688
+
689
+ BlockArg.new(name: name, location: location)
690
+ end
691
+
692
+ # :call-seq:
693
+ # on_bodystmt: (
694
+ # Statements statements,
695
+ # (nil | Rescue) rescue_clause,
696
+ # (nil | Statements) else_clause,
697
+ # (nil | Ensure) ensure_clause
698
+ # ) -> BodyStmt
699
+ def on_bodystmt(statements, rescue_clause, else_clause, ensure_clause)
700
+ BodyStmt.new(
701
+ statements: statements,
702
+ rescue_clause: rescue_clause,
703
+ else_keyword: else_clause && find_token(Kw, "else"),
704
+ else_clause: else_clause,
705
+ ensure_clause: ensure_clause,
706
+ location: Location.fixed(line: lineno, char: char_pos, column: current_column)
707
+ )
708
+ end
709
+
710
+ # :call-seq:
711
+ # on_brace_block: (
712
+ # (nil | BlockVar) block_var,
713
+ # Statements statements
714
+ # ) -> BraceBlock
715
+ def on_brace_block(block_var, statements)
716
+ lbrace = find_token(LBrace)
717
+ rbrace = find_token(RBrace)
718
+ location = (block_var || lbrace).location
719
+ start_char = find_next_statement_start(location.end_char)
720
+
721
+ statements.bind(
722
+ start_char,
723
+ start_char - line_counts[location.start_line - 1].start,
724
+ rbrace.location.start_char,
725
+ rbrace.location.start_column
726
+ )
727
+
728
+ location =
729
+ Location.new(
730
+ start_line: lbrace.location.start_line,
731
+ start_char: lbrace.location.start_char,
732
+ start_column: lbrace.location.start_column,
733
+ end_line: [rbrace.location.end_line, statements.location.end_line].max,
734
+ end_char: rbrace.location.end_char,
735
+ end_column: rbrace.location.end_column
736
+ )
737
+
738
+ BraceBlock.new(
739
+ lbrace: lbrace,
740
+ block_var: block_var,
741
+ statements: statements,
742
+ location: location
743
+ )
744
+ end
745
+
746
+ # :call-seq:
747
+ # on_break: (Args arguments) -> Break
748
+ def on_break(arguments)
749
+ keyword = find_token(Kw, "break")
750
+
751
+ location = keyword.location
752
+ location = location.to(arguments.location) if arguments.parts.any?
753
+
754
+ Break.new(arguments: arguments, location: location)
755
+ end
756
+
757
+ # :call-seq:
758
+ # on_call: (
759
+ # untyped receiver,
760
+ # (:"::" | Op | Period) operator,
761
+ # (:call | Backtick | Const | Ident | Op) message
762
+ # ) -> Call
763
+ def on_call(receiver, operator, message)
764
+ ending = message
765
+ ending = operator if message == :call
766
+
767
+ Call.new(
768
+ receiver: receiver,
769
+ operator: operator,
770
+ message: message,
771
+ arguments: nil,
772
+ location: receiver.location.to(ending.location)
773
+ )
774
+ end
775
+
776
+ # :call-seq:
777
+ # on_case: (untyped value, untyped consequent) -> Case | RAssign
778
+ def on_case(value, consequent)
779
+ if keyword = find_token(Kw, "case", consume: false)
780
+ tokens.delete(keyword)
781
+
782
+ Case.new(
783
+ keyword: keyword,
784
+ value: value,
785
+ consequent: consequent,
786
+ location: keyword.location.to(consequent.location)
787
+ )
788
+ else
789
+ operator = find_token(Kw, "in", consume: false) || find_token(Op, "=>")
790
+
791
+ RAssign.new(
792
+ value: value,
793
+ operator: operator,
794
+ pattern: consequent,
795
+ location: value.location.to(consequent.location)
796
+ )
797
+ end
798
+ end
799
+
800
+ # :call-seq:
801
+ # on_class: (
802
+ # (ConstPathRef | ConstRef | TopConstRef) constant,
803
+ # untyped superclass,
804
+ # BodyStmt bodystmt
805
+ # ) -> ClassDeclaration
806
+ def on_class(constant, superclass, bodystmt)
807
+ beginning = find_token(Kw, "class")
808
+ ending = find_token(Kw, "end")
809
+ location = (superclass || constant).location
810
+ start_char = find_next_statement_start(location.end_char)
811
+
812
+ bodystmt.bind(
813
+ start_char,
814
+ start_char - line_counts[location.start_line - 1].start,
815
+ ending.location.start_char,
816
+ ending.location.start_column
817
+ )
818
+
819
+ ClassDeclaration.new(
820
+ constant: constant,
821
+ superclass: superclass,
822
+ bodystmt: bodystmt,
823
+ location: beginning.location.to(ending.location)
824
+ )
825
+ end
826
+
827
+ # :call-seq:
828
+ # on_comma: (String value) -> Comma
829
+ def on_comma(value)
830
+ node =
831
+ Comma.new(
832
+ value: value,
833
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
834
+ )
835
+
836
+ tokens << node
837
+ node
838
+ end
839
+
840
+ # :call-seq:
841
+ # on_command: ((Const | Ident) message, Args arguments) -> Command
842
+ def on_command(message, arguments)
843
+ Command.new(
844
+ message: message,
845
+ arguments: arguments,
846
+ location: message.location.to(arguments.location)
847
+ )
848
+ end
849
+
850
+ # :call-seq:
851
+ # on_command_call: (
852
+ # untyped receiver,
853
+ # (:"::" | Op | Period) operator,
854
+ # (Const | Ident | Op) message,
855
+ # (nil | Args) arguments
856
+ # ) -> CommandCall
857
+ def on_command_call(receiver, operator, message, arguments)
858
+ ending = arguments || message
859
+
860
+ CommandCall.new(
861
+ receiver: receiver,
862
+ operator: operator,
863
+ message: message,
864
+ arguments: arguments,
865
+ location: receiver.location.to(ending.location)
866
+ )
867
+ end
868
+
869
+ # :call-seq:
870
+ # on_comment: (String value) -> Comment
871
+ def on_comment(value)
872
+ line = lineno
873
+ comment =
874
+ Comment.new(
875
+ value: value.chomp,
876
+ inline: value.strip != lines[line - 1].strip,
877
+ location:
878
+ Location.token(line: line, char: char_pos, column: current_column, size: value.size - 1)
879
+ )
880
+
881
+ @comments << comment
882
+ comment
883
+ end
884
+
885
+ # :call-seq:
886
+ # on_const: (String value) -> Const
887
+ def on_const(value)
888
+ Const.new(
889
+ value: value,
890
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
891
+ )
892
+ end
893
+
894
+ # :call-seq:
895
+ # on_const_path_field: (untyped parent, Const constant) -> ConstPathField
896
+ def on_const_path_field(parent, constant)
897
+ ConstPathField.new(
898
+ parent: parent,
899
+ constant: constant,
900
+ location: parent.location.to(constant.location)
901
+ )
902
+ end
903
+
904
+ # :call-seq:
905
+ # on_const_path_ref: (untyped parent, Const constant) -> ConstPathRef
906
+ def on_const_path_ref(parent, constant)
907
+ ConstPathRef.new(
908
+ parent: parent,
909
+ constant: constant,
910
+ location: parent.location.to(constant.location)
911
+ )
912
+ end
913
+
914
+ # :call-seq:
915
+ # on_const_ref: (Const constant) -> ConstRef
916
+ def on_const_ref(constant)
917
+ ConstRef.new(constant: constant, location: constant.location)
918
+ end
919
+
920
+ # :call-seq:
921
+ # on_cvar: (String value) -> CVar
922
+ def on_cvar(value)
923
+ CVar.new(
924
+ value: value,
925
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
926
+ )
927
+ end
928
+
929
+ # :call-seq:
930
+ # on_def: (
931
+ # (Backtick | Const | Ident | Kw | Op) name,
932
+ # (nil | Params | Paren) params,
933
+ # untyped bodystmt
934
+ # ) -> Def | DefEndless
935
+ def on_def(name, params, bodystmt)
936
+ # Make sure to delete this token in case you're defining something like
937
+ # def class which would lead to this being a kw and causing all kinds of
938
+ # trouble
939
+ tokens.delete(name)
940
+
941
+ # Find the beginning of the method definition, which works for single-line
942
+ # and normal method definitions.
943
+ beginning = find_token(Kw, "def")
944
+
945
+ # If there aren't any params then we need to correct the params node
946
+ # location information
947
+ if params.is_a?(Params) && params.empty?
948
+ end_char = name.location.end_char
949
+ end_column = name.location.end_column
950
+ location =
951
+ Location.new(
952
+ start_line: params.location.start_line,
953
+ start_char: end_char,
954
+ start_column: end_column,
955
+ end_line: params.location.end_line,
956
+ end_char: end_char,
957
+ end_column: end_column
958
+ )
959
+
960
+ params = Params.new(location: location)
961
+ end
962
+
963
+ ending = find_token(Kw, "end", consume: false)
964
+
965
+ if ending
966
+ tokens.delete(ending)
967
+ start_char = find_next_statement_start(params.location.end_char)
968
+
969
+ bodystmt.bind(
970
+ start_char,
971
+ start_char - line_counts[params.location.start_line - 1].start,
972
+ ending.location.start_char,
973
+ ending.location.start_column
974
+ )
975
+
976
+ Def.new(
977
+ name: name,
978
+ params: params,
979
+ bodystmt: bodystmt,
980
+ location: beginning.location.to(ending.location)
981
+ )
982
+ else
983
+ # In Ruby >= 3.1.0, this is a BodyStmt that wraps a single statement in
984
+ # the statements list. Before, it was just the individual statement.
985
+ statement = bodystmt.is_a?(BodyStmt) ? bodystmt.statements : bodystmt
986
+
987
+ DefEndless.new(
988
+ target: nil,
989
+ operator: nil,
990
+ name: name,
991
+ paren: params,
992
+ statement: statement,
993
+ location: beginning.location.to(bodystmt.location)
994
+ )
995
+ end
996
+ end
997
+
998
+ # :call-seq:
999
+ # on_defined: (untyped value) -> Defined
1000
+ def on_defined(value)
1001
+ beginning = find_token(Kw, "defined?")
1002
+ ending = value
1003
+
1004
+ range = beginning.location.end_char...value.location.start_char
1005
+ if source[range].include?("(")
1006
+ find_token(LParen)
1007
+ ending = find_token(RParen)
1008
+ end
1009
+
1010
+ Defined.new(value: value, location: beginning.location.to(ending.location))
1011
+ end
1012
+
1013
+ # :call-seq:
1014
+ # on_defs: (
1015
+ # untyped target,
1016
+ # (Op | Period) operator,
1017
+ # (Backtick | Const | Ident | Kw | Op) name,
1018
+ # (Params | Paren) params,
1019
+ # BodyStmt bodystmt
1020
+ # ) -> Defs
1021
+ def on_defs(target, operator, name, params, bodystmt)
1022
+ # Make sure to delete this token in case you're defining something
1023
+ # like def class which would lead to this being a kw and causing all kinds
1024
+ # of trouble
1025
+ tokens.delete(name)
1026
+
1027
+ # If there aren't any params then we need to correct the params node
1028
+ # location information
1029
+ if params.is_a?(Params) && params.empty?
1030
+ end_char = name.location.end_char
1031
+ end_column = name.location.end_column
1032
+ location =
1033
+ Location.new(
1034
+ start_line: params.location.start_line,
1035
+ start_char: end_char,
1036
+ start_column: end_column,
1037
+ end_line: params.location.end_line,
1038
+ end_char: end_char,
1039
+ end_column: end_column
1040
+ )
1041
+
1042
+ params = Params.new(location: location)
1043
+ end
1044
+
1045
+ beginning = find_token(Kw, "def")
1046
+ ending = find_token(Kw, "end", consume: false)
1047
+
1048
+ if ending
1049
+ tokens.delete(ending)
1050
+ start_char = find_next_statement_start(params.location.end_char)
1051
+
1052
+ bodystmt.bind(
1053
+ start_char,
1054
+ start_char - line_counts[params.location.start_line - 1].start,
1055
+ ending.location.start_char,
1056
+ ending.location.start_column
1057
+ )
1058
+
1059
+ Defs.new(
1060
+ target: target,
1061
+ operator: operator,
1062
+ name: name,
1063
+ params: params,
1064
+ bodystmt: bodystmt,
1065
+ location: beginning.location.to(ending.location)
1066
+ )
1067
+ else
1068
+ # In Ruby >= 3.1.0, this is a BodyStmt that wraps a single statement in
1069
+ # the statements list. Before, it was just the individual statement.
1070
+ statement = bodystmt.is_a?(BodyStmt) ? bodystmt.statements : bodystmt
1071
+
1072
+ DefEndless.new(
1073
+ target: target,
1074
+ operator: operator,
1075
+ name: name,
1076
+ paren: params,
1077
+ statement: statement,
1078
+ location: beginning.location.to(bodystmt.location)
1079
+ )
1080
+ end
1081
+ end
1082
+
1083
+ # :call-seq:
1084
+ # on_do_block: (BlockVar block_var, BodyStmt bodystmt) -> DoBlock
1085
+ def on_do_block(block_var, bodystmt)
1086
+ beginning = find_token(Kw, "do")
1087
+ ending = find_token(Kw, "end")
1088
+ location = (block_var || beginning).location
1089
+ start_char = find_next_statement_start(location.end_char)
1090
+
1091
+ bodystmt.bind(
1092
+ start_char,
1093
+ start_char - line_counts[location.start_line - 1].start,
1094
+ ending.location.start_char,
1095
+ ending.location.start_column
1096
+ )
1097
+
1098
+ DoBlock.new(
1099
+ keyword: beginning,
1100
+ block_var: block_var,
1101
+ bodystmt: bodystmt,
1102
+ location: beginning.location.to(ending.location)
1103
+ )
1104
+ end
1105
+
1106
+ # :call-seq:
1107
+ # on_dot2: ((nil | untyped) left, (nil | untyped) right) -> Dot2
1108
+ def on_dot2(left, right)
1109
+ operator = find_token(Op, "..")
1110
+
1111
+ beginning = left || operator
1112
+ ending = right || operator
1113
+
1114
+ Dot2.new(
1115
+ left: left,
1116
+ right: right,
1117
+ location: beginning.location.to(ending.location)
1118
+ )
1119
+ end
1120
+
1121
+ # :call-seq:
1122
+ # on_dot3: ((nil | untyped) left, (nil | untyped) right) -> Dot3
1123
+ def on_dot3(left, right)
1124
+ operator = find_token(Op, "...")
1125
+
1126
+ beginning = left || operator
1127
+ ending = right || operator
1128
+
1129
+ Dot3.new(
1130
+ left: left,
1131
+ right: right,
1132
+ location: beginning.location.to(ending.location)
1133
+ )
1134
+ end
1135
+
1136
+ # :call-seq:
1137
+ # on_dyna_symbol: (StringContent string_content) -> DynaSymbol
1138
+ def on_dyna_symbol(string_content)
1139
+ if find_token(SymBeg, consume: false)
1140
+ # A normal dynamic symbol
1141
+ symbeg = find_token(SymBeg)
1142
+ tstring_end = find_token(TStringEnd, location: symbeg.location)
1143
+
1144
+ DynaSymbol.new(
1145
+ quote: symbeg.value,
1146
+ parts: string_content.parts,
1147
+ location: symbeg.location.to(tstring_end.location)
1148
+ )
1149
+ else
1150
+ # A dynamic symbol as a hash key
1151
+ tstring_beg = find_token(TStringBeg)
1152
+ label_end = find_token(LabelEnd)
1153
+
1154
+ DynaSymbol.new(
1155
+ parts: string_content.parts,
1156
+ quote: label_end.value[0],
1157
+ location: tstring_beg.location.to(label_end.location)
1158
+ )
1159
+ end
1160
+ end
1161
+
1162
+ # :call-seq:
1163
+ # on_else: (Statements statements) -> Else
1164
+ def on_else(statements)
1165
+ keyword = find_token(Kw, "else")
1166
+
1167
+ # else can either end with an end keyword (in which case we'll want to
1168
+ # consume that event) or it can end with an ensure keyword (in which case
1169
+ # we'll leave that to the ensure to handle).
1170
+ index =
1171
+ tokens.rindex do |token|
1172
+ token.is_a?(Kw) && %w[end ensure].include?(token.value)
1173
+ end
1174
+
1175
+ node = tokens[index]
1176
+ ending = node.value == "end" ? tokens.delete_at(index) : node
1177
+ start_char = find_next_statement_start(keyword.location.end_char)
1178
+
1179
+ statements.bind(
1180
+ start_char,
1181
+ start_char - line_counts[keyword.location.start_line - 1].start,
1182
+ ending.location.start_char,
1183
+ ending.location.start_column
1184
+ )
1185
+
1186
+ Else.new(
1187
+ keyword: keyword,
1188
+ statements: statements,
1189
+ location: keyword.location.to(ending.location)
1190
+ )
1191
+ end
1192
+
1193
+ # :call-seq:
1194
+ # on_elsif: (
1195
+ # untyped predicate,
1196
+ # Statements statements,
1197
+ # (nil | Elsif | Else) consequent
1198
+ # ) -> Elsif
1199
+ def on_elsif(predicate, statements, consequent)
1200
+ beginning = find_token(Kw, "elsif")
1201
+ ending = consequent || find_token(Kw, "end")
1202
+
1203
+ statements.bind(
1204
+ predicate.location.end_char,
1205
+ predicate.location.end_column,
1206
+ ending.location.start_char,
1207
+ ending.location.start_column
1208
+ )
1209
+
1210
+ Elsif.new(
1211
+ predicate: predicate,
1212
+ statements: statements,
1213
+ consequent: consequent,
1214
+ location: beginning.location.to(ending.location)
1215
+ )
1216
+ end
1217
+
1218
+ # :call-seq:
1219
+ # on_embdoc: (String value) -> EmbDoc
1220
+ def on_embdoc(value)
1221
+ @embdoc.value << value
1222
+ @embdoc
1223
+ end
1224
+
1225
+ # :call-seq:
1226
+ # on_embdoc_beg: (String value) -> EmbDoc
1227
+ def on_embdoc_beg(value)
1228
+ @embdoc =
1229
+ EmbDoc.new(
1230
+ value: value,
1231
+ location: Location.fixed(line: lineno, column: current_column, char: char_pos)
1232
+ )
1233
+ end
1234
+
1235
+ # :call-seq:
1236
+ # on_embdoc_end: (String value) -> EmbDoc
1237
+ def on_embdoc_end(value)
1238
+ location = @embdoc.location
1239
+ embdoc =
1240
+ EmbDoc.new(
1241
+ value: @embdoc.value << value.chomp,
1242
+ location:
1243
+ Location.new(
1244
+ start_line: location.start_line,
1245
+ start_char: location.start_char,
1246
+ start_column: location.start_column,
1247
+ end_line: lineno,
1248
+ end_char: char_pos + value.length - 1,
1249
+ end_column: current_column + value.length - 1
1250
+ )
1251
+ )
1252
+
1253
+ @comments << embdoc
1254
+ @embdoc = nil
1255
+
1256
+ embdoc
1257
+ end
1258
+
1259
+ # :call-seq:
1260
+ # on_embexpr_beg: (String value) -> EmbExprBeg
1261
+ def on_embexpr_beg(value)
1262
+ node =
1263
+ EmbExprBeg.new(
1264
+ value: value,
1265
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
1266
+ )
1267
+
1268
+ tokens << node
1269
+ node
1270
+ end
1271
+
1272
+ # :call-seq:
1273
+ # on_embexpr_end: (String value) -> EmbExprEnd
1274
+ def on_embexpr_end(value)
1275
+ node =
1276
+ EmbExprEnd.new(
1277
+ value: value,
1278
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
1279
+ )
1280
+
1281
+ tokens << node
1282
+ node
1283
+ end
1284
+
1285
+ # :call-seq:
1286
+ # on_embvar: (String value) -> EmbVar
1287
+ def on_embvar(value)
1288
+ node =
1289
+ EmbVar.new(
1290
+ value: value,
1291
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
1292
+ )
1293
+
1294
+ tokens << node
1295
+ node
1296
+ end
1297
+
1298
+ # :call-seq:
1299
+ # on_ensure: (Statements statements) -> Ensure
1300
+ def on_ensure(statements)
1301
+ keyword = find_token(Kw, "ensure")
1302
+
1303
+ # We don't want to consume the :@kw event, because that would break
1304
+ # def..ensure..end chains.
1305
+ ending = find_token(Kw, "end", consume: false)
1306
+ start_char = find_next_statement_start(keyword.location.end_char)
1307
+ statements.bind(
1308
+ start_char,
1309
+ start_char - line_counts[keyword.location.start_line - 1].start,
1310
+ ending.location.start_char,
1311
+ ending.location.start_column
1312
+ )
1313
+
1314
+ Ensure.new(
1315
+ keyword: keyword,
1316
+ statements: statements,
1317
+ location: keyword.location.to(ending.location)
1318
+ )
1319
+ end
1320
+
1321
+ # The handler for this event accepts no parameters (though in previous
1322
+ # versions of Ruby it accepted a string literal with a value of ",").
1323
+ #
1324
+ # :call-seq:
1325
+ # on_excessed_comma: () -> ExcessedComma
1326
+ def on_excessed_comma(*)
1327
+ comma = find_token(Comma)
1328
+
1329
+ ExcessedComma.new(value: comma.value, location: comma.location)
1330
+ end
1331
+
1332
+ # :call-seq:
1333
+ # on_fcall: ((Const | Ident) value) -> FCall
1334
+ def on_fcall(value)
1335
+ FCall.new(value: value, arguments: nil, location: value.location)
1336
+ end
1337
+
1338
+ # :call-seq:
1339
+ # on_field: (
1340
+ # untyped parent,
1341
+ # (:"::" | Op | Period) operator
1342
+ # (Const | Ident) name
1343
+ # ) -> Field
1344
+ def on_field(parent, operator, name)
1345
+ Field.new(
1346
+ parent: parent,
1347
+ operator: operator,
1348
+ name: name,
1349
+ location: parent.location.to(name.location)
1350
+ )
1351
+ end
1352
+
1353
+ # :call-seq:
1354
+ # on_float: (String value) -> FloatLiteral
1355
+ def on_float(value)
1356
+ FloatLiteral.new(
1357
+ value: value,
1358
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
1359
+ )
1360
+ end
1361
+
1362
+ # :call-seq:
1363
+ # on_fndptn: (
1364
+ # (nil | untyped) constant,
1365
+ # VarField left,
1366
+ # Array[untyped] values,
1367
+ # VarField right
1368
+ # ) -> FndPtn
1369
+ def on_fndptn(constant, left, values, right)
1370
+ beginning = constant || find_token(LBracket)
1371
+ ending = find_token(RBracket)
1372
+
1373
+ FndPtn.new(
1374
+ constant: constant,
1375
+ left: left,
1376
+ values: values,
1377
+ right: right,
1378
+ location: beginning.location.to(ending.location)
1379
+ )
1380
+ end
1381
+
1382
+ # :call-seq:
1383
+ # on_for: (
1384
+ # (MLHS | VarField) value,
1385
+ # untyped collection,
1386
+ # Statements statements
1387
+ # ) -> For
1388
+ def on_for(index, collection, statements)
1389
+ beginning = find_token(Kw, "for")
1390
+ in_keyword = find_token(Kw, "in")
1391
+ ending = find_token(Kw, "end")
1392
+
1393
+ # Consume the do keyword if it exists so that it doesn't get confused for
1394
+ # some other block
1395
+ keyword = find_token(Kw, "do", consume: false)
1396
+ if keyword && keyword.location.start_char > collection.location.end_char &&
1397
+ keyword.location.end_char < ending.location.start_char
1398
+ tokens.delete(keyword)
1399
+ end
1400
+
1401
+ statements.bind(
1402
+ (keyword || collection).location.end_char,
1403
+ (keyword || collection).location.end_column,
1404
+ ending.location.start_char,
1405
+ ending.location.start_column
1406
+ )
1407
+
1408
+ if index.is_a?(MLHS)
1409
+ comma_range = index.location.end_char...in_keyword.location.start_char
1410
+ index.comma = true if source[comma_range].strip.start_with?(",")
1411
+ end
1412
+
1413
+ For.new(
1414
+ index: index,
1415
+ collection: collection,
1416
+ statements: statements,
1417
+ location: beginning.location.to(ending.location)
1418
+ )
1419
+ end
1420
+
1421
+ # :call-seq:
1422
+ # on_gvar: (String value) -> GVar
1423
+ def on_gvar(value)
1424
+ GVar.new(
1425
+ value: value,
1426
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
1427
+ )
1428
+ end
1429
+
1430
+ # :call-seq:
1431
+ # on_hash: ((nil | Array[AssocNew | AssocSplat]) assocs) -> HashLiteral
1432
+ def on_hash(assocs)
1433
+ lbrace = find_token(LBrace)
1434
+ rbrace = find_token(RBrace)
1435
+
1436
+ HashLiteral.new(
1437
+ lbrace: lbrace,
1438
+ assocs: assocs || [],
1439
+ location: lbrace.location.to(rbrace.location)
1440
+ )
1441
+ end
1442
+
1443
+ # :call-seq:
1444
+ # on_heredoc_beg: (String value) -> HeredocBeg
1445
+ def on_heredoc_beg(value)
1446
+ location =
1447
+ Location.token(line: lineno, char: char_pos, column: current_column, size: value.size + 1)
1448
+
1449
+ # Here we're going to artificially create an extra node type so that if
1450
+ # there are comments after the declaration of a heredoc, they get printed.
1451
+ beginning = HeredocBeg.new(value: value, location: location)
1452
+ @heredocs << Heredoc.new(beginning: beginning, location: location)
1453
+
1454
+ beginning
1455
+ end
1456
+
1457
+ # :call-seq:
1458
+ # on_heredoc_dedent: (StringContent string, Integer width) -> Heredoc
1459
+ def on_heredoc_dedent(string, width)
1460
+ heredoc = @heredocs[-1]
1461
+
1462
+ @heredocs[-1] = Heredoc.new(
1463
+ beginning: heredoc.beginning,
1464
+ ending: heredoc.ending,
1465
+ parts: string.parts,
1466
+ location: heredoc.location
1467
+ )
1468
+ end
1469
+
1470
+ # :call-seq:
1471
+ # on_heredoc_end: (String value) -> Heredoc
1472
+ def on_heredoc_end(value)
1473
+ heredoc = @heredocs[-1]
1474
+
1475
+ @heredocs[-1] = Heredoc.new(
1476
+ beginning: heredoc.beginning,
1477
+ ending: value.chomp,
1478
+ parts: heredoc.parts,
1479
+ location:
1480
+ Location.new(
1481
+ start_line: heredoc.location.start_line,
1482
+ start_char: heredoc.location.start_char,
1483
+ start_column: heredoc.location.start_column,
1484
+ end_line: lineno,
1485
+ end_char: char_pos,
1486
+ end_column: current_column,
1487
+ )
1488
+ )
1489
+ end
1490
+
1491
+ # :call-seq:
1492
+ # on_hshptn: (
1493
+ # (nil | untyped) constant,
1494
+ # Array[[Label, untyped]] keywords,
1495
+ # (nil | VarField) keyword_rest
1496
+ # ) -> HshPtn
1497
+ def on_hshptn(constant, keywords, keyword_rest)
1498
+ parts = [constant, keywords, keyword_rest].flatten(2).compact
1499
+
1500
+ HshPtn.new(
1501
+ constant: constant,
1502
+ keywords: keywords,
1503
+ keyword_rest: keyword_rest,
1504
+ location: parts[0].location.to(parts[-1].location)
1505
+ )
1506
+ end
1507
+
1508
+ # :call-seq:
1509
+ # on_ident: (String value) -> Ident
1510
+ def on_ident(value)
1511
+ Ident.new(
1512
+ value: value,
1513
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
1514
+ )
1515
+ end
1516
+
1517
+ # :call-seq:
1518
+ # on_if: (
1519
+ # untyped predicate,
1520
+ # Statements statements,
1521
+ # (nil | Elsif | Else) consequent
1522
+ # ) -> If
1523
+ def on_if(predicate, statements, consequent)
1524
+ beginning = find_token(Kw, "if")
1525
+ ending = consequent || find_token(Kw, "end")
1526
+
1527
+ statements.bind(
1528
+ predicate.location.end_char,
1529
+ predicate.location.end_column,
1530
+ ending.location.start_char,
1531
+ ending.location.start_column
1532
+ )
1533
+
1534
+ If.new(
1535
+ predicate: predicate,
1536
+ statements: statements,
1537
+ consequent: consequent,
1538
+ location: beginning.location.to(ending.location)
1539
+ )
1540
+ end
1541
+
1542
+ # :call-seq:
1543
+ # on_ifop: (untyped predicate, untyped truthy, untyped falsy) -> IfOp
1544
+ def on_ifop(predicate, truthy, falsy)
1545
+ IfOp.new(
1546
+ predicate: predicate,
1547
+ truthy: truthy,
1548
+ falsy: falsy,
1549
+ location: predicate.location.to(falsy.location)
1550
+ )
1551
+ end
1552
+
1553
+ # :call-seq:
1554
+ # on_if_mod: (untyped predicate, untyped statement) -> IfMod
1555
+ def on_if_mod(predicate, statement)
1556
+ find_token(Kw, "if")
1557
+
1558
+ IfMod.new(
1559
+ statement: statement,
1560
+ predicate: predicate,
1561
+ location: statement.location.to(predicate.location)
1562
+ )
1563
+ end
1564
+
1565
+ # def on_ignored_nl(value)
1566
+ # value
1567
+ # end
1568
+
1569
+ # def on_ignored_sp(value)
1570
+ # value
1571
+ # end
1572
+
1573
+ # :call-seq:
1574
+ # on_imaginary: (String value) -> Imaginary
1575
+ def on_imaginary(value)
1576
+ Imaginary.new(
1577
+ value: value,
1578
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
1579
+ )
1580
+ end
1581
+
1582
+ # :call-seq:
1583
+ # on_in: (RAssign pattern, nil statements, nil consequent) -> RAssign
1584
+ # | (
1585
+ # untyped pattern,
1586
+ # Statements statements,
1587
+ # (nil | In | Else) consequent
1588
+ # ) -> In
1589
+ def on_in(pattern, statements, consequent)
1590
+ # Here we have a rightward assignment
1591
+ return pattern unless statements
1592
+
1593
+ beginning = find_token(Kw, "in")
1594
+ ending = consequent || find_token(Kw, "end")
1595
+
1596
+ statements_start = pattern
1597
+ if token = find_token(Kw, "then", consume: false)
1598
+ tokens.delete(token)
1599
+ statements_start = token
1600
+ end
1601
+
1602
+ start_char = find_next_statement_start(statements_start.location.end_char)
1603
+ statements.bind(
1604
+ start_char,
1605
+ start_char - line_counts[statements_start.location.start_line - 1].start,
1606
+ ending.location.start_char,
1607
+ ending.location.start_column
1608
+ )
1609
+
1610
+ In.new(
1611
+ pattern: pattern,
1612
+ statements: statements,
1613
+ consequent: consequent,
1614
+ location: beginning.location.to(ending.location)
1615
+ )
1616
+ end
1617
+
1618
+ # :call-seq:
1619
+ # on_int: (String value) -> Int
1620
+ def on_int(value)
1621
+ Int.new(
1622
+ value: value,
1623
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
1624
+ )
1625
+ end
1626
+
1627
+ # :call-seq:
1628
+ # on_ivar: (String value) -> IVar
1629
+ def on_ivar(value)
1630
+ IVar.new(
1631
+ value: value,
1632
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
1633
+ )
1634
+ end
1635
+
1636
+ # :call-seq:
1637
+ # on_kw: (String value) -> Kw
1638
+ def on_kw(value)
1639
+ node =
1640
+ Kw.new(
1641
+ value: value,
1642
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
1643
+ )
1644
+
1645
+ tokens << node
1646
+ node
1647
+ end
1648
+
1649
+ # :call-seq:
1650
+ # on_kwrest_param: ((nil | Ident) name) -> KwRestParam
1651
+ def on_kwrest_param(name)
1652
+ location = find_token(Op, "**").location
1653
+ location = location.to(name.location) if name
1654
+
1655
+ KwRestParam.new(name: name, location: location)
1656
+ end
1657
+
1658
+ # :call-seq:
1659
+ # on_label: (String value) -> Label
1660
+ def on_label(value)
1661
+ Label.new(
1662
+ value: value,
1663
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
1664
+ )
1665
+ end
1666
+
1667
+ # :call-seq:
1668
+ # on_label_end: (String value) -> LabelEnd
1669
+ def on_label_end(value)
1670
+ node =
1671
+ LabelEnd.new(
1672
+ value: value,
1673
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
1674
+ )
1675
+
1676
+ tokens << node
1677
+ node
1678
+ end
1679
+
1680
+ # :call-seq:
1681
+ # on_lambda: (
1682
+ # (Params | Paren) params,
1683
+ # (BodyStmt | Statements) statements
1684
+ # ) -> Lambda
1685
+ def on_lambda(params, statements)
1686
+ beginning = find_token(TLambda)
1687
+
1688
+ if tokens.any? { |token|
1689
+ token.is_a?(TLamBeg) &&
1690
+ token.location.start_char > beginning.location.start_char
1691
+ }
1692
+ opening = find_token(TLamBeg)
1693
+ closing = find_token(RBrace)
1694
+ else
1695
+ opening = find_token(Kw, "do")
1696
+ closing = find_token(Kw, "end")
1697
+ end
1698
+
1699
+ statements.bind(
1700
+ opening.location.end_char,
1701
+ opening.location.end_column,
1702
+ closing.location.start_char,
1703
+ closing.location.start_column
1704
+ )
1705
+
1706
+ Lambda.new(
1707
+ params: params,
1708
+ statements: statements,
1709
+ location: beginning.location.to(closing.location)
1710
+ )
1711
+ end
1712
+
1713
+ # :call-seq:
1714
+ # on_lbrace: (String value) -> LBrace
1715
+ def on_lbrace(value)
1716
+ node =
1717
+ LBrace.new(
1718
+ value: value,
1719
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
1720
+ )
1721
+
1722
+ tokens << node
1723
+ node
1724
+ end
1725
+
1726
+ # :call-seq:
1727
+ # on_lbracket: (String value) -> LBracket
1728
+ def on_lbracket(value)
1729
+ node =
1730
+ LBracket.new(
1731
+ value: value,
1732
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
1733
+ )
1734
+
1735
+ tokens << node
1736
+ node
1737
+ end
1738
+
1739
+ # :call-seq:
1740
+ # on_lparen: (String value) -> LParen
1741
+ def on_lparen(value)
1742
+ node =
1743
+ LParen.new(
1744
+ value: value,
1745
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
1746
+ )
1747
+
1748
+ tokens << node
1749
+ node
1750
+ end
1751
+
1752
+ # def on_magic_comment(key, value)
1753
+ # [key, value]
1754
+ # end
1755
+
1756
+ # :call-seq:
1757
+ # on_massign: ((MLHS | MLHSParen) target, untyped value) -> MAssign
1758
+ def on_massign(target, value)
1759
+ comma_range = target.location.end_char...value.location.start_char
1760
+ target.comma = true if source[comma_range].strip.start_with?(",")
1761
+
1762
+ MAssign.new(
1763
+ target: target,
1764
+ value: value,
1765
+ location: target.location.to(value.location)
1766
+ )
1767
+ end
1768
+
1769
+ # :call-seq:
1770
+ # on_method_add_arg: (
1771
+ # (Call | FCall) call,
1772
+ # (ArgParen | Args) arguments
1773
+ # ) -> Call | FCall
1774
+ def on_method_add_arg(call, arguments)
1775
+ location = call.location
1776
+ location = location.to(arguments.location) if arguments.is_a?(ArgParen)
1777
+
1778
+ if call.is_a?(FCall)
1779
+ FCall.new(value: call.value, arguments: arguments, location: location)
1780
+ else
1781
+ Call.new(
1782
+ receiver: call.receiver,
1783
+ operator: call.operator,
1784
+ message: call.message,
1785
+ arguments: arguments,
1786
+ location: location
1787
+ )
1788
+ end
1789
+ end
1790
+
1791
+ # :call-seq:
1792
+ # on_method_add_block: (
1793
+ # (Call | Command | CommandCall | FCall) call,
1794
+ # (BraceBlock | DoBlock) block
1795
+ # ) -> MethodAddBlock
1796
+ def on_method_add_block(call, block)
1797
+ MethodAddBlock.new(
1798
+ call: call,
1799
+ block: block,
1800
+ location: call.location.to(block.location)
1801
+ )
1802
+ end
1803
+
1804
+ # :call-seq:
1805
+ # on_mlhs_add: (
1806
+ # MLHS mlhs,
1807
+ # (ARefField | Field | Ident | MLHSParen | VarField) part
1808
+ # ) -> MLHS
1809
+ def on_mlhs_add(mlhs, part)
1810
+ location =
1811
+ mlhs.parts.empty? ? part.location : mlhs.location.to(part.location)
1812
+
1813
+ MLHS.new(parts: mlhs.parts << part, location: location)
1814
+ end
1815
+
1816
+ # :call-seq:
1817
+ # on_mlhs_add_post: (MLHS left, MLHS right) -> MLHS
1818
+ def on_mlhs_add_post(left, right)
1819
+ MLHS.new(
1820
+ parts: left.parts + right.parts,
1821
+ location: left.location.to(right.location)
1822
+ )
1823
+ end
1824
+
1825
+ # :call-seq:
1826
+ # on_mlhs_add_star: (
1827
+ # MLHS mlhs,
1828
+ # (nil | ARefField | Field | Ident | VarField) part
1829
+ # ) -> MLHS
1830
+ def on_mlhs_add_star(mlhs, part)
1831
+ beginning = find_token(Op, "*")
1832
+ ending = part || beginning
1833
+
1834
+ location = beginning.location.to(ending.location)
1835
+ arg_star = ArgStar.new(value: part, location: location)
1836
+
1837
+ location = mlhs.location.to(location) unless mlhs.parts.empty?
1838
+ MLHS.new(parts: mlhs.parts << arg_star, location: location)
1839
+ end
1840
+
1841
+ # :call-seq:
1842
+ # on_mlhs_new: () -> MLHS
1843
+ def on_mlhs_new
1844
+ MLHS.new(parts: [], location: Location.fixed(line: lineno, char: char_pos, column: current_column))
1845
+ end
1846
+
1847
+ # :call-seq:
1848
+ # on_mlhs_paren: ((MLHS | MLHSParen) contents) -> MLHSParen
1849
+ def on_mlhs_paren(contents)
1850
+ lparen = find_token(LParen)
1851
+ rparen = find_token(RParen)
1852
+
1853
+ comma_range = lparen.location.end_char...rparen.location.start_char
1854
+ contents.comma = true if source[comma_range].strip.end_with?(",")
1855
+
1856
+ MLHSParen.new(
1857
+ contents: contents,
1858
+ location: lparen.location.to(rparen.location)
1859
+ )
1860
+ end
1861
+
1862
+ # :call-seq:
1863
+ # on_module: (
1864
+ # (ConstPathRef | ConstRef | TopConstRef) constant,
1865
+ # BodyStmt bodystmt
1866
+ # ) -> ModuleDeclaration
1867
+ def on_module(constant, bodystmt)
1868
+ beginning = find_token(Kw, "module")
1869
+ ending = find_token(Kw, "end")
1870
+ start_char = find_next_statement_start(constant.location.end_char)
1871
+
1872
+ bodystmt.bind(
1873
+ start_char,
1874
+ start_char - line_counts[constant.location.start_line - 1].start,
1875
+ ending.location.start_char,
1876
+ ending.location.start_column
1877
+ )
1878
+
1879
+ ModuleDeclaration.new(
1880
+ constant: constant,
1881
+ bodystmt: bodystmt,
1882
+ location: beginning.location.to(ending.location)
1883
+ )
1884
+ end
1885
+
1886
+ # :call-seq:
1887
+ # on_mrhs_new: () -> MRHS
1888
+ def on_mrhs_new
1889
+ MRHS.new(parts: [], location: Location.fixed(line: lineno, char: char_pos, column: current_column))
1890
+ end
1891
+
1892
+ # :call-seq:
1893
+ # on_mrhs_add: (MRHS mrhs, untyped part) -> MRHS
1894
+ def on_mrhs_add(mrhs, part)
1895
+ location =
1896
+ if mrhs.parts.empty?
1897
+ mrhs.location
1898
+ else
1899
+ mrhs.location.to(part.location)
1900
+ end
1901
+
1902
+ MRHS.new(parts: mrhs.parts << part, location: location)
1903
+ end
1904
+
1905
+ # :call-seq:
1906
+ # on_mrhs_add_star: (MRHS mrhs, untyped value) -> MRHS
1907
+ def on_mrhs_add_star(mrhs, value)
1908
+ beginning = find_token(Op, "*")
1909
+ ending = value || beginning
1910
+
1911
+ arg_star =
1912
+ ArgStar.new(
1913
+ value: value,
1914
+ location: beginning.location.to(ending.location)
1915
+ )
1916
+
1917
+ location =
1918
+ if mrhs.parts.empty?
1919
+ arg_star.location
1920
+ else
1921
+ mrhs.location.to(arg_star.location)
1922
+ end
1923
+
1924
+ MRHS.new(parts: mrhs.parts << arg_star, location: location)
1925
+ end
1926
+
1927
+ # :call-seq:
1928
+ # on_mrhs_new_from_args: (Args arguments) -> MRHS
1929
+ def on_mrhs_new_from_args(arguments)
1930
+ MRHS.new(parts: arguments.parts, location: arguments.location)
1931
+ end
1932
+
1933
+ # :call-seq:
1934
+ # on_next: (Args arguments) -> Next
1935
+ def on_next(arguments)
1936
+ keyword = find_token(Kw, "next")
1937
+
1938
+ location = keyword.location
1939
+ location = location.to(arguments.location) if arguments.parts.any?
1940
+
1941
+ Next.new(arguments: arguments, location: location)
1942
+ end
1943
+
1944
+ # def on_nl(value)
1945
+ # value
1946
+ # end
1947
+
1948
+ # def on_nokw_param(value)
1949
+ # value
1950
+ # end
1951
+
1952
+ # :call-seq:
1953
+ # on_op: (String value) -> Op
1954
+ def on_op(value)
1955
+ node =
1956
+ Op.new(
1957
+ value: value,
1958
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
1959
+ )
1960
+
1961
+ tokens << node
1962
+ node
1963
+ end
1964
+
1965
+ # :call-seq:
1966
+ # on_opassign: (
1967
+ # (ARefField | ConstPathField | Field | TopConstField | VarField) target,
1968
+ # Op operator,
1969
+ # untyped value
1970
+ # ) -> OpAssign
1971
+ def on_opassign(target, operator, value)
1972
+ OpAssign.new(
1973
+ target: target,
1974
+ operator: operator,
1975
+ value: value,
1976
+ location: target.location.to(value.location)
1977
+ )
1978
+ end
1979
+
1980
+ # def on_operator_ambiguous(value)
1981
+ # value
1982
+ # end
1983
+
1984
+ # :call-seq:
1985
+ # on_params: (
1986
+ # (nil | Array[Ident]) requireds,
1987
+ # (nil | Array[[Ident, untyped]]) optionals,
1988
+ # (nil | ArgsForward | ExcessedComma | RestParam) rest,
1989
+ # (nil | Array[Ident]) posts,
1990
+ # (nil | Array[[Ident, nil | untyped]]) keywords,
1991
+ # (nil | :nil | ArgsForward | KwRestParam) keyword_rest,
1992
+ # (nil | :& | BlockArg) block
1993
+ # ) -> Params
1994
+ def on_params(
1995
+ requireds,
1996
+ optionals,
1997
+ rest,
1998
+ posts,
1999
+ keywords,
2000
+ keyword_rest,
2001
+ block
2002
+ )
2003
+ parts = [
2004
+ *requireds,
2005
+ *optionals&.flatten(1),
2006
+ rest,
2007
+ *posts,
2008
+ *keywords&.flat_map { |(key, value)| [key, value || nil] },
2009
+ (keyword_rest if keyword_rest != :nil),
2010
+ (block if block != :&)
2011
+ ].compact
2012
+
2013
+ location =
2014
+ if parts.any?
2015
+ parts[0].location.to(parts[-1].location)
2016
+ else
2017
+ Location.fixed(line: lineno, char: char_pos, column: current_column)
2018
+ end
2019
+
2020
+ Params.new(
2021
+ requireds: requireds || [],
2022
+ optionals: optionals || [],
2023
+ rest: rest,
2024
+ posts: posts || [],
2025
+ keywords: keywords || [],
2026
+ keyword_rest: keyword_rest,
2027
+ block: (block if block != :&),
2028
+ location: location
2029
+ )
2030
+ end
2031
+
2032
+ # :call-seq:
2033
+ # on_paren: (untyped contents) -> Paren
2034
+ def on_paren(contents)
2035
+ lparen = find_token(LParen)
2036
+ rparen = find_token(RParen)
2037
+
2038
+ if contents && contents.is_a?(Params)
2039
+ location = contents.location
2040
+ start_char = find_next_statement_start(lparen.location.end_char)
2041
+ location =
2042
+ Location.new(
2043
+ start_line: location.start_line,
2044
+ start_char: start_char,
2045
+ start_column: start_char - line_counts[lparen.location.start_line - 1].start,
2046
+ end_line: location.end_line,
2047
+ end_char: rparen.location.start_char,
2048
+ end_column: rparen.location.start_column
2049
+ )
2050
+
2051
+ contents =
2052
+ Params.new(
2053
+ requireds: contents.requireds,
2054
+ optionals: contents.optionals,
2055
+ rest: contents.rest,
2056
+ posts: contents.posts,
2057
+ keywords: contents.keywords,
2058
+ keyword_rest: contents.keyword_rest,
2059
+ block: contents.block,
2060
+ location: location
2061
+ )
2062
+ end
2063
+
2064
+ Paren.new(
2065
+ lparen: lparen,
2066
+ contents: contents || nil,
2067
+ location: lparen.location.to(rparen.location)
2068
+ )
2069
+ end
2070
+
2071
+ # If we encounter a parse error, just immediately bail out so that our
2072
+ # runner can catch it.
2073
+ def on_parse_error(error, *)
2074
+ raise ParseError.new(error, lineno, column)
2075
+ end
2076
+ alias on_alias_error on_parse_error
2077
+ alias on_assign_error on_parse_error
2078
+ alias on_class_name_error on_parse_error
2079
+ alias on_param_error on_parse_error
2080
+
2081
+ # :call-seq:
2082
+ # on_period: (String value) -> Period
2083
+ def on_period(value)
2084
+ Period.new(
2085
+ value: value,
2086
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
2087
+ )
2088
+ end
2089
+
2090
+ # :call-seq:
2091
+ # on_program: (Statements statements) -> Program
2092
+ def on_program(statements)
2093
+ last_column = source.length - line_counts[lines.length - 1].start
2094
+ location =
2095
+ Location.new(
2096
+ start_line: 1,
2097
+ start_char: 0,
2098
+ start_column: 0,
2099
+ end_line: lines.length,
2100
+ end_char: source.length,
2101
+ end_column: last_column
2102
+ )
2103
+
2104
+ statements.body << @__end__ if @__end__
2105
+ statements.bind(0, 0, source.length, last_column)
2106
+
2107
+ program = Program.new(statements: statements, location: location)
2108
+ attach_comments(program, @comments)
2109
+
2110
+ program
2111
+ end
2112
+
2113
+ # Attaches comments to the nodes in the tree that most closely correspond to
2114
+ # the location of the comments.
2115
+ def attach_comments(program, comments)
2116
+ comments.each do |comment|
2117
+ preceding, enclosing, following = nearest_nodes(program, comment)
2118
+
2119
+ if comment.inline?
2120
+ if preceding
2121
+ preceding.comments << comment
2122
+ comment.trailing!
2123
+ elsif following
2124
+ following.comments << comment
2125
+ comment.leading!
2126
+ elsif enclosing
2127
+ enclosing.comments << comment
2128
+ else
2129
+ program.comments << comment
2130
+ end
2131
+ else
2132
+ # If a comment exists on its own line, prefer a leading comment.
2133
+ if following
2134
+ following.comments << comment
2135
+ comment.leading!
2136
+ elsif preceding
2137
+ preceding.comments << comment
2138
+ comment.trailing!
2139
+ elsif enclosing
2140
+ enclosing.comments << comment
2141
+ else
2142
+ program.comments << comment
2143
+ end
2144
+ end
2145
+ end
2146
+ end
2147
+
2148
+ # Responsible for finding the nearest nodes to the given comment within the
2149
+ # context of the given encapsulating node.
2150
+ def nearest_nodes(node, comment)
2151
+ comment_start = comment.location.start_char
2152
+ comment_end = comment.location.end_char
2153
+
2154
+ child_nodes = node.child_nodes.compact
2155
+ preceding = nil
2156
+ following = nil
2157
+
2158
+ left = 0
2159
+ right = child_nodes.length
2160
+
2161
+ # This is a custom binary search that finds the nearest nodes to the given
2162
+ # comment. When it finds a node that completely encapsulates the comment,
2163
+ # it recursed downward into the tree.
2164
+ while left < right
2165
+ middle = (left + right) / 2
2166
+ child = child_nodes[middle]
2167
+
2168
+ node_start = child.location.start_char
2169
+ node_end = child.location.end_char
2170
+
2171
+ if node_start <= comment_start && comment_end <= node_end
2172
+ # The comment is completely contained by this child node. Abandon the
2173
+ # binary search at this level.
2174
+ return nearest_nodes(child, comment)
2175
+ end
2176
+
2177
+ if node_end <= comment_start
2178
+ # This child node falls completely before the comment. Because we will
2179
+ # never consider this node or any nodes before it again, this node
2180
+ # must be the closest preceding node we have encountered so far.
2181
+ preceding = child
2182
+ left = middle + 1
2183
+ next
2184
+ end
2185
+
2186
+ if comment_end <= node_start
2187
+ # This child node falls completely after the comment. Because we will
2188
+ # never consider this node or any nodes after it again, this node must
2189
+ # be the closest following node we have encountered so far.
2190
+ following = child
2191
+ right = middle
2192
+ next
2193
+ end
2194
+
2195
+ # This should only happen if there is a bug in this parser.
2196
+ raise "Comment location overlaps with node location"
2197
+ end
2198
+
2199
+ [preceding, node, following]
2200
+ end
2201
+
2202
+ # :call-seq:
2203
+ # on_qsymbols_add: (QSymbols qsymbols, TStringContent element) -> QSymbols
2204
+ def on_qsymbols_add(qsymbols, element)
2205
+ QSymbols.new(
2206
+ beginning: qsymbols.beginning,
2207
+ elements: qsymbols.elements << element,
2208
+ location: qsymbols.location.to(element.location)
2209
+ )
2210
+ end
2211
+
2212
+ # :call-seq:
2213
+ # on_qsymbols_beg: (String value) -> QSymbolsBeg
2214
+ def on_qsymbols_beg(value)
2215
+ node =
2216
+ QSymbolsBeg.new(
2217
+ value: value,
2218
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
2219
+ )
2220
+
2221
+ tokens << node
2222
+ node
2223
+ end
2224
+
2225
+ # :call-seq:
2226
+ # on_qsymbols_new: () -> QSymbols
2227
+ def on_qsymbols_new
2228
+ beginning = find_token(QSymbolsBeg)
2229
+
2230
+ QSymbols.new(
2231
+ beginning: beginning,
2232
+ elements: [],
2233
+ location: beginning.location
2234
+ )
2235
+ end
2236
+
2237
+ # :call-seq:
2238
+ # on_qwords_add: (QWords qwords, TStringContent element) -> QWords
2239
+ def on_qwords_add(qwords, element)
2240
+ QWords.new(
2241
+ beginning: qwords.beginning,
2242
+ elements: qwords.elements << element,
2243
+ location: qwords.location.to(element.location)
2244
+ )
2245
+ end
2246
+
2247
+ # :call-seq:
2248
+ # on_qwords_beg: (String value) -> QWordsBeg
2249
+ def on_qwords_beg(value)
2250
+ node =
2251
+ QWordsBeg.new(
2252
+ value: value,
2253
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
2254
+ )
2255
+
2256
+ tokens << node
2257
+ node
2258
+ end
2259
+
2260
+ # :call-seq:
2261
+ # on_qwords_new: () -> QWords
2262
+ def on_qwords_new
2263
+ beginning = find_token(QWordsBeg)
2264
+
2265
+ QWords.new(beginning: beginning, elements: [], location: beginning.location)
2266
+ end
2267
+
2268
+ # :call-seq:
2269
+ # on_rational: (String value) -> RationalLiteral
2270
+ def on_rational(value)
2271
+ RationalLiteral.new(
2272
+ value: value,
2273
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
2274
+ )
2275
+ end
2276
+
2277
+ # :call-seq:
2278
+ # on_rbrace: (String value) -> RBrace
2279
+ def on_rbrace(value)
2280
+ node =
2281
+ RBrace.new(
2282
+ value: value,
2283
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
2284
+ )
2285
+
2286
+ tokens << node
2287
+ node
2288
+ end
2289
+
2290
+ # :call-seq:
2291
+ # on_rbracket: (String value) -> RBracket
2292
+ def on_rbracket(value)
2293
+ node =
2294
+ RBracket.new(
2295
+ value: value,
2296
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
2297
+ )
2298
+
2299
+ tokens << node
2300
+ node
2301
+ end
2302
+
2303
+ # :call-seq:
2304
+ # on_redo: () -> Redo
2305
+ def on_redo
2306
+ keyword = find_token(Kw, "redo")
2307
+
2308
+ Redo.new(value: keyword.value, location: keyword.location)
2309
+ end
2310
+
2311
+ # :call-seq:
2312
+ # on_regexp_add: (
2313
+ # RegexpContent regexp_content,
2314
+ # (StringDVar | StringEmbExpr | TStringContent) part
2315
+ # ) -> RegexpContent
2316
+ def on_regexp_add(regexp_content, part)
2317
+ RegexpContent.new(
2318
+ beginning: regexp_content.beginning,
2319
+ parts: regexp_content.parts << part,
2320
+ location: regexp_content.location.to(part.location)
2321
+ )
2322
+ end
2323
+
2324
+ # :call-seq:
2325
+ # on_regexp_beg: (String value) -> RegexpBeg
2326
+ def on_regexp_beg(value)
2327
+ node =
2328
+ RegexpBeg.new(
2329
+ value: value,
2330
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
2331
+ )
2332
+
2333
+ tokens << node
2334
+ node
2335
+ end
2336
+
2337
+ # :call-seq:
2338
+ # on_regexp_end: (String value) -> RegexpEnd
2339
+ def on_regexp_end(value)
2340
+ RegexpEnd.new(
2341
+ value: value,
2342
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
2343
+ )
2344
+ end
2345
+
2346
+ # :call-seq:
2347
+ # on_regexp_literal: (
2348
+ # RegexpContent regexp_content,
2349
+ # RegexpEnd ending
2350
+ # ) -> RegexpLiteral
2351
+ def on_regexp_literal(regexp_content, ending)
2352
+ RegexpLiteral.new(
2353
+ beginning: regexp_content.beginning,
2354
+ ending: ending.value,
2355
+ parts: regexp_content.parts,
2356
+ location: regexp_content.location.to(ending.location)
2357
+ )
2358
+ end
2359
+
2360
+ # :call-seq:
2361
+ # on_regexp_new: () -> RegexpContent
2362
+ def on_regexp_new
2363
+ regexp_beg = find_token(RegexpBeg)
2364
+
2365
+ RegexpContent.new(
2366
+ beginning: regexp_beg.value,
2367
+ parts: [],
2368
+ location: regexp_beg.location
2369
+ )
2370
+ end
2371
+
2372
+ # :call-seq:
2373
+ # on_rescue: (
2374
+ # (nil | [untyped] | MRHS | MRHSAddStar) exceptions,
2375
+ # (nil | Field | VarField) variable,
2376
+ # Statements statements,
2377
+ # (nil | Rescue) consequent
2378
+ # ) -> Rescue
2379
+ def on_rescue(exceptions, variable, statements, consequent)
2380
+ keyword = find_token(Kw, "rescue")
2381
+ exceptions = exceptions[0] if exceptions.is_a?(Array)
2382
+
2383
+ last_node = variable || exceptions || keyword
2384
+ start_char = find_next_statement_start(last_node.location.end_char)
2385
+ statements.bind(
2386
+ start_char,
2387
+ start_char - line_counts[last_node.location.start_line - 1].start,
2388
+ char_pos,
2389
+ current_column
2390
+ )
2391
+
2392
+ # We add an additional inner node here that ripper doesn't provide so that
2393
+ # we have a nice place to attach inline comments. But we only need it if
2394
+ # we have an exception or a variable that we're rescuing.
2395
+ rescue_ex =
2396
+ if exceptions || variable
2397
+ RescueEx.new(
2398
+ exceptions: exceptions,
2399
+ variable: variable,
2400
+ location:
2401
+ Location.new(
2402
+ start_line: keyword.location.start_line,
2403
+ start_char: keyword.location.end_char + 1,
2404
+ start_column: keyword.location.end_column + 1,
2405
+ end_line: last_node.location.end_line,
2406
+ end_char: last_node.location.end_char,
2407
+ end_column: last_node.location.end_column
2408
+ )
2409
+ )
2410
+ end
2411
+
2412
+ Rescue.new(
2413
+ keyword: keyword,
2414
+ exception: rescue_ex,
2415
+ statements: statements,
2416
+ consequent: consequent,
2417
+ location:
2418
+ Location.new(
2419
+ start_line: keyword.location.start_line,
2420
+ start_char: keyword.location.start_char,
2421
+ start_column: keyword.location.start_column,
2422
+ end_line: lineno,
2423
+ end_char: char_pos,
2424
+ end_column: current_column
2425
+ )
2426
+ )
2427
+ end
2428
+
2429
+ # :call-seq:
2430
+ # on_rescue_mod: (untyped statement, untyped value) -> RescueMod
2431
+ def on_rescue_mod(statement, value)
2432
+ find_token(Kw, "rescue")
2433
+
2434
+ RescueMod.new(
2435
+ statement: statement,
2436
+ value: value,
2437
+ location: statement.location.to(value.location)
2438
+ )
2439
+ end
2440
+
2441
+ # :call-seq:
2442
+ # on_rest_param: ((nil | Ident) name) -> RestParam
2443
+ def on_rest_param(name)
2444
+ location = find_token(Op, "*").location
2445
+ location = location.to(name.location) if name
2446
+
2447
+ RestParam.new(name: name, location: location)
2448
+ end
2449
+
2450
+ # :call-seq:
2451
+ # on_retry: () -> Retry
2452
+ def on_retry
2453
+ keyword = find_token(Kw, "retry")
2454
+
2455
+ Retry.new(value: keyword.value, location: keyword.location)
2456
+ end
2457
+
2458
+ # :call-seq:
2459
+ # on_return: (Args arguments) -> Return
2460
+ def on_return(arguments)
2461
+ keyword = find_token(Kw, "return")
2462
+
2463
+ Return.new(
2464
+ arguments: arguments,
2465
+ location: keyword.location.to(arguments.location)
2466
+ )
2467
+ end
2468
+
2469
+ # :call-seq:
2470
+ # on_return0: () -> Return0
2471
+ def on_return0
2472
+ keyword = find_token(Kw, "return")
2473
+
2474
+ Return0.new(value: keyword.value, location: keyword.location)
2475
+ end
2476
+
2477
+ # :call-seq:
2478
+ # on_rparen: (String value) -> RParen
2479
+ def on_rparen(value)
2480
+ node =
2481
+ RParen.new(
2482
+ value: value,
2483
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
2484
+ )
2485
+
2486
+ tokens << node
2487
+ node
2488
+ end
2489
+
2490
+ # :call-seq:
2491
+ # on_sclass: (untyped target, BodyStmt bodystmt) -> SClass
2492
+ def on_sclass(target, bodystmt)
2493
+ beginning = find_token(Kw, "class")
2494
+ ending = find_token(Kw, "end")
2495
+ start_char = find_next_statement_start(target.location.end_char)
2496
+
2497
+ bodystmt.bind(
2498
+ start_char,
2499
+ start_char - line_counts[target.location.start_line - 1].start,
2500
+ ending.location.start_char,
2501
+ ending.location.start_column
2502
+ )
2503
+
2504
+ SClass.new(
2505
+ target: target,
2506
+ bodystmt: bodystmt,
2507
+ location: beginning.location.to(ending.location)
2508
+ )
2509
+ end
2510
+
2511
+ # def on_semicolon(value)
2512
+ # value
2513
+ # end
2514
+
2515
+ # def on_sp(value)
2516
+ # value
2517
+ # end
2518
+
2519
+ # stmts_add is a parser event that represents a single statement inside a
2520
+ # list of statements within any lexical block. It accepts as arguments the
2521
+ # parent stmts node as well as an stmt which can be any expression in
2522
+ # Ruby.
2523
+ def on_stmts_add(statements, statement)
2524
+ location =
2525
+ if statements.body.empty?
2526
+ statement.location
2527
+ else
2528
+ statements.location.to(statement.location)
2529
+ end
2530
+
2531
+ Statements.new(self, body: statements.body << statement, location: location)
2532
+ end
2533
+
2534
+ # :call-seq:
2535
+ # on_stmts_new: () -> Statements
2536
+ def on_stmts_new
2537
+ Statements.new(
2538
+ self,
2539
+ body: [],
2540
+ location: Location.fixed(line: lineno, char: char_pos, column: current_column)
2541
+ )
2542
+ end
2543
+
2544
+ # :call-seq:
2545
+ # on_string_add: (
2546
+ # String string,
2547
+ # (StringEmbExpr | StringDVar | TStringContent) part
2548
+ # ) -> StringContent
2549
+ def on_string_add(string, part)
2550
+ location =
2551
+ string.parts.any? ? string.location.to(part.location) : part.location
2552
+
2553
+ StringContent.new(parts: string.parts << part, location: location)
2554
+ end
2555
+
2556
+ # :call-seq:
2557
+ # on_string_concat: (
2558
+ # (StringConcat | StringLiteral) left,
2559
+ # StringLiteral right
2560
+ # ) -> StringConcat
2561
+ def on_string_concat(left, right)
2562
+ StringConcat.new(
2563
+ left: left,
2564
+ right: right,
2565
+ location: left.location.to(right.location)
2566
+ )
2567
+ end
2568
+
2569
+ # :call-seq:
2570
+ # on_string_content: () -> StringContent
2571
+ def on_string_content
2572
+ StringContent.new(
2573
+ parts: [],
2574
+ location: Location.fixed(line: lineno, char: char_pos, column: current_column)
2575
+ )
2576
+ end
2577
+
2578
+ # :call-seq:
2579
+ # on_string_dvar: ((Backref | VarRef) variable) -> StringDVar
2580
+ def on_string_dvar(variable)
2581
+ embvar = find_token(EmbVar)
2582
+
2583
+ StringDVar.new(
2584
+ variable: variable,
2585
+ location: embvar.location.to(variable.location)
2586
+ )
2587
+ end
2588
+
2589
+ # :call-seq:
2590
+ # on_string_embexpr: (Statements statements) -> StringEmbExpr
2591
+ def on_string_embexpr(statements)
2592
+ embexpr_beg = find_token(EmbExprBeg)
2593
+ embexpr_end = find_token(EmbExprEnd)
2594
+
2595
+ statements.bind(
2596
+ embexpr_beg.location.end_char,
2597
+ embexpr_beg.location.end_column,
2598
+ embexpr_end.location.start_char,
2599
+ embexpr_end.location.start_column
2600
+ )
2601
+
2602
+ location =
2603
+ Location.new(
2604
+ start_line: embexpr_beg.location.start_line,
2605
+ start_char: embexpr_beg.location.start_char,
2606
+ start_column: embexpr_beg.location.start_column,
2607
+ end_line: [
2608
+ embexpr_end.location.end_line,
2609
+ statements.location.end_line
2610
+ ].max,
2611
+ end_char: embexpr_end.location.end_char,
2612
+ end_column: embexpr_end.location.end_column
2613
+ )
2614
+
2615
+ StringEmbExpr.new(statements: statements, location: location)
2616
+ end
2617
+
2618
+ # :call-seq:
2619
+ # on_string_literal: (String string) -> Heredoc | StringLiteral
2620
+ def on_string_literal(string)
2621
+ heredoc = @heredocs[-1]
2622
+
2623
+ if heredoc && heredoc.ending
2624
+ heredoc = @heredocs.pop
2625
+
2626
+ Heredoc.new(
2627
+ beginning: heredoc.beginning,
2628
+ ending: heredoc.ending,
2629
+ parts: string.parts,
2630
+ location: heredoc.location
2631
+ )
2632
+ else
2633
+ tstring_beg = find_token(TStringBeg)
2634
+ tstring_end = find_token(TStringEnd, location: tstring_beg.location)
2635
+
2636
+ location =
2637
+ Location.new(
2638
+ start_line: tstring_beg.location.start_line,
2639
+ start_char: tstring_beg.location.start_char,
2640
+ start_column: tstring_beg.location.start_column,
2641
+ end_line: [
2642
+ tstring_end.location.end_line,
2643
+ string.location.end_line
2644
+ ].max,
2645
+ end_char: tstring_end.location.end_char,
2646
+ end_column: tstring_end.location.end_column
2647
+ )
2648
+
2649
+ StringLiteral.new(
2650
+ parts: string.parts,
2651
+ quote: tstring_beg.value,
2652
+ location: location
2653
+ )
2654
+ end
2655
+ end
2656
+
2657
+ # :call-seq:
2658
+ # on_super: ((ArgParen | Args) arguments) -> Super
2659
+ def on_super(arguments)
2660
+ keyword = find_token(Kw, "super")
2661
+
2662
+ Super.new(
2663
+ arguments: arguments,
2664
+ location: keyword.location.to(arguments.location)
2665
+ )
2666
+ end
2667
+
2668
+ # symbeg is a token that represents the beginning of a symbol literal. In
2669
+ # most cases it will contain just ":" as in the value, but if its a dynamic
2670
+ # symbol being defined it will contain ":'" or ":\"".
2671
+ def on_symbeg(value)
2672
+ node =
2673
+ SymBeg.new(
2674
+ value: value,
2675
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
2676
+ )
2677
+
2678
+ tokens << node
2679
+ node
2680
+ end
2681
+
2682
+ # :call-seq:
2683
+ # on_symbol: (
2684
+ # (Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op) value
2685
+ # ) -> SymbolContent
2686
+ def on_symbol(value)
2687
+ tokens.delete(value)
2688
+
2689
+ SymbolContent.new(value: value, location: value.location)
2690
+ end
2691
+
2692
+ # :call-seq:
2693
+ # on_symbol_literal: (
2694
+ # (
2695
+ # Backtick | Const | CVar | GVar | Ident |
2696
+ # IVar | Kw | Op | SymbolContent
2697
+ # ) value
2698
+ # ) -> SymbolLiteral
2699
+ def on_symbol_literal(value)
2700
+ if value.is_a?(SymbolContent)
2701
+ symbeg = find_token(SymBeg)
2702
+
2703
+ SymbolLiteral.new(
2704
+ value: value.value,
2705
+ location: symbeg.location.to(value.location)
2706
+ )
2707
+ else
2708
+ tokens.delete(value)
2709
+ SymbolLiteral.new(value: value, location: value.location)
2710
+ end
2711
+ end
2712
+
2713
+ # :call-seq:
2714
+ # on_symbols_add: (Symbols symbols, Word word) -> Symbols
2715
+ def on_symbols_add(symbols, word)
2716
+ Symbols.new(
2717
+ beginning: symbols.beginning,
2718
+ elements: symbols.elements << word,
2719
+ location: symbols.location.to(word.location)
2720
+ )
2721
+ end
2722
+
2723
+ # :call-seq:
2724
+ # on_symbols_beg: (String value) -> SymbolsBeg
2725
+ def on_symbols_beg(value)
2726
+ node =
2727
+ SymbolsBeg.new(
2728
+ value: value,
2729
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
2730
+ )
2731
+
2732
+ tokens << node
2733
+ node
2734
+ end
2735
+
2736
+ # :call-seq:
2737
+ # on_symbols_new: () -> Symbols
2738
+ def on_symbols_new
2739
+ beginning = find_token(SymbolsBeg)
2740
+
2741
+ Symbols.new(
2742
+ beginning: beginning,
2743
+ elements: [],
2744
+ location: beginning.location
2745
+ )
2746
+ end
2747
+
2748
+ # :call-seq:
2749
+ # on_tlambda: (String value) -> TLambda
2750
+ def on_tlambda(value)
2751
+ node =
2752
+ TLambda.new(
2753
+ value: value,
2754
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
2755
+ )
2756
+
2757
+ tokens << node
2758
+ node
2759
+ end
2760
+
2761
+ # :call-seq:
2762
+ # on_tlambeg: (String value) -> TLamBeg
2763
+ def on_tlambeg(value)
2764
+ node =
2765
+ TLamBeg.new(
2766
+ value: value,
2767
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
2768
+ )
2769
+
2770
+ tokens << node
2771
+ node
2772
+ end
2773
+
2774
+ # :call-seq:
2775
+ # on_top_const_field: (Const constant) -> TopConstRef
2776
+ def on_top_const_field(constant)
2777
+ operator = find_colon2_before(constant)
2778
+
2779
+ TopConstField.new(
2780
+ constant: constant,
2781
+ location: operator.location.to(constant.location)
2782
+ )
2783
+ end
2784
+
2785
+ # :call-seq:
2786
+ # on_top_const_ref: (Const constant) -> TopConstRef
2787
+ def on_top_const_ref(constant)
2788
+ operator = find_colon2_before(constant)
2789
+
2790
+ TopConstRef.new(
2791
+ constant: constant,
2792
+ location: operator.location.to(constant.location)
2793
+ )
2794
+ end
2795
+
2796
+ # :call-seq:
2797
+ # on_tstring_beg: (String value) -> TStringBeg
2798
+ def on_tstring_beg(value)
2799
+ node =
2800
+ TStringBeg.new(
2801
+ value: value,
2802
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
2803
+ )
2804
+
2805
+ tokens << node
2806
+ node
2807
+ end
2808
+
2809
+ # :call-seq:
2810
+ # on_tstring_content: (String value) -> TStringContent
2811
+ def on_tstring_content(value)
2812
+ TStringContent.new(
2813
+ value: value,
2814
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
2815
+ )
2816
+ end
2817
+
2818
+ # :call-seq:
2819
+ # on_tstring_end: (String value) -> TStringEnd
2820
+ def on_tstring_end(value)
2821
+ node =
2822
+ TStringEnd.new(
2823
+ value: value,
2824
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
2825
+ )
2826
+
2827
+ tokens << node
2828
+ node
2829
+ end
2830
+
2831
+ # :call-seq:
2832
+ # on_unary: (:not operator, untyped statement) -> Not
2833
+ # | (Symbol operator, untyped statement) -> Unary
2834
+ def on_unary(operator, statement)
2835
+ if operator == :not
2836
+ # We have somewhat special handling of the not operator since if it has
2837
+ # parentheses they don't get reported as a paren node for some reason.
2838
+
2839
+ beginning = find_token(Kw, "not")
2840
+ ending = statement
2841
+
2842
+ range = beginning.location.end_char...statement.location.start_char
2843
+ paren = source[range].include?("(")
2844
+
2845
+ if paren
2846
+ find_token(LParen)
2847
+ ending = find_token(RParen)
2848
+ end
2849
+
2850
+ Not.new(
2851
+ statement: statement,
2852
+ parentheses: paren,
2853
+ location: beginning.location.to(ending.location)
2854
+ )
2855
+ else
2856
+ # Special case instead of using find_token here. It turns out that
2857
+ # if you have a range that goes from a negative number to a negative
2858
+ # number then you can end up with a .. or a ... that's higher in the
2859
+ # stack. So we need to explicitly disallow those operators.
2860
+ index =
2861
+ tokens.rindex do |token|
2862
+ token.is_a?(Op) &&
2863
+ token.location.start_char < statement.location.start_char &&
2864
+ !%w[.. ...].include?(token.value)
2865
+ end
2866
+
2867
+ beginning = tokens.delete_at(index)
2868
+
2869
+ Unary.new(
2870
+ operator: operator[0], # :+@ -> "+"
2871
+ statement: statement,
2872
+ location: beginning.location.to(statement.location)
2873
+ )
2874
+ end
2875
+ end
2876
+
2877
+ # :call-seq:
2878
+ # on_undef: (Array[DynaSymbol | SymbolLiteral] symbols) -> Undef
2879
+ def on_undef(symbols)
2880
+ keyword = find_token(Kw, "undef")
2881
+
2882
+ Undef.new(
2883
+ symbols: symbols,
2884
+ location: keyword.location.to(symbols.last.location)
2885
+ )
2886
+ end
2887
+
2888
+ # :call-seq:
2889
+ # on_unless: (
2890
+ # untyped predicate,
2891
+ # Statements statements,
2892
+ # ((nil | Elsif | Else) consequent)
2893
+ # ) -> Unless
2894
+ def on_unless(predicate, statements, consequent)
2895
+ beginning = find_token(Kw, "unless")
2896
+ ending = consequent || find_token(Kw, "end")
2897
+
2898
+ statements.bind(
2899
+ predicate.location.end_char,
2900
+ predicate.location.end_column,
2901
+ ending.location.start_char,
2902
+ ending.location.start_column
2903
+ )
2904
+
2905
+ Unless.new(
2906
+ predicate: predicate,
2907
+ statements: statements,
2908
+ consequent: consequent,
2909
+ location: beginning.location.to(ending.location)
2910
+ )
2911
+ end
2912
+
2913
+ # :call-seq:
2914
+ # on_unless_mod: (untyped predicate, untyped statement) -> UnlessMod
2915
+ def on_unless_mod(predicate, statement)
2916
+ find_token(Kw, "unless")
2917
+
2918
+ UnlessMod.new(
2919
+ statement: statement,
2920
+ predicate: predicate,
2921
+ location: statement.location.to(predicate.location)
2922
+ )
2923
+ end
2924
+
2925
+ # :call-seq:
2926
+ # on_until: (untyped predicate, Statements statements) -> Until
2927
+ def on_until(predicate, statements)
2928
+ beginning = find_token(Kw, "until")
2929
+ ending = find_token(Kw, "end")
2930
+
2931
+ # Consume the do keyword if it exists so that it doesn't get confused for
2932
+ # some other block
2933
+ keyword = find_token(Kw, "do", consume: false)
2934
+ if keyword && keyword.location.start_char > predicate.location.end_char &&
2935
+ keyword.location.end_char < ending.location.start_char
2936
+ tokens.delete(keyword)
2937
+ end
2938
+
2939
+ # Update the Statements location information
2940
+ statements.bind(
2941
+ predicate.location.end_char,
2942
+ predicate.location.end_column,
2943
+ ending.location.start_char,
2944
+ ending.location.start_column
2945
+ )
2946
+
2947
+ Until.new(
2948
+ predicate: predicate,
2949
+ statements: statements,
2950
+ location: beginning.location.to(ending.location)
2951
+ )
2952
+ end
2953
+
2954
+ # :call-seq:
2955
+ # on_until_mod: (untyped predicate, untyped statement) -> UntilMod
2956
+ def on_until_mod(predicate, statement)
2957
+ find_token(Kw, "until")
2958
+
2959
+ UntilMod.new(
2960
+ statement: statement,
2961
+ predicate: predicate,
2962
+ location: statement.location.to(predicate.location)
2963
+ )
2964
+ end
2965
+
2966
+ # :call-seq:
2967
+ # on_var_alias: (GVar left, (Backref | GVar) right) -> VarAlias
2968
+ def on_var_alias(left, right)
2969
+ keyword = find_token(Kw, "alias")
2970
+
2971
+ VarAlias.new(
2972
+ left: left,
2973
+ right: right,
2974
+ location: keyword.location.to(right.location)
2975
+ )
2976
+ end
2977
+
2978
+ # :call-seq:
2979
+ # on_var_field: (
2980
+ # (nil | Const | CVar | GVar | Ident | IVar) value
2981
+ # ) -> VarField
2982
+ def on_var_field(value)
2983
+ location =
2984
+ if value
2985
+ value.location
2986
+ else
2987
+ # You can hit this pattern if you're assigning to a splat using
2988
+ # pattern matching syntax in Ruby 2.7+
2989
+ Location.fixed(line: lineno, char: char_pos, column: current_column)
2990
+ end
2991
+
2992
+ VarField.new(value: value, location: location)
2993
+ end
2994
+
2995
+ # :call-seq:
2996
+ # on_var_ref: ((Const | CVar | GVar | Ident | IVar | Kw) value) -> VarRef
2997
+ def on_var_ref(value)
2998
+ pin = find_token(Op, "^", consume: false)
2999
+
3000
+ if pin && pin.location.start_char == value.location.start_char - 1
3001
+ tokens.delete(pin)
3002
+ PinnedVarRef.new(value: value, location: pin.location.to(value.location))
3003
+ else
3004
+ VarRef.new(value: value, location: value.location)
3005
+ end
3006
+ end
3007
+
3008
+ # :call-seq:
3009
+ # on_vcall: (Ident ident) -> VCall
3010
+ def on_vcall(ident)
3011
+ VCall.new(value: ident, location: ident.location)
3012
+ end
3013
+
3014
+ # :call-seq:
3015
+ # on_void_stmt: () -> VoidStmt
3016
+ def on_void_stmt
3017
+ VoidStmt.new(location: Location.fixed(line: lineno, char: char_pos, column: current_column))
3018
+ end
3019
+
3020
+ # :call-seq:
3021
+ # on_when: (
3022
+ # Args arguments,
3023
+ # Statements statements,
3024
+ # (nil | Else | When) consequent
3025
+ # ) -> When
3026
+ def on_when(arguments, statements, consequent)
3027
+ beginning = find_token(Kw, "when")
3028
+ ending = consequent || find_token(Kw, "end")
3029
+
3030
+ statements_start = arguments
3031
+ if token = find_token(Kw, "then", consume: false)
3032
+ tokens.delete(token)
3033
+ statements_start = token
3034
+ end
3035
+
3036
+ start_char = find_next_statement_start(statements_start.location.end_char)
3037
+
3038
+ statements.bind(
3039
+ start_char,
3040
+ start_char - line_counts[statements_start.location.start_line - 1].start,
3041
+ ending.location.start_char,
3042
+ ending.location.start_column
3043
+ )
3044
+
3045
+ When.new(
3046
+ arguments: arguments,
3047
+ statements: statements,
3048
+ consequent: consequent,
3049
+ location: beginning.location.to(ending.location)
3050
+ )
3051
+ end
3052
+
3053
+ # :call-seq:
3054
+ # on_while: (untyped predicate, Statements statements) -> While
3055
+ def on_while(predicate, statements)
3056
+ beginning = find_token(Kw, "while")
3057
+ ending = find_token(Kw, "end")
3058
+
3059
+ # Consume the do keyword if it exists so that it doesn't get confused for
3060
+ # some other block
3061
+ keyword = find_token(Kw, "do", consume: false)
3062
+ if keyword && keyword.location.start_char > predicate.location.end_char &&
3063
+ keyword.location.end_char < ending.location.start_char
3064
+ tokens.delete(keyword)
3065
+ end
3066
+
3067
+ # Update the Statements location information
3068
+ statements.bind(
3069
+ predicate.location.end_char,
3070
+ predicate.location.end_column,
3071
+ ending.location.start_char,
3072
+ ending.location.start_column
3073
+ )
3074
+
3075
+ While.new(
3076
+ predicate: predicate,
3077
+ statements: statements,
3078
+ location: beginning.location.to(ending.location)
3079
+ )
3080
+ end
3081
+
3082
+ # :call-seq:
3083
+ # on_while_mod: (untyped predicate, untyped statement) -> WhileMod
3084
+ def on_while_mod(predicate, statement)
3085
+ find_token(Kw, "while")
3086
+
3087
+ WhileMod.new(
3088
+ statement: statement,
3089
+ predicate: predicate,
3090
+ location: statement.location.to(predicate.location)
3091
+ )
3092
+ end
3093
+
3094
+ # :call-seq:
3095
+ # on_word_add: (
3096
+ # Word word,
3097
+ # (StringEmbExpr | StringDVar | TStringContent) part
3098
+ # ) -> Word
3099
+ def on_word_add(word, part)
3100
+ location =
3101
+ word.parts.empty? ? part.location : word.location.to(part.location)
3102
+
3103
+ Word.new(parts: word.parts << part, location: location)
3104
+ end
3105
+
3106
+ # :call-seq:
3107
+ # on_word_new: () -> Word
3108
+ def on_word_new
3109
+ Word.new(parts: [], location: Location.fixed(line: lineno, char: char_pos, column: current_column))
3110
+ end
3111
+
3112
+ # :call-seq:
3113
+ # on_words_add: (Words words, Word word) -> Words
3114
+ def on_words_add(words, word)
3115
+ Words.new(
3116
+ beginning: words.beginning,
3117
+ elements: words.elements << word,
3118
+ location: words.location.to(word.location)
3119
+ )
3120
+ end
3121
+
3122
+ # :call-seq:
3123
+ # on_words_beg: (String value) -> WordsBeg
3124
+ def on_words_beg(value)
3125
+ node =
3126
+ WordsBeg.new(
3127
+ value: value,
3128
+ location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size)
3129
+ )
3130
+
3131
+ tokens << node
3132
+ node
3133
+ end
3134
+
3135
+ # :call-seq:
3136
+ # on_words_new: () -> Words
3137
+ def on_words_new
3138
+ beginning = find_token(WordsBeg)
3139
+
3140
+ Words.new(beginning: beginning, elements: [], location: beginning.location)
3141
+ end
3142
+
3143
+ # def on_words_sep(value)
3144
+ # value
3145
+ # end
3146
+
3147
+ # :call-seq:
3148
+ # on_xstring_add: (
3149
+ # XString xstring,
3150
+ # (StringEmbExpr | StringDVar | TStringContent) part
3151
+ # ) -> XString
3152
+ def on_xstring_add(xstring, part)
3153
+ XString.new(
3154
+ parts: xstring.parts << part,
3155
+ location: xstring.location.to(part.location)
3156
+ )
3157
+ end
3158
+
3159
+ # :call-seq:
3160
+ # on_xstring_new: () -> XString
3161
+ def on_xstring_new
3162
+ heredoc = @heredocs[-1]
3163
+
3164
+ location =
3165
+ if heredoc && heredoc.beginning.value.include?("`")
3166
+ heredoc.location
3167
+ else
3168
+ find_token(Backtick).location
3169
+ end
3170
+
3171
+ XString.new(parts: [], location: location)
3172
+ end
3173
+
3174
+ # :call-seq:
3175
+ # on_xstring_literal: (XString xstring) -> Heredoc | XStringLiteral
3176
+ def on_xstring_literal(xstring)
3177
+ heredoc = @heredocs[-1]
3178
+
3179
+ if heredoc && heredoc.beginning.value.include?("`")
3180
+ Heredoc.new(
3181
+ beginning: heredoc.beginning,
3182
+ ending: heredoc.ending,
3183
+ parts: xstring.parts,
3184
+ location: heredoc.location
3185
+ )
3186
+ else
3187
+ ending = find_token(TStringEnd, location: xstring.location)
3188
+
3189
+ XStringLiteral.new(
3190
+ parts: xstring.parts,
3191
+ location: xstring.location.to(ending.location)
3192
+ )
3193
+ end
3194
+ end
3195
+
3196
+ # :call-seq:
3197
+ # on_yield: ((Args | Paren) arguments) -> Yield
3198
+ def on_yield(arguments)
3199
+ keyword = find_token(Kw, "yield")
3200
+
3201
+ Yield.new(
3202
+ arguments: arguments,
3203
+ location: keyword.location.to(arguments.location)
3204
+ )
3205
+ end
3206
+
3207
+ # :call-seq:
3208
+ # on_yield0: () -> Yield0
3209
+ def on_yield0
3210
+ keyword = find_token(Kw, "yield")
3211
+
3212
+ Yield0.new(value: keyword.value, location: keyword.location)
3213
+ end
3214
+
3215
+ # :call-seq:
3216
+ # on_zsuper: () -> ZSuper
3217
+ def on_zsuper
3218
+ keyword = find_token(Kw, "super")
3219
+
3220
+ ZSuper.new(value: keyword.value, location: keyword.location)
3221
+ end
3222
+ end
3223
+ end