prism 1.8.0 → 1.9.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2344922b08aa30076aab32c5a92e0d7f21a05f03bb85d089cc69e228a71e3b20
4
- data.tar.gz: 7a44533dd2827ec9f6c31fc69c533c9f90b88520aef71219a348aa61fa460fbd
3
+ metadata.gz: 1f7205a73da36d10903c1495fd475e014a30c875568c932568cac22351eb3059
4
+ data.tar.gz: d59eac5f6e8e8955ea9b259b63eec423a0af06c79a748edaa9f64f24463dc874
5
5
  SHA512:
6
- metadata.gz: c278e7b881b89f51150850df09062d9a9bd5e5076746e4b86ab199729a588f8aa346e12b6ba9b95dc0cf53592866bf216977bbb7da10cef6bd8a018681e9f45f
7
- data.tar.gz: '009dee7695d1f7d58adb68d3829f97ecff445cc0eb8509526979f938932e553abdc2ba601049df67b8b29a75b7d542a9fb06d4c1984131d6f60e2ac9e3cf83c2'
6
+ metadata.gz: 59571b78dba19ec01a6f52094cc6d30e5648e633cd24f3abfbbc765145bed778f88fea75eada071d780ecb3dbe77a1bebab563d676289fed0ea6d21e50056290
7
+ data.tar.gz: 3c5b40536f98839fefb0afcefd49d5e78a3a31f9a55bd33b78b80665b2ca3bd1c2262c1836f9c6f1d3ddc7ad2d9b7dd12f06b3dff2b4aea9475a7171340d95b4
data/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [Unreleased]
8
+
9
+ ## [1.9.0] - 2026-01-27
10
+
11
+ ### Added
12
+
13
+ - Lots of work on the Ripper translation layer to make it more compatible and efficient.
14
+ - Alias `Prism::Node#breadth_first_search` to `Prism::Node#find`.
15
+ - Add `Prism::Node#breadth_first_search_all`/`Prism::Node#find_all` for finding all nodes matching a condition.
16
+
17
+ ### Changed
18
+
19
+ - Fixed location of opening tokens when invalid syntax is parsed.
20
+ - Fix RBI for parsing options.
21
+
7
22
  ## [1.8.0] - 2026-01-12
8
23
 
9
24
  ### Added
@@ -19,8 +34,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
19
34
  - Decouple ripper translator from ripper library.
20
35
  - Sync Prism::Translation::ParserCurrent with Ruby 4.0.
21
36
 
22
- ## [Unreleased]
23
-
24
37
  ## [1.7.0] - 2025-12-18
25
38
 
26
39
  ### Added
@@ -731,7 +744,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
731
744
 
732
745
  - 🎉 Initial release! 🎉
733
746
 
734
- [unreleased]: https://github.com/ruby/prism/compare/v1.8.0...HEAD
747
+ [unreleased]: https://github.com/ruby/prism/compare/v1.9.0...HEAD
748
+ [1.9.0]: https://github.com/ruby/prism/compare/v1.8.0...v1.9.0
735
749
  [1.8.0]: https://github.com/ruby/prism/compare/v1.7.0...v1.8.0
736
750
  [1.7.0]: https://github.com/ruby/prism/compare/v1.6.0...v1.7.0
737
751
  [1.6.0]: https://github.com/ruby/prism/compare/v1.5.2...v1.6.0
data/config.yml CHANGED
@@ -1269,17 +1269,17 @@ nodes:
1269
1269
  - name: opening_loc
1270
1270
  type: location
1271
1271
  comment: |
1272
- Represents the location of the opening `|`.
1272
+ Represents the location of the opening `{` or `do`.
1273
1273
 
1274
1274
  [1, 2, 3].each { |i| puts x }
1275
- ^
1275
+ ^
1276
1276
  - name: closing_loc
1277
1277
  type: location
1278
1278
  comment: |
1279
- Represents the location of the closing `|`.
1279
+ Represents the location of the closing `}` or `end`.
1280
1280
 
1281
1281
  [1, 2, 3].each { |i| puts x }
1282
- ^
1282
+ ^
1283
1283
  comment: |
1284
1284
  Represents a block of ruby code.
1285
1285
 
@@ -1,22 +1,6 @@
1
1
  # Ripper translation
2
2
 
3
- Prism provides the ability to mirror the `Ripper` standard library. You can do this by:
4
-
5
- ```ruby
6
- require "prism/translation/ripper/shim"
7
- ```
8
-
9
- This provides the APIs like:
10
-
11
- ```ruby
12
- Ripper.lex
13
- Ripper.parse
14
- Ripper.sexp_raw
15
- Ripper.sexp
16
-
17
- Ripper::SexpBuilder
18
- Ripper::SexpBuilderPP
19
- ```
3
+ Prism provides the ability to mirror the `Ripper` standard library. It is available under `Prism::Translation::Ripper`. You can use the entire public API, and also some undocumented features that are commonly used.
20
4
 
21
5
  Briefly, `Ripper` is a streaming parser that allows you to construct your own syntax tree. As an example:
22
6
 
@@ -49,6 +33,13 @@ ArithmeticRipper.new("1 + 2 - 3").parse # => [0]
49
33
 
50
34
  The exact names of the `on_*` methods are listed in the `Ripper` source.
51
35
 
36
+ You can can also automatically use the ripper translation in places that don't explicitly use the translation layer by doing the following:
37
+
38
+ ```ruby
39
+ # Will redirect access of the `Ripper` constant to `Prism::Translation::Ripper`.
40
+ require "prism/translation/ripper/shim"
41
+ ```
42
+
52
43
  ## Background
53
44
 
54
45
  It is helpful to understand the differences between the `Ripper` library and the `Prism` library. Both libraries perform parsing and provide you with APIs to manipulate and understand the resulting syntax tree. However, there are a few key differences.
@@ -1,7 +1,7 @@
1
1
  #ifndef PRISM_EXT_NODE_H
2
2
  #define PRISM_EXT_NODE_H
3
3
 
4
- #define EXPECTED_PRISM_VERSION "1.8.0"
4
+ #define EXPECTED_PRISM_VERSION "1.9.0"
5
5
 
6
6
  #include <ruby.h>
7
7
  #include <ruby/encoding.h>
data/include/prism/ast.h CHANGED
@@ -1826,20 +1826,20 @@ typedef struct pm_block_node {
1826
1826
  /**
1827
1827
  * BlockNode#opening_loc
1828
1828
  *
1829
- * Represents the location of the opening `|`.
1829
+ * Represents the location of the opening `{` or `do`.
1830
1830
  *
1831
1831
  * [1, 2, 3].each { |i| puts x }
1832
- * ^
1832
+ * ^
1833
1833
  */
1834
1834
  pm_location_t opening_loc;
1835
1835
 
1836
1836
  /**
1837
1837
  * BlockNode#closing_loc
1838
1838
  *
1839
- * Represents the location of the closing `|`.
1839
+ * Represents the location of the closing `}` or `end`.
1840
1840
  *
1841
1841
  * [1, 2, 3].each { |i| puts x }
1842
- * ^
1842
+ * ^
1843
1843
  */
1844
1844
  pm_location_t closing_loc;
1845
1845
  } pm_block_node_t;
@@ -14,7 +14,7 @@
14
14
  /**
15
15
  * The minor version of the Prism library as an int.
16
16
  */
17
- #define PRISM_VERSION_MINOR 8
17
+ #define PRISM_VERSION_MINOR 9
18
18
 
19
19
  /**
20
20
  * The patch version of the Prism library as an int.
@@ -24,6 +24,6 @@
24
24
  /**
25
25
  * The version of the Prism library as a constant string.
26
26
  */
27
- #define PRISM_VERSION "1.8.0"
27
+ #define PRISM_VERSION "1.9.0"
28
28
 
29
29
  #endif
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  # :markup: markdown
3
3
 
4
- require "delegate"
5
-
6
4
  module Prism
7
5
  # This class is responsible for lexing the source using prism and then
8
6
  # converting those tokens to be compatible with Ripper. In the vast majority
@@ -201,87 +199,51 @@ module Prism
201
199
  # When we produce tokens, we produce the same arrays that Ripper does.
202
200
  # However, we add a couple of convenience methods onto them to make them a
203
201
  # little easier to work with. We delegate all other methods to the array.
204
- class Token < SimpleDelegator
205
- # @dynamic initialize, each, []
202
+ class Token < BasicObject
203
+ # Create a new token object with the given ripper-compatible array.
204
+ def initialize(array)
205
+ @array = array
206
+ end
206
207
 
207
208
  # The location of the token in the source.
208
209
  def location
209
- self[0]
210
+ @array[0]
210
211
  end
211
212
 
212
213
  # The type of the token.
213
214
  def event
214
- self[1]
215
+ @array[1]
215
216
  end
216
217
 
217
218
  # The slice of the source that this token represents.
218
219
  def value
219
- self[2]
220
+ @array[2]
220
221
  end
221
222
 
222
223
  # The state of the lexer when this token was produced.
223
224
  def state
224
- self[3]
225
+ @array[3]
225
226
  end
226
- end
227
227
 
228
- # Ripper doesn't include the rest of the token in the event, so we need to
229
- # trim it down to just the content on the first line when comparing.
230
- class EndContentToken < Token
228
+ # We want to pretend that this is just an Array.
231
229
  def ==(other) # :nodoc:
232
- [self[0], self[1], self[2][0..self[2].index("\n")], self[3]] == other
230
+ @array == other
233
231
  end
234
- end
235
232
 
236
- # Tokens where state should be ignored
237
- # used for :on_comment, :on_heredoc_end, :on_embexpr_end
238
- class IgnoreStateToken < Token
239
- def ==(other) # :nodoc:
240
- self[0...-1] == other[0...-1]
233
+ def respond_to_missing?(name, include_private = false) # :nodoc:
234
+ @array.respond_to?(name, include_private)
241
235
  end
242
- end
243
236
 
244
- # Ident tokens for the most part are exactly the same, except sometimes we
245
- # know an ident is a local when ripper doesn't (when they are introduced
246
- # through named captures in regular expressions). In that case we don't
247
- # compare the state.
248
- class IdentToken < Token
249
- def ==(other) # :nodoc:
250
- (self[0...-1] == other[0...-1]) && (
251
- (other[3] == Translation::Ripper::EXPR_LABEL | Translation::Ripper::EXPR_END) ||
252
- (other[3] & (Translation::Ripper::EXPR_ARG | Translation::Ripper::EXPR_CMDARG) != 0)
253
- )
237
+ def method_missing(name, ...) # :nodoc:
238
+ @array.send(name, ...)
254
239
  end
255
240
  end
256
241
 
257
- # Ignored newlines can occasionally have a LABEL state attached to them, so
258
- # we compare the state differently here.
259
- class IgnoredNewlineToken < Token
260
- def ==(other) # :nodoc:
261
- return false unless self[0...-1] == other[0...-1]
262
-
263
- if self[3] == Translation::Ripper::EXPR_ARG | Translation::Ripper::EXPR_LABELED
264
- other[3] & Translation::Ripper::EXPR_ARG | Translation::Ripper::EXPR_LABELED != 0
265
- else
266
- self[3] == other[3]
267
- end
268
- end
269
- end
270
-
271
- # If we have an identifier that follows a method name like:
272
- #
273
- # def foo bar
274
- #
275
- # then Ripper will mark bar as END|LABEL if there is a local in a parent
276
- # scope named bar because it hasn't pushed the local table yet. We do this
277
- # more accurately, so we need to allow comparing against both END and
278
- # END|LABEL.
279
- class ParamToken < Token
242
+ # Tokens where state should be ignored
243
+ # used for :on_sp, :on_comment, :on_heredoc_end, :on_embexpr_end
244
+ class IgnoreStateToken < Token
280
245
  def ==(other) # :nodoc:
281
- (self[0...-1] == other[0...-1]) && (
282
- (other[3] == Translation::Ripper::EXPR_END) ||
283
- (other[3] == Translation::Ripper::EXPR_END | Translation::Ripper::EXPR_LABEL)
284
- )
246
+ self[0...-1] == other[0...-1]
285
247
  end
286
248
  end
287
249
 
@@ -619,10 +581,10 @@ module Prism
619
581
  BOM_FLUSHED = RUBY_VERSION >= "3.3.0"
620
582
  private_constant :BOM_FLUSHED
621
583
 
622
- attr_reader :source, :options
584
+ attr_reader :options
623
585
 
624
- def initialize(source, **options)
625
- @source = source
586
+ def initialize(code, **options)
587
+ @code = code
626
588
  @options = options
627
589
  end
628
590
 
@@ -632,12 +594,14 @@ module Prism
632
594
  state = :default
633
595
  heredoc_stack = [[]] #: Array[Array[Heredoc::PlainHeredoc | Heredoc::DashHeredoc | Heredoc::DedentingHeredoc]]
634
596
 
635
- result = Prism.lex(source, **options)
597
+ result = Prism.lex(@code, **options)
598
+ source = result.source
636
599
  result_value = result.value
637
600
  previous_state = nil #: State?
638
601
  last_heredoc_end = nil #: Integer?
602
+ eof_token = nil
639
603
 
640
- bom = source.byteslice(0..2) == "\xEF\xBB\xBF"
604
+ bom = source.slice(0, 3) == "\xEF\xBB\xBF"
641
605
 
642
606
  result_value.each_with_index do |(token, lex_state), index|
643
607
  lineno = token.location.start_line
@@ -675,12 +639,15 @@ module Prism
675
639
 
676
640
  event = RIPPER.fetch(token.type)
677
641
  value = token.value
678
- lex_state = Translation::Ripper::Lexer::State.new(lex_state)
642
+ lex_state = Translation::Ripper::Lexer::State.cached(lex_state)
679
643
 
680
644
  token =
681
645
  case event
682
646
  when :on___end__
683
- EndContentToken.new([[lineno, column], event, value, lex_state])
647
+ # Ripper doesn't include the rest of the token in the event, so we need to
648
+ # trim it down to just the content on the first line.
649
+ value = value[0..value.index("\n")]
650
+ Token.new([[lineno, column], event, value, lex_state])
684
651
  when :on_comment
685
652
  IgnoreStateToken.new([[lineno, column], event, value, lex_state])
686
653
  when :on_heredoc_end
@@ -688,33 +655,18 @@ module Prism
688
655
  # want to bother comparing the state on them.
689
656
  last_heredoc_end = token.location.end_offset
690
657
  IgnoreStateToken.new([[lineno, column], event, value, lex_state])
691
- when :on_ident
692
- if lex_state == Translation::Ripper::EXPR_END
693
- # If we have an identifier that follows a method name like:
694
- #
695
- # def foo bar
696
- #
697
- # then Ripper will mark bar as END|LABEL if there is a local in a
698
- # parent scope named bar because it hasn't pushed the local table
699
- # yet. We do this more accurately, so we need to allow comparing
700
- # against both END and END|LABEL.
701
- ParamToken.new([[lineno, column], event, value, lex_state])
702
- elsif lex_state == Translation::Ripper::EXPR_END | Translation::Ripper::EXPR_LABEL
703
- # In the event that we're comparing identifiers, we're going to
704
- # allow a little divergence. Ripper doesn't account for local
705
- # variables introduced through named captures in regexes, and we
706
- # do, which accounts for this difference.
707
- IdentToken.new([[lineno, column], event, value, lex_state])
708
- else
709
- Token.new([[lineno, column], event, value, lex_state])
710
- end
711
658
  when :on_embexpr_end
712
659
  IgnoreStateToken.new([[lineno, column], event, value, lex_state])
713
- when :on_ignored_nl
714
- # Ignored newlines can occasionally have a LABEL state attached to
715
- # them which doesn't actually impact anything. We don't mirror that
716
- # state so we ignored it.
717
- IgnoredNewlineToken.new([[lineno, column], event, value, lex_state])
660
+ when :on_words_sep
661
+ # Ripper emits one token each per line.
662
+ value.each_line.with_index do |line, index|
663
+ if index > 0
664
+ lineno += 1
665
+ column = 0
666
+ end
667
+ tokens << Token.new([[lineno, column], event, line, lex_state])
668
+ end
669
+ tokens.pop
718
670
  when :on_regexp_end
719
671
  # On regex end, Ripper scans and then sets end state, so the ripper
720
672
  # lexed output is begin, when it should be end. prism sets lex state
@@ -739,13 +691,14 @@ module Prism
739
691
  counter += { on_embexpr_beg: -1, on_embexpr_end: 1 }[current_event] || 0
740
692
  end
741
693
 
742
- Translation::Ripper::Lexer::State.new(result_value[current_index][1])
694
+ Translation::Ripper::Lexer::State.cached(result_value[current_index][1])
743
695
  else
744
696
  previous_state
745
697
  end
746
698
 
747
699
  Token.new([[lineno, column], event, value, lex_state])
748
700
  when :on_eof
701
+ eof_token = token
749
702
  previous_token = result_value[index - 1][0]
750
703
 
751
704
  # If we're at the end of the file and the previous token was a
@@ -768,7 +721,7 @@ module Prism
768
721
  end_offset += 3
769
722
  end
770
723
 
771
- tokens << Token.new([[lineno, 0], :on_nl, source.byteslice(start_offset...end_offset), lex_state])
724
+ tokens << Token.new([[lineno, 0], :on_nl, source.slice(start_offset, end_offset - start_offset), lex_state])
772
725
  end
773
726
  end
774
727
 
@@ -859,10 +812,98 @@ module Prism
859
812
  # Drop the EOF token from the list
860
813
  tokens = tokens[0...-1]
861
814
 
862
- # We sort by location to compare against Ripper's output
863
- tokens.sort_by!(&:location)
815
+ # We sort by location because Ripper.lex sorts.
816
+ # Manually implemented instead of `sort_by!(&:location)` for performance.
817
+ tokens.sort_by! do |token|
818
+ line, column = token.location
819
+ source.byte_offset(line, column)
820
+ end
821
+
822
+ # Add :on_sp tokens
823
+ tokens = add_on_sp_tokens(tokens, source, result.data_loc, bom, eof_token)
824
+
825
+ Result.new(tokens, result.comments, result.magic_comments, result.data_loc, result.errors, result.warnings, source)
826
+ end
827
+
828
+ def add_on_sp_tokens(tokens, source, data_loc, bom, eof_token)
829
+ new_tokens = []
830
+
831
+ prev_token_state = Translation::Ripper::Lexer::State.cached(Translation::Ripper::EXPR_BEG)
832
+ prev_token_end = bom ? 3 : 0
833
+
834
+ tokens.each do |token|
835
+ line, column = token.location
836
+ start_offset = source.byte_offset(line, column)
837
+
838
+ # Ripper reports columns on line 1 without counting the BOM, so we
839
+ # adjust to get the real offset
840
+ start_offset += 3 if line == 1 && bom
841
+
842
+ if start_offset > prev_token_end
843
+ sp_value = source.slice(prev_token_end, start_offset - prev_token_end)
844
+ sp_line = source.line(prev_token_end)
845
+ sp_column = source.column(prev_token_end)
846
+ # Ripper reports columns on line 1 without counting the BOM
847
+ sp_column -= 3 if sp_line == 1 && bom
848
+ continuation_index = sp_value.byteindex("\\")
849
+
850
+ # ripper emits up to three :on_sp tokens when line continuations are used
851
+ if continuation_index
852
+ next_whitespace_index = continuation_index + 1
853
+ next_whitespace_index += 1 if sp_value.byteslice(next_whitespace_index) == "\r"
854
+ next_whitespace_index += 1
855
+ first_whitespace = sp_value[0...continuation_index]
856
+ continuation = sp_value[continuation_index...next_whitespace_index]
857
+ second_whitespace = sp_value[next_whitespace_index..]
858
+
859
+ new_tokens << IgnoreStateToken.new([
860
+ [sp_line, sp_column],
861
+ :on_sp,
862
+ first_whitespace,
863
+ prev_token_state
864
+ ]) unless first_whitespace.empty?
865
+
866
+ new_tokens << IgnoreStateToken.new([
867
+ [sp_line, sp_column + continuation_index],
868
+ :on_sp,
869
+ continuation,
870
+ prev_token_state
871
+ ])
872
+
873
+ new_tokens << IgnoreStateToken.new([
874
+ [sp_line + 1, 0],
875
+ :on_sp,
876
+ second_whitespace,
877
+ prev_token_state
878
+ ]) unless second_whitespace.empty?
879
+ else
880
+ new_tokens << IgnoreStateToken.new([
881
+ [sp_line, sp_column],
882
+ :on_sp,
883
+ sp_value,
884
+ prev_token_state
885
+ ])
886
+ end
887
+ end
888
+
889
+ new_tokens << token
890
+ prev_token_state = token.state
891
+ prev_token_end = start_offset + token.value.bytesize
892
+ end
893
+
894
+ unless data_loc # no trailing :on_sp with __END__ as it is always preceded by :on_nl
895
+ end_offset = eof_token.location.end_offset
896
+ if prev_token_end < end_offset
897
+ new_tokens << IgnoreStateToken.new([
898
+ [source.line(prev_token_end), source.column(prev_token_end)],
899
+ :on_sp,
900
+ source.slice(prev_token_end, end_offset - prev_token_end),
901
+ prev_token_state
902
+ ])
903
+ end
904
+ end
864
905
 
865
- Result.new(tokens, result.comments, result.magic_comments, result.data_loc, result.errors, result.warnings, Source.for(source))
906
+ new_tokens
866
907
  end
867
908
  end
868
909
 
data/lib/prism/node.rb CHANGED
@@ -194,25 +194,13 @@ module Prism
194
194
  def tunnel(line, column)
195
195
  queue = [self] #: Array[Prism::node]
196
196
  result = [] #: Array[Prism::node]
197
+ offset = source.byte_offset(line, column)
197
198
 
198
199
  while (node = queue.shift)
199
200
  result << node
200
201
 
201
202
  node.each_child_node do |child_node|
202
- child_location = child_node.location
203
-
204
- start_line = child_location.start_line
205
- end_line = child_location.end_line
206
-
207
- if start_line == end_line
208
- if line == start_line && column >= child_location.start_column && column < child_location.end_column
209
- queue << child_node
210
- break
211
- end
212
- elsif (line == start_line && column >= child_location.start_column) || (line == end_line && column < child_location.end_column)
213
- queue << child_node
214
- break
215
- elsif line > start_line && line < end_line
203
+ if child_node.start_offset <= offset && offset < child_node.end_offset
216
204
  queue << child_node
217
205
  break
218
206
  end
@@ -223,7 +211,7 @@ module Prism
223
211
  end
224
212
 
225
213
  # Returns the first node that matches the given block when visited in a
226
- # depth-first search. This is useful for finding a node that matches a
214
+ # breadth-first search. This is useful for finding a node that matches a
227
215
  # particular condition.
228
216
  #
229
217
  # node.breadth_first_search { |node| node.node_id == node_id }
@@ -238,6 +226,26 @@ module Prism
238
226
 
239
227
  nil
240
228
  end
229
+ alias find breadth_first_search
230
+
231
+ # Returns all of the nodes that match the given block when visited in a
232
+ # breadth-first search. This is useful for finding all nodes that match a
233
+ # particular condition.
234
+ #
235
+ # node.breadth_first_search_all { |node| node.is_a?(Prism::CallNode) }
236
+ #
237
+ def breadth_first_search_all(&block)
238
+ queue = [self] #: Array[Prism::node]
239
+ results = [] #: Array[Prism::node]
240
+
241
+ while (node = queue.shift)
242
+ results << node if yield node
243
+ queue.concat(node.compact_child_nodes)
244
+ end
245
+
246
+ results
247
+ end
248
+ alias find_all breadth_first_search_all
241
249
 
242
250
  # Returns a list of the fields that exist for this node class. Fields
243
251
  # describe the structure of the node. This kind of reflection is useful for
@@ -2025,10 +2033,10 @@ module Prism
2025
2033
  # ^^^^^^
2026
2034
  attr_reader :body
2027
2035
 
2028
- # Represents the location of the opening `|`.
2036
+ # Represents the location of the opening `{` or `do`.
2029
2037
  #
2030
2038
  # [1, 2, 3].each { |i| puts x }
2031
- # ^
2039
+ # ^
2032
2040
  def opening_loc
2033
2041
  location = @opening_loc
2034
2042
  return location if location.is_a?(Location)
@@ -2041,10 +2049,10 @@ module Prism
2041
2049
  repository.enter(node_id, :opening_loc)
2042
2050
  end
2043
2051
 
2044
- # Represents the location of the closing `|`.
2052
+ # Represents the location of the closing `}` or `end`.
2045
2053
  #
2046
2054
  # [1, 2, 3].each { |i| puts x }
2047
- # ^
2055
+ # ^
2048
2056
  def closing_loc
2049
2057
  location = @closing_loc
2050
2058
  return location if location.is_a?(Location)
@@ -76,6 +76,15 @@ module Prism
76
76
  source.byteslice(byte_offset, length) or raise
77
77
  end
78
78
 
79
+ # Converts the line number and column in bytes to a byte offset.
80
+ def byte_offset(line, column)
81
+ normal = line - @start_line
82
+ raise IndexError if normal < 0
83
+ offsets.fetch(normal) + column
84
+ rescue IndexError
85
+ raise ArgumentError, "line #{line} is out of range"
86
+ end
87
+
79
88
  # Binary search through the offsets to find the line number for the given
80
89
  # byte offset.
81
90
  def line(byte_offset)
@@ -21,7 +21,7 @@ module Prism
21
21
 
22
22
  # The minor version of prism that we are expecting to find in the serialized
23
23
  # strings.
24
- MINOR_VERSION = 8
24
+ MINOR_VERSION = 9
25
25
 
26
26
  # The patch version of prism that we are expecting to find in the serialized
27
27
  # strings.
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prism
4
+ module Translation
5
+ class Ripper
6
+ class Filter # :nodoc:
7
+ # :stopdoc:
8
+ def initialize(src, filename = '-', lineno = 1)
9
+ @__lexer = Lexer.new(src, filename, lineno)
10
+ @__line = nil
11
+ @__col = nil
12
+ @__state = nil
13
+ end
14
+
15
+ def filename
16
+ @__lexer.filename
17
+ end
18
+
19
+ def lineno
20
+ @__line
21
+ end
22
+
23
+ def column
24
+ @__col
25
+ end
26
+
27
+ def state
28
+ @__state
29
+ end
30
+
31
+ def parse(init = nil)
32
+ data = init
33
+ @__lexer.lex.each do |pos, event, tok, state|
34
+ @__line, @__col = *pos
35
+ @__state = state
36
+ data = if respond_to?(event, true)
37
+ then __send__(event, tok, data)
38
+ else on_default(event, tok, data)
39
+ end
40
+ end
41
+ data
42
+ end
43
+
44
+ private
45
+
46
+ def on_default(event, token, data)
47
+ data
48
+ end
49
+ # :startdoc:
50
+ end
51
+ end
52
+ end
53
+ end