racc-rbs-inline 0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b215555957e98247fb76e1d65743e02e5b3ec8e84d4c143e40bb59c54b9ec51c
4
+ data.tar.gz: a809fc1c903ecbc510e63839f35d61dda7f349aa7abed5c5588acd4998f0fd47
5
+ SHA512:
6
+ metadata.gz: ef470246d25e93fa2331241b931e441c13622bfec4afbec0caa2222f6f63a262da9558046c2d33d5497a03881dabb3888b4099ce7d1de02b4904cb08a8ea9467
7
+ data.tar.gz: b3878b62cc6659442d201861fb4db51a9609de9fe4e7d6331b1176f56f4f6f4787beacf3eab1a5953911d952c844300bbe96fe5a4f84e278b0a1b753fab975ad
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,269 @@
1
+ # racc-rbs-inline
2
+
3
+ Add inline RBS annotations to a Racc-generated Ruby parser, without modifying Racc.
4
+ The annotations work with **rbs-inline 0.14** and **Steep 2.0's native inline support**.
5
+ Generated parsers keep their existing runtime dependencies.
6
+
7
+ ## Installation
8
+
9
+ Until the first RubyGems release, add this development dependency to your Gemfile:
10
+
11
+ ```ruby
12
+ group :development do
13
+ gem "racc-rbs-inline", git: "https://github.com/ydah/racc-rbs-inline"
14
+ gem "racc", "1.8.1"
15
+ gem "rbs-inline", "0.14.0" # only needed for the extraction workflow
16
+ gem "steep", "2.0.0"
17
+ end
18
+ ```
19
+
20
+ Run `bundle install`. The annotator requires Ruby 3.2+, Prism 1.0+, Racc 1.6.2+
21
+ (below 2.0), and RBS 4.1+ (below 5.0).
22
+
23
+ ## Usage
24
+
25
+ Add one command after your existing Racc invocation:
26
+
27
+ ```sh
28
+ bundle exec racc -o lib/parser.rb parser.y
29
+ bundle exec racc-rbs-inline annotate lib/parser.rb
30
+ ```
31
+
32
+ Or generate and annotate together:
33
+
34
+ ```sh
35
+ bundle exec racc-rbs-inline generate parser.y -o lib/parser.rb -- --embedded --frozen
36
+ ```
37
+
38
+ `-o` is required. Arguments after `--` are passed to Racc, which runs as a separate
39
+ process. Racc's exit status is preserved if generation fails. For executable
40
+ output with Racc 1.8.1, specify a path such as `--executable=/usr/bin/ruby`;
41
+ Racc itself rejects an omitted executable path on some Ruby versions.
42
+
43
+ ```sh
44
+ # Annotate several files in place.
45
+ bundle exec racc-rbs-inline annotate lib/parser.rb lib/other_parser.rb
46
+
47
+ # Print the transformed source, leaving files alone.
48
+ bundle exec racc-rbs-inline annotate --stdout lib/parser.rb
49
+
50
+ # Add semantic value types declared in the grammar.
51
+ bundle exec racc-rbs-inline annotate lib/parser.rb --grammar parser.y
52
+
53
+ # List files needing annotation; exit 1 if any need changes, otherwise 0.
54
+ bundle exec racc-rbs-inline annotate --check lib/parser.rb
55
+
56
+ # Copy runtime signatures (defaults to sig/racc/parser.rbs).
57
+ bundle exec racc-rbs-inline sig --output sig/racc
58
+
59
+ bundle exec racc-rbs-inline --help
60
+ bundle exec racc-rbs-inline --version
61
+ ```
62
+
63
+ Invalid Ruby, including invalid Ruby exposed when action wrappers are removed,
64
+ is rejected before writing the annotated file. `annotate` validates every input
65
+ before writing any of them. Repeating annotation leaves the file unchanged.
66
+
67
+ The library API returns source text without performing file I/O:
68
+
69
+ ```ruby
70
+ require "racc_rbs_inline"
71
+
72
+ annotated_source = RaccRbsInline.annotate(source)
73
+ RaccRbsInline.annotated?(annotated_source) # => true
74
+
75
+ # Supply grammar text to add or refresh semantic value types.
76
+ annotated_source = RaccRbsInline.annotate(source,
77
+ grammar: File.read("parser.y"), grammar_filename: "parser.y")
78
+ ```
79
+
80
+ `annotated?` detects the `# rbs_inline: enabled` comment. Without a grammar,
81
+ files already carrying that marker are left untouched. Supplying a grammar
82
+ validates the generated tables and refreshes reduction signatures, including
83
+ files that already have generic annotations.
84
+
85
+ ## Semantic value types
86
+
87
+ Declare terminal and nonterminal types in grammar comments:
88
+
89
+ ```ruby
90
+ class TypedCalc
91
+ # @racc-type NUMBER exp target : Integer
92
+ # @racc-type '+' '(' ')' : String
93
+ rule
94
+ target: exp { result = val[0] }
95
+ exp: NUMBER { result = val[0] }
96
+ | exp '+' NUMBER { result = val[0] + val[2] }
97
+ | '(' exp ')' { result = val[1] }
98
+ end
99
+ ```
100
+
101
+ `generate` and the Rake task read these declarations automatically. For an
102
+ existing generated parser, pass `annotate --grammar parser.y`. Use the same
103
+ Racc version that generated the parser: mismatched rule tables are rejected
104
+ before writing. Invalid RBS types, unknown symbols, and conflicting declarations
105
+ are also errors. Semantic type processing requires quoted grammar symbols to
106
+ be literal strings without Ruby interpolation.
107
+
108
+ Each reduction receives a tuple of the right-hand-side types and returns the
109
+ left-hand-side type. Undeclared symbols stay `untyped`. The initial `result`
110
+ parameter includes both the first right-hand-side type (or `nil` for an empty
111
+ rule) and the left-hand-side type. `no_result_var` grammars receive two
112
+ parameters instead. Racc 1.8's `?`, `*`, and `+` symbols become optional or array
113
+ types; groups become `Array[untyped]`. Generated helper reductions and
114
+ `_reduce_none` retain generic signatures.
115
+
116
+ With the declarations above, returning `val[0]` from the parenthesized rule is
117
+ reported as a `String`/`Integer` mismatch by both checking workflows. A runnable
118
+ example, including token handling, is in
119
+ [test/fixtures/typed_calc.y](test/fixtures/typed_calc.y).
120
+
121
+ ## Rake integration
122
+
123
+ ```ruby
124
+ require "racc_rbs_inline/rake_task"
125
+
126
+ RaccRbsInline::RakeTask.new(:parser) do |task|
127
+ task.grammar = "parser.y"
128
+ task.output = "lib/parser.rb"
129
+ task.options = ["--embedded", "--frozen"]
130
+ end
131
+ ```
132
+
133
+ `bundle exec rake parser` regenerates and annotates the output when the grammar
134
+ is newer or the output is absent. The output is also available as a file task.
135
+
136
+ ## Type checking
137
+
138
+ First copy the `Racc::Parser` runtime signatures:
139
+
140
+ ```sh
141
+ bundle exec racc-rbs-inline sig
142
+ ```
143
+
144
+ These signatures live in `data/racc` inside the gem, so they are not automatically
145
+ loaded with this gem's own signatures. Racc signatures have been submitted in
146
+ [gem_rbs_collection PR #1085](https://github.com/ruby/gem_rbs_collection/pull/1085).
147
+ Until they are accepted, use the `sig` command. Once collection support is
148
+ available, use `rbs collection install`
149
+ and remove your copied `sig/racc` directory to avoid duplicate declarations.
150
+
151
+ ### Native inline support (Steep 2.0)
152
+
153
+ ```ruby
154
+ # Steepfile
155
+ target :lib do
156
+ signature "sig/racc"
157
+ check "lib", inline: true
158
+ end
159
+ ```
160
+
161
+ ```sh
162
+ bundle exec steep check
163
+ ```
164
+
165
+ ### Extract signatures with rbs-inline
166
+
167
+ ```ruby
168
+ # Steepfile
169
+ target :lib do
170
+ signature "sig"
171
+ check "lib"
172
+ end
173
+ ```
174
+
175
+ ```sh
176
+ bundle exec rbs-inline --output=sig/generated lib
177
+ bundle exec steep check
178
+ ```
179
+
180
+ Choose one workflow; do not load extracted declarations while also enabling
181
+ native inline checking for the same parser.
182
+
183
+ A complete small grammar is in [test/fixtures/sum.y](test/fixtures/sum.y).
184
+ Copy that grammar to `parser.y` in a project with the Gemfile above:
185
+
186
+ ```sh
187
+ mkdir -p lib
188
+ bundle exec racc-rbs-inline generate parser.y -o lib/parser.rb
189
+ bundle exec racc-rbs-inline sig
190
+ # Save either Steepfile above, then run that workflow's commands.
191
+ ```
192
+
193
+ The integration tests exercise this grammar through both workflows, including
194
+ strict diagnostics. To enable strict diagnostics in your own target, add
195
+ `configure_code_diagnostics(Steep::Diagnostic::Ruby.strict)`.
196
+
197
+ ## What gets annotated
198
+
199
+ The annotator locates classes whose bodies assign `Racc_arg`. It unwraps their
200
+ heredoc `module_eval` calls, then annotates ten tables, three constants, and all
201
+ `_reduce_N` / `_reduce_none` methods. Reduction arguments and results use
202
+ `untyped` unless grammar declarations supply semantic value types.
203
+
204
+ User-written actions, `header`, `inner`, and `footer` code remain your
205
+ responsibility to type. Existing annotations in `inner` become visible after
206
+ unwrapping. Separate instance variable annotations from method definitions
207
+ with a blank line:
208
+
209
+ ```ruby
210
+ ---- inner
211
+ # @rbs @tokens: Array[Array[untyped]]
212
+
213
+ #: (Array[Array[untyped]]) -> void
214
+ def initialize(tokens)
215
+ @tokens = tokens
216
+ end
217
+ ```
218
+
219
+ **Backtrace locations change:** unwrapping `module_eval` means exceptions point
220
+ to the generated `.rb` file instead of the grammar's `.y` lines, as with Racc's
221
+ `--no-line-convert` option. Code relying on `__FILE__`, `__LINE__`, or lexical
222
+ scope inside those wrappers should account for the same change.
223
+
224
+ The implementation preserves other source bytes, including embedded runtime
225
+ code, header/footer wrappers, encoding declarations, and line endings.
226
+
227
+ ## Development
228
+
229
+ ```sh
230
+ bundle install
231
+ bundle exec rbs collection install
232
+ ruby script/fetch-racc
233
+ bundle exec rake
234
+ ```
235
+
236
+ `rake test` runs unit tests, 13 golden/behavior variants, both type-checking
237
+ workflows, semantic value checks, deliberately corrupt table tests, and the
238
+ upstream Racc corpus.
239
+ The corpus is fetched from the pinned `v1.8.1` tag into ignored `tmp/racc` and is
240
+ not redistributed. Set `RACC_CORPUS` to use another local copy. Without the
241
+ corpus, that regression test reports a skip; CI always fetches it.
242
+
243
+ The 58 upstream grammars include three intentional generator errors and two
244
+ with invalid Ruby actions. All other 53 are checked separately to avoid class
245
+ name collisions. User-code signature failures are documented in
246
+ [test/regression_test.rb](test/regression_test.rb); generated table diagnostics
247
+ must be absent. The embedded runtime is checked byte-for-byte while golden
248
+ files normalize its representation, version banners, random delimiter IDs,
249
+ and executable paths.
250
+
251
+ `rake typecheck` regenerates this gem's own signatures and checks them with
252
+ Steep. CI covers Ruby 3.2, 3.3, 3.4, and 4.0, with Racc 1.8.1 as the baseline
253
+ and additional Racc 1.6.2 / 1.7.3 compatibility jobs.
254
+ Racc 1.6.2 does not support `--frozen`; that variant is skipped there.
255
+ Racc 1.6.2 and 1.7.3 cannot generate the four upstream grammars using Racc 1.8's
256
+ group/optional/repetition syntax, so their corpus checks cover the remaining 49.
257
+
258
+ The separate Lrama acceptance job checks the pinned upstream parser before and
259
+ after generation with `--embedded --frozen` (79 examples each), then checks
260
+ generated annotations with strict Steep diagnostics. Run it locally with
261
+ `bundle exec ruby script/test-lrama`; it fetches Lrama into ignored `tmp/lrama`
262
+ and writes reports to `tmp/lrama-acceptance`. It restores the original parser
263
+ after the check and requires a clean checkout at the pinned revision.
264
+ Application type errors remain in the report; two unrelated profiler signatures
265
+ using unsupported `void` unions are excluded from the temporary checking project.
266
+
267
+ ## License
268
+
269
+ [MIT](LICENSE.txt).
@@ -0,0 +1,99 @@
1
+ # Type signatures for the Racc runtime (lib/racc/parser.rb, racc 1.8.x)
2
+ module Racc
3
+ VERSION: String
4
+ Version: String
5
+ Copyright: String
6
+
7
+ Racc_No_Extensions: bool
8
+
9
+ class ParseError < StandardError
10
+ end
11
+
12
+ class Parser
13
+ # Token symbols as serialized in racc_token_table
14
+ type token_symbol = Symbol | String | false
15
+
16
+ # The 14-element Racc_arg tuple (same order as ParserFileGenerator emits)
17
+ type racc_arg = [
18
+ Array[Integer?], # action_table
19
+ Array[Integer?], # action_check
20
+ Array[Integer?], # action_default
21
+ Array[Integer?], # action_pointer
22
+ Array[Integer?], # goto_table
23
+ Array[Integer?], # goto_check
24
+ Array[Integer?], # goto_default
25
+ Array[Integer?], # goto_pointer
26
+ Integer, # nt_base
27
+ Array[Integer | Symbol], # reduce_table
28
+ Hash[untyped, Integer], # token_table
29
+ Integer, # shift_n
30
+ Integer, # reduce_n
31
+ bool # use_result_var
32
+ ]
33
+
34
+ interface _DebugOut
35
+ def print: (*untyped) -> untyped
36
+ def puts: (*untyped) -> untyped
37
+ end
38
+
39
+ Racc_Runtime_Version: String
40
+ Racc_Runtime_Core_Version_R: String
41
+ Racc_Runtime_Core_Version_C: String
42
+ Racc_Main_Parsing_Routine: Symbol
43
+ Racc_YY_Parse_Method: Symbol
44
+ Racc_Runtime_Core_Version: String
45
+ Racc_Runtime_Type: String
46
+
47
+ @yydebug: bool
48
+ @racc_debug_out: _DebugOut
49
+ @racc_state: Array[Integer]
50
+ @racc_tstack: Array[Integer]
51
+ @racc_vstack: Array[untyped]
52
+ @racc_t: Integer?
53
+ @racc_val: untyped
54
+ @racc_read_next: bool
55
+ @racc_user_yyerror: bool
56
+ @racc_error_status: Integer
57
+
58
+ def self.racc_runtime_type: () -> String
59
+
60
+ # Subclasses define their own constructors; keep it permissive so that
61
+ # unannotated overrides do not inherit `() -> void` from Object.
62
+ def initialize: (?) -> void
63
+
64
+ # Methods users commonly override: keep types permissive.
65
+ def next_token: () -> Array[untyped]?
66
+ def on_error: (Integer t, untyped val, Array[untyped] vstack) -> untyped
67
+
68
+ def do_parse: () -> untyped
69
+ def yyparse: (untyped recv, interned mid) -> untyped
70
+ def yyerror: () -> bot
71
+ def yyaccept: () -> bot
72
+ def yyerrok: () -> void
73
+ def token_to_str: (Integer t) -> String?
74
+
75
+ def racc_read_token: (Integer t, untyped tok, untyped val) -> void
76
+ def racc_shift: (Integer tok, Array[Integer] tstack, Array[untyped] vstack) -> void
77
+ def racc_reduce: (Array[Integer] toks, Integer sim, Array[Integer] tstack, Array[untyped] vstack) -> void
78
+ def racc_accept: () -> void
79
+ def racc_e_pop: (Array[Integer] state, Array[Integer] tstack, Array[untyped] vstack) -> void
80
+ def racc_next_state: (Integer curstate, Array[Integer] state) -> void
81
+ def racc_print_stacks: (Array[Integer] t, Array[untyped] v) -> void
82
+ def racc_print_states: (Array[Integer] s) -> void
83
+ def racc_token2str: (Integer tok) -> String
84
+
85
+ def _racc_setup: () -> racc_arg
86
+ def _racc_init_sysvars: () -> void
87
+ def _racc_do_parse_rb: (racc_arg arg, bool in_debug) -> untyped
88
+ def _racc_yyparse_rb: (untyped recv, interned mid, racc_arg arg, bool c_debug) -> untyped
89
+ def _racc_evalact: (Integer act, racc_arg arg) -> Integer?
90
+ def _racc_do_reduce: (racc_arg arg, Integer act) -> Integer?
91
+
92
+ private
93
+
94
+ def _racc_do_parse_c: (racc_arg arg, bool in_debug) -> untyped
95
+ def _racc_yyparse_c: (untyped recv, interned mid, racc_arg arg, bool c_debug) -> untyped
96
+ end
97
+ end
98
+
99
+ class ::ParseError = Racc::ParseError
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "racc_rbs_inline/cli"
5
+
6
+ exit RaccRbsInline::CLI.run(ARGV)
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require "prism"
5
+
6
+ module RaccRbsInline
7
+ class Annotator
8
+ MAGIC = "# rbs_inline: enabled" #: String
9
+ INTEGER_TABLE = /\Aracc_(?:action|goto)_(?:table|check|pointer|default)\z/ #: Regexp
10
+ REDUCTION = /\A_reduce_(?:\d+|none)\z/ #: Regexp
11
+ CONSTANT_TYPES = {
12
+ Racc_arg: "::Racc::Parser::racc_arg",
13
+ Racc_token_to_s_table: "Array[String]",
14
+ Racc_debug_parser: "bool"
15
+ }.freeze #: Hash[Symbol, String]
16
+
17
+ #: (String source, ?grammar: String?, ?grammar_filename: String) -> String
18
+ def annotate(source, grammar: nil, grammar_filename: "grammar.y")
19
+ existing = annotated?(source)
20
+ return source if existing && !grammar
21
+
22
+ types = nil #: GrammarTypes?
23
+ if grammar
24
+ require_relative "grammar_types"
25
+ types = GrammarTypes.new(grammar, filename: grammar_filename)
26
+ end
27
+
28
+ source = unwrap(source)
29
+ source = add_annotations(source, types: types, existing: existing)
30
+ existing ? source : add_magic_comment(source)
31
+ end
32
+
33
+ #: (String source) -> bool
34
+ def annotated?(source)
35
+ return false unless source.include?(MAGIC)
36
+
37
+ Prism.parse(source).comments.any? { |comment| comment.location.slice.strip == MAGIC }
38
+ end
39
+
40
+ private
41
+
42
+ #: (String source) -> String
43
+ def unwrap(source)
44
+ edits = [] #: Array[[Integer, Integer, String]]
45
+ binary = source.b
46
+ parser_bodies(parse!(source)).each do |body|
47
+ body.body.each do |statement|
48
+ next unless statement.is_a?(Prism::CallNode) && statement.name == :module_eval && statement.receiver.nil?
49
+
50
+ heredoc = statement.arguments&.arguments&.first
51
+ next unless heredoc.is_a?(Prism::StringNode) && heredoc.heredoc?
52
+ closing = heredoc.closing_loc
53
+ next unless closing
54
+
55
+ start = line_start(binary, statement.location.start_offset)
56
+ edits << [start, closing.end_offset, heredoc.content_loc.slice.b]
57
+ end
58
+ end
59
+ apply(source, edits)
60
+ end
61
+
62
+ #: (String source, types: GrammarTypes?, existing: bool) -> String
63
+ def add_annotations(source, types:, existing:)
64
+ edits = [] #: Array[[Integer, Integer, String]]
65
+ binary = source.b
66
+ parser_bodies(parse!(source)).each do |body|
67
+ signatures = types ? types.signatures(body: body) : {} #: Hash[Symbol, String]
68
+ body.body.each do |statement|
69
+ case statement
70
+ when Prism::LocalVariableWriteNode
71
+ type = table_type(statement)
72
+ edits << trailing(statement, type) if type && !existing
73
+ when Prism::ConstantWriteNode
74
+ type = CONSTANT_TYPES[statement.name]
75
+ edits << trailing(statement, type) if type && !existing
76
+ when Prism::DefNode
77
+ next unless statement.name.to_s.match?(REDUCTION)
78
+
79
+ count = statement.parameters&.requireds&.size
80
+ unless [2, 3].include?(count)
81
+ raise Error, "Unsupported parameters for #{statement.name}: expected two or three required parameters"
82
+ end
83
+ parameters = "Array[untyped] val, Array[untyped] _values"
84
+ parameters += ", untyped result" if count == 3
85
+ start = line_start(binary, statement.location.start_offset)
86
+ indent = binary.byteslice(start, statement.location.start_offset - start)
87
+ signature = signatures.fetch(statement.name, "#: (#{parameters}) -> untyped")
88
+ finish = start
89
+ if existing && start.positive?
90
+ previous = line_start(binary, start - 1)
91
+ line = binary.byteslice(previous, start - previous)
92
+ start = previous if line&.match?(/\A[ \t]*#:/)
93
+ end
94
+ edits << [start, finish, "#{indent}#{signature}#{newline(source)}".b]
95
+ end
96
+ end
97
+ end
98
+ apply(source, edits)
99
+ end
100
+
101
+ #: (Prism::LocalVariableWriteNode node) -> String?
102
+ def table_type(node)
103
+ name = node.name.to_s
104
+ return "Array[Integer?]" if name.match?(INTEGER_TABLE)
105
+ return "Array[Integer | Symbol]" if name == "racc_reduce_table"
106
+ return unless name == "racc_token_table"
107
+
108
+ literal = node.value.is_a?(Prism::HashNode) && node.value.elements.all? do |element|
109
+ element.is_a?(Prism::AssocNode) &&
110
+ [Prism::SymbolNode, Prism::StringNode, Prism::FalseNode].any? { |type| element.key.is_a?(type) }
111
+ end
112
+ literal ? "Hash[::Racc::Parser::token_symbol, Integer]" : "Hash[untyped, Integer]"
113
+ end
114
+
115
+ #: (Prism::Node node, String type) -> [Integer, Integer, String]
116
+ def trailing(node, type)
117
+ offset = node.location.end_offset
118
+ [offset, offset, " #: #{type}".b]
119
+ end
120
+
121
+ #: (String source) -> String
122
+ def add_magic_comment(source)
123
+ offset = 0
124
+ source.b.each_line do |line|
125
+ break unless line.start_with?("#!") || line.match?(/\A#[^\r\n]*(?:frozen[_-]string[_-]literal\s*:|(?:coding|encoding)\s*[:=])/i)
126
+
127
+ offset += line.bytesize
128
+ end
129
+ apply(source, [[offset, offset, "#{MAGIC}#{newline(source)}".b]])
130
+ end
131
+
132
+ #: (String source) -> String
133
+ def newline(source)
134
+ source.include?("\r\n") ? "\r\n" : "\n"
135
+ end
136
+
137
+ #: (String source) -> Prism::ProgramNode
138
+ def parse!(source)
139
+ result = Prism.parse(source)
140
+ if result.failure?
141
+ details = result.errors.map { |error| "line #{error.location.start_line}: #{error.message}" }.join(", ")
142
+ raise Error, "Invalid Ruby source: #{details}"
143
+ end
144
+ result.value
145
+ end
146
+
147
+ #: (Prism::ProgramNode program) -> Array[Prism::StatementsNode]
148
+ def parser_bodies(program)
149
+ bodies = [] #: Array[Prism::StatementsNode]
150
+ pending = [program] #: Array[Prism::Node]
151
+ while (node = pending.pop)
152
+ if node.is_a?(Prism::ClassNode) && node.body.is_a?(Prism::StatementsNode) &&
153
+ node.body.body.any? { |statement| statement.is_a?(Prism::ConstantWriteNode) && statement.name == :Racc_arg }
154
+ bodies << node.body
155
+ end
156
+ pending.concat(node.compact_child_nodes)
157
+ end
158
+ raise Error, "Racc parser class not found" if bodies.empty?
159
+
160
+ bodies
161
+ end
162
+
163
+ #: (String binary, Integer offset) -> Integer
164
+ def line_start(binary, offset)
165
+ return 0 if offset.zero?
166
+
167
+ previous = binary.rindex("\n", offset - 1)
168
+ previous ? previous + 1 : 0
169
+ end
170
+
171
+ #: (String source, Array[[Integer, Integer, String]] edits) -> String
172
+ def apply(source, edits)
173
+ binary = source.b
174
+ edits.sort_by { |start, _, _| -start }.each do |start, finish, replacement|
175
+ binary[start...finish] = replacement
176
+ end
177
+ binary.force_encoding(source.encoding)
178
+ end
179
+ end
180
+ end