spoom 1.7.16 → 1.8.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,484 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Sorbet
6
+ module Translate
7
+ module RBSCommentsToSorbetSigs
8
+ # @abstract
9
+ class BaseTranslator < Translator
10
+ include Spoom::RBS::ExtractRBSComments
11
+
12
+ #: (String, file: String, ?options: Options) -> void
13
+ def initialize(
14
+ ruby_contents,
15
+ file:,
16
+ options: Options.default
17
+ )
18
+ super(ruby_contents, file:)
19
+
20
+ @max_line_length = case (format = options.output_format)
21
+ when HumanReadableRBIFormat
22
+ format.max_line_length
23
+ else
24
+ nil
25
+ end #: Integer?
26
+
27
+ @overloads_strategy = options.overloads_strategy #: Symbol
28
+ @type_translator = RBI::RBS::TypeTranslator.new #: RBI::RBS::TypeTranslator
29
+ end
30
+
31
+ # @override
32
+ #: (Prism::ProgramNode node) -> void
33
+ def visit_program_node(node)
34
+ # Process all type aliases from the entire file first
35
+ apply_type_aliases(@comments)
36
+
37
+ # Now process the rest of the file with type aliases available
38
+ super
39
+ end
40
+
41
+ # @override
42
+ #: (Prism::ClassNode node) -> void
43
+ def visit_class_node(node)
44
+ apply_class_annotations(node)
45
+
46
+ super
47
+ end
48
+
49
+ # @override
50
+ #: (Prism::ModuleNode node) -> void
51
+ def visit_module_node(node)
52
+ apply_class_annotations(node)
53
+
54
+ super
55
+ end
56
+
57
+ # @override
58
+ #: (Prism::SingletonClassNode node) -> void
59
+ def visit_singleton_class_node(node)
60
+ apply_class_annotations(node)
61
+
62
+ super
63
+ end
64
+
65
+ # @override
66
+ #: (Prism::DefNode node) -> void
67
+ def visit_def_node(node)
68
+ rewrite_def(node, node_rbs_comments(node))
69
+ end
70
+
71
+ # @override
72
+ #: (Prism::CallNode node) -> void
73
+ def visit_call_node(node)
74
+ case node.message
75
+ when "attr_reader", "attr_writer", "attr_accessor"
76
+ visit_attr(node)
77
+ else
78
+ def_node = node.arguments&.arguments&.first
79
+ if def_node&.is_a?(Prism::DefNode)
80
+ rewrite_def(def_node, node_rbs_comments(node))
81
+ return
82
+ end
83
+
84
+ super
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ #: (Prism::CallNode) -> void
91
+ def visit_attr(node)
92
+ comments = node_rbs_comments(node)
93
+ return if comments.empty?
94
+
95
+ return if comments.signatures.empty?
96
+
97
+ signatures = apply_overloads_strategy(
98
+ comments.signatures,
99
+ method_name: node.message.to_s,
100
+ location: "#{@file}:#{node.location.start_line}",
101
+ )
102
+
103
+ known_annotations = nil #: Array[Spoom::RBS::Annotation]?
104
+
105
+ signatures.each do |signature|
106
+ attr_type = ::RBS::Parser.parse_type(signature.string)
107
+ sig = RBI::Sig.new
108
+
109
+ if node.message == "attr_writer"
110
+ if node.arguments&.arguments&.size != 1
111
+ raise Error, "AttrWriter must have exactly one name"
112
+ end
113
+
114
+ name = node.arguments&.arguments&.first #: as Prism::SymbolNode
115
+ sig.params << RBI::SigParam.new(
116
+ name.slice[1..-1], #: as String
117
+ @type_translator.translate(attr_type),
118
+ )
119
+ end
120
+
121
+ sig.return_type = @type_translator.translate(attr_type)
122
+
123
+ known_annotations = apply_member_annotations(comments.method_annotations, sig)
124
+
125
+ @rewriter << Source::Replace.new(
126
+ signature.location.start_offset,
127
+ signature.location.end_offset,
128
+ pad_out_line_count(of: sig.string(max_line_length: @max_line_length), to_height_of: signature),
129
+ )
130
+ rescue ::RBS::ParsingError, ::RBI::Error
131
+ # Ignore signatures with errors
132
+ next
133
+ end
134
+
135
+ if known_annotations
136
+ rewrite_member_annotations(comments.method_annotations, known: known_annotations)
137
+ end
138
+ end
139
+
140
+ #: (Prism::DefNode, Spoom::RBS::Comments) -> void
141
+ def rewrite_def(def_node, comments)
142
+ return if comments.empty?
143
+ return if comments.signatures.empty?
144
+
145
+ signatures = apply_overloads_strategy(
146
+ comments.signatures,
147
+ method_name: def_node.name.to_s,
148
+ location: "#{@file}:#{def_node.location.start_line}",
149
+ )
150
+
151
+ builder = RBI::Parser::TreeBuilder.new(@ruby_contents, comments: [], file: @file)
152
+ builder.visit(def_node)
153
+ rbi_node = builder.tree.nodes.first #: as RBI::Method
154
+
155
+ known_annotations = nil #: Array[Spoom::RBS::Annotation]?
156
+
157
+ signatures.each do |signature|
158
+ begin
159
+ method_type = ::RBS::Parser.parse_method_type(signature.string)
160
+ rescue ::RBS::ParsingError
161
+ next
162
+ end
163
+
164
+ translator = RBI::RBS::MethodTypeTranslator.new(rbi_node)
165
+
166
+ begin
167
+ translator.visit(method_type)
168
+ rescue ::RBI::Error
169
+ next
170
+ end
171
+
172
+ sig = translator.result
173
+
174
+ known_annotations = apply_member_annotations(comments.method_annotations, sig)
175
+
176
+ # Sorbet runtime doesn't support `sig` on `method_added` or
177
+ # `singleton_method_added`, so we always use `without_runtime` for them.
178
+ if def_node.name == :method_added || def_node.name == :singleton_method_added
179
+ sig.without_runtime = true
180
+ end
181
+
182
+ @rewriter << Source::Replace.new(
183
+ signature.location.start_offset,
184
+ signature.location.end_offset,
185
+ pad_out_line_count(of: sig.string(max_line_length: @max_line_length), to_height_of: signature),
186
+ )
187
+ end
188
+
189
+ if known_annotations
190
+ rewrite_member_annotations(comments.method_annotations, known: known_annotations)
191
+ end
192
+ end
193
+
194
+ #: (Array[Spoom::RBS::Signature], method_name: String, location: String) -> Array[Spoom::RBS::Signature]
195
+ def apply_overloads_strategy(signatures, method_name:, location:)
196
+ return signatures if signatures.size <= 1
197
+
198
+ case @overloads_strategy
199
+ when :translate_all
200
+ signatures
201
+ when :translate_last
202
+ others = signatures[0...-1] #: as !nil
203
+ others.each { |signature| rewrite_discarded_overload(signature) }
204
+
205
+ kept = signatures.last #: as Spoom::RBS::Signature
206
+ [kept]
207
+ else # :raise
208
+ raise Error, "Method `#{method_name}` at #{location} has multiple overloaded signatures"
209
+ end
210
+ end
211
+
212
+ # Called for every overloaded method sig that we discard because it wasn't the last one.
213
+ # @abstract
214
+ #: (Spoom::RBS::Signature) -> void
215
+ def rewrite_discarded_overload(signature) = raise
216
+
217
+ #: (PrismTypes::anyScopeNode) -> void
218
+ def apply_class_annotations(node)
219
+ comments = node_rbs_comments(node)
220
+ return if comments.empty?
221
+
222
+ insert_pos = case node
223
+ when Prism::ClassNode
224
+ (node.superclass || node.constant_path).location.end_offset
225
+ when Prism::ModuleNode
226
+ node.constant_path.location.end_offset
227
+ when Prism::SingletonClassNode
228
+ node.expression.location.end_offset
229
+ end
230
+
231
+ # Only translate (and `extend T::Helpers`) when there's at least one *known* class
232
+ # annotation. A node with only unknown annotations (e.g. `@private`) is left untouched.
233
+ if comments.class_annotations.any?
234
+ unless already_extends?(node, /^(::)?T::Helpers$/)
235
+ extend_with("T::Helpers", into: node, at: insert_pos)
236
+ end
237
+
238
+ comments.annotations.reverse_each do |annotation|
239
+ content = case annotation.string
240
+ when "@abstract"
241
+ "abstract!"
242
+ when "@interface"
243
+ "interface!"
244
+ when "@sealed"
245
+ "sealed!"
246
+ when "@final"
247
+ "final!"
248
+ when /^@requires_ancestor: /
249
+ srb_type = ::RBS::Parser.parse_type(annotation.string.delete_prefix("@requires_ancestor: "))
250
+ rbs_type = @type_translator.translate(srb_type)
251
+ "requires_ancestor { #{rbs_type} }"
252
+ else
253
+ apply_class_annotation(annotation, parent_node: node, insert_pos:, sorbet_replacement: nil)
254
+ next
255
+ end
256
+
257
+ apply_class_annotation(annotation, parent_node: node, insert_pos:, sorbet_replacement: content)
258
+ rescue ::RBS::ParsingError, ::RBI::Error
259
+ apply_class_annotation(annotation, parent_node: node, insert_pos:, sorbet_replacement: nil)
260
+ next
261
+ end
262
+ end
263
+
264
+ signatures = comments.signatures
265
+ if signatures.any?
266
+ signatures.each do |signature|
267
+ # Only type param signatures (e.g. `#: [A, B]`) are valid on class/module nodes
268
+ next unless signature.string.start_with?("[")
269
+
270
+ type_params = ::RBS::Parser.parse_type_params(signature.string)
271
+ rewrite_type_params_signature(signature, type_params:)
272
+ next if type_params.empty?
273
+
274
+ unless already_extends?(node, /^(::)?T::Generic$/)
275
+ extend_with("T::Generic", into: node, at: insert_pos)
276
+ end
277
+
278
+ type_params.each do |type_param|
279
+ type_member = "#{type_param.name} = type_member"
280
+
281
+ case type_param.variance
282
+ when :covariant
283
+ type_member = "#{type_member}(:out)"
284
+ when :contravariant
285
+ type_member = "#{type_member}(:in)"
286
+ end
287
+
288
+ if type_param.upper_bound || type_param.default_type
289
+ if type_param.upper_bound
290
+ rbs_type = @type_translator.translate(type_param.upper_bound)
291
+ type_member = "#{type_member} {{ upper: #{rbs_type} }}"
292
+ end
293
+
294
+ if type_param.default_type
295
+ rbs_type = @type_translator.translate(type_param.default_type)
296
+ type_member = "#{type_member} {{ fixed: #{rbs_type} }}"
297
+ end
298
+ end
299
+
300
+ insert_type_member(type_member, parent_node: node, insert_pos:)
301
+ rescue ::RBS::ParsingError, ::RBI::Error
302
+ # Ignore signatures with errors
303
+ next
304
+ end
305
+ end
306
+ end
307
+ end
308
+
309
+ # @param is_known: true if this is an RBS annotation that we recognize
310
+ # false for some other `@`-prefixed thing, like a documentation `@param` tag.
311
+ # @abstract
312
+ #: (
313
+ #| Spoom::RBS::Annotation,
314
+ #| parent_node: PrismTypes::anyScopeNode,
315
+ #| insert_pos: Integer,
316
+ #| sorbet_replacement: String?
317
+ #| ) -> void
318
+ def apply_class_annotation(annotation, parent_node:, insert_pos:, sorbet_replacement:) = raise
319
+
320
+ # Rewrites the `#: [...]` type params comment (e.g. delete it, or mark it as translated).
321
+ # @abstract
322
+ #: (Spoom::RBS::Signature, type_params: Array[::RBS::AST::TypeParam]) -> void
323
+ def rewrite_type_params_signature(signature, type_params:) = raise
324
+
325
+ # Inserts a single `type_member` declaration into the class/module body.
326
+ # @abstract
327
+ #: (String type_member, parent_node: PrismTypes::anyScopeNode, insert_pos: Integer) -> void
328
+ def insert_type_member(type_member, parent_node:, insert_pos:) = raise
329
+
330
+ #: (Array[Spoom::RBS::Annotation], RBI::Sig) -> Array[Spoom::RBS::Annotation]
331
+ def apply_member_annotations(annotations, sig)
332
+ known = [] #: Array[Spoom::RBS::Annotation]
333
+
334
+ annotations.each do |annotation|
335
+ case annotation.string
336
+ when "@abstract"
337
+ sig.is_abstract = true
338
+ when "@final"
339
+ sig.is_final = true
340
+ when "@override"
341
+ sig.is_override = true
342
+ when "@override(allow_incompatible: true)"
343
+ sig.is_override = true
344
+ sig.allow_incompatible_override = true
345
+ when "@override(allow_incompatible: :visibility)"
346
+ sig.is_override = true
347
+ sig.allow_incompatible_override_visibility = true
348
+ when "@overridable"
349
+ sig.is_overridable = true
350
+ when "@without_runtime"
351
+ sig.without_runtime = true
352
+ else
353
+ next
354
+ end
355
+
356
+ known << annotation
357
+ end
358
+
359
+ known
360
+ end
361
+
362
+ # Rewrites the member annotation comments in the source. Called once per method,
363
+ # regardless of how many overloaded signatures share the annotations, to avoid
364
+ # emitting duplicate markers.
365
+ #
366
+ #: (Array[Spoom::RBS::Annotation], known: Array[Spoom::RBS::Annotation]) -> void
367
+ def rewrite_member_annotations(annotations, known:)
368
+ annotations.each do |annotation|
369
+ rewrite_annotation(annotation, is_known: known.include?(annotation))
370
+ end
371
+ end
372
+
373
+ # @param is_known: true if this is an RBS annotation that we recognize
374
+ # false for some other `@`-prefixed thing, like a documentation `@param` tag.
375
+ # @overridable
376
+ #: (Spoom::RBS::Annotation, is_known: bool) -> void
377
+ def rewrite_annotation(annotation, is_known:) = nil # no-op
378
+
379
+ # @abstract
380
+ #: (String mixin_name, into: PrismTypes::anyScopeNode, at: Integer) -> void
381
+ def extend_with(mixin_name, into:, at:) = raise
382
+
383
+ #: (PrismTypes::anyScopeNode, Regexp) -> bool
384
+ def already_extends?(node, constant_regex)
385
+ node.child_nodes.any? do |c|
386
+ next false unless c.is_a?(Prism::CallNode)
387
+ next false unless c.message == "extend"
388
+ next false unless c.receiver.nil? || c.receiver.is_a?(Prism::SelfNode)
389
+ next false unless c.arguments&.arguments&.size == 1
390
+
391
+ arg = c.arguments&.arguments&.first
392
+ next false unless arg.is_a?(Prism::ConstantPathNode)
393
+ next false unless arg.slice.match?(constant_regex)
394
+
395
+ true
396
+ end
397
+ end
398
+
399
+ #: (Array[Prism::Comment]) -> Array[Spoom::RBS::TypeAlias]
400
+ def collect_type_aliases(comments)
401
+ type_aliases = [] #: Array[Spoom::RBS::TypeAlias]
402
+
403
+ return type_aliases if comments.empty?
404
+
405
+ continuation_comments = [] #: Array[Prism::Comment]
406
+
407
+ comments.reverse_each do |comment|
408
+ string = comment.slice
409
+
410
+ if string.start_with?("#:")
411
+ string = string.delete_prefix("#:").strip
412
+ location = comment.location
413
+
414
+ if string.start_with?("type ")
415
+ continuation_comments.reverse_each do |continuation_comment|
416
+ string = "#{string}#{continuation_comment.slice.delete_prefix("#|")}"
417
+ location = location.join(continuation_comment.location)
418
+ end
419
+
420
+ type_aliases << Spoom::RBS::TypeAlias.new(string, location)
421
+ end
422
+
423
+ # Clear the continuation comments regardless of whether we found a type alias or not
424
+ continuation_comments.clear
425
+ elsif string.start_with?("#|")
426
+ continuation_comments << comment
427
+ else
428
+ continuation_comments.clear
429
+ end
430
+ end
431
+
432
+ type_aliases
433
+ end
434
+
435
+ #: (Array[Prism::Comment]) -> void
436
+ def apply_type_aliases(comments)
437
+ type_aliases = collect_type_aliases(comments)
438
+
439
+ type_aliases.each do |type_alias|
440
+ indent = " " * type_alias.location.start_column
441
+ insert_pos = adjust_to_line_start(type_alias.location.start_offset)
442
+
443
+ from = insert_pos
444
+ to = adjust_to_line_end(type_alias.location.end_offset)
445
+
446
+ *, decls = ::RBS::Parser.parse_signature(type_alias.string)
447
+
448
+ # We only expect there to be a single type alias declaration
449
+ next unless decls.size == 1 && decls.first.is_a?(::RBS::AST::Declarations::TypeAlias)
450
+
451
+ rbs_type = decls.first
452
+ sorbet_type = @type_translator.translate(rbs_type.type)
453
+
454
+ alias_name = ::RBS::TypeName.new(
455
+ namespace: rbs_type.name.namespace,
456
+ name: rbs_type.name.name.to_s.gsub(/(?:^|_)([a-z\d]*)/i) do |match|
457
+ match = match.delete_prefix("_")
458
+ !match.empty? ? match[0].upcase.concat(match[1..-1]) : +""
459
+ end,
460
+ )
461
+
462
+ @rewriter << Source::Delete.new(from, to)
463
+ content = "#{indent}#{alias_name} = T.type_alias { #{sorbet_type.to_rbi} }\n"
464
+ content = pad_out_line_count(of: content, to_height_of: type_alias)
465
+ @rewriter << Source::Insert.new(insert_pos, content)
466
+ rescue ::RBS::ParsingError, ::RBI::Error
467
+ # Ignore type aliases with errors
468
+ next
469
+ end
470
+ end
471
+
472
+ # @overridable
473
+ #: (of: String, to_height_of: Spoom::RBS::Comment) -> String
474
+ def pad_out_line_count(of:, to_height_of:)
475
+ replacement = of
476
+
477
+ # no-op implementation
478
+ replacement
479
+ end
480
+ end
481
+ end
482
+ end
483
+ end
484
+ end
@@ -0,0 +1,72 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/base_translator"
5
+ require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/options"
6
+
7
+ module Spoom
8
+ module Sorbet
9
+ module Translate
10
+ module RBSCommentsToSorbetSigs
11
+ class HumanReadableTranslator < BaseTranslator
12
+ private
13
+
14
+ # Deletes the discarded overload from the source codes
15
+ # @override
16
+ #: (Spoom::RBS::Signature) -> void
17
+ def rewrite_discarded_overload(signature)
18
+ from = adjust_to_line_start(signature.location.start_offset)
19
+ to = adjust_to_line_end(signature.location.end_offset)
20
+ @rewriter << Source::Delete.new(from, to)
21
+ end
22
+
23
+ # @override
24
+ #: (
25
+ #| Spoom::RBS::Annotation,
26
+ #| parent_node: PrismTypes::anyScopeNode,
27
+ #| insert_pos: Integer,
28
+ #| sorbet_replacement: String?
29
+ #| ) -> void
30
+ def apply_class_annotation(annotation, parent_node:, insert_pos:, sorbet_replacement:)
31
+ return unless sorbet_replacement # unknown annotation.
32
+
33
+ from = adjust_to_line_start(annotation.location.start_offset)
34
+ to = adjust_to_line_end(annotation.location.end_offset)
35
+
36
+ @rewriter << Source::Delete.new(from, to)
37
+
38
+ indent = " " * (parent_node.location.start_column + 2)
39
+ newline = parent_node.body.nil? ? "" : "\n"
40
+ @rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{sorbet_replacement}#{newline}")
41
+ end
42
+
43
+ # @override
44
+ #: (Spoom::RBS::Signature, type_params: Array[::RBS::AST::TypeParam]) -> void
45
+ def rewrite_type_params_signature(signature, type_params:)
46
+ from = adjust_to_line_start(signature.location.start_offset)
47
+ to = adjust_to_line_end(signature.location.end_offset)
48
+ @rewriter << Source::Delete.new(from, to)
49
+ end
50
+
51
+ # @override
52
+ #: (String type_member, parent_node: PrismTypes::anyScopeNode, insert_pos: Integer) -> void
53
+ def insert_type_member(type_member, parent_node:, insert_pos:)
54
+ indent = " " * (parent_node.location.start_column + 2)
55
+ newline = parent_node.body.nil? ? "" : "\n"
56
+ @rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{type_member}#{newline}")
57
+ end
58
+
59
+ # @override
60
+ #: (String mixin_name, into: Prism::Node, at: Integer) -> void
61
+ def extend_with(mixin_name, into:, at:)
62
+ indent = " " * (into.location.start_column + 2)
63
+ # `extend` is always followed by an annotation or `type_member`, so it always needs a
64
+ # trailing newline to separate them. Since it's never the last inserted line, that
65
+ # trailing newline can't leave a blank line before `end` (unlike the lines that follow).
66
+ @rewriter << Source::Insert.new(at, "\n#{indent}extend #{mixin_name}\n")
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,115 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/base_translator"
5
+ require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/options"
6
+
7
+ module Spoom
8
+ module Sorbet
9
+ module Translate
10
+ module RBSCommentsToSorbetSigs
11
+ class LineMatchingTranslator < BaseTranslator
12
+ private
13
+
14
+ # Comments out the discarded overload
15
+ # @override
16
+ #: (Spoom::RBS::Signature) -> void
17
+ def rewrite_discarded_overload(signature)
18
+ @rewriter << Source::Insert.new(signature.location.start_offset + 1, " RBS_DISCARDED_OVERLOAD")
19
+
20
+ signature.continuation_locations.each do |location|
21
+ @rewriter << Source::Insert.new(location.start_offset + 1, " RBS_DISCARDED_OVERLOAD:")
22
+ end
23
+ end
24
+
25
+ # @override
26
+ #: (
27
+ #| Spoom::RBS::Annotation,
28
+ #| parent_node: PrismTypes::anyScopeNode,
29
+ #| insert_pos: Integer,
30
+ #| sorbet_replacement: String?
31
+ #| ) -> void
32
+ def apply_class_annotation(annotation, parent_node:, insert_pos:, sorbet_replacement:)
33
+ case annotation.string
34
+ when /^@requires_ancestor: /
35
+ @rewriter << Source::Replace.new(
36
+ annotation.location.start_offset,
37
+ annotation.location.end_offset,
38
+ "# RBS_REWRITTEN_ANNOTATION: #{annotation.string}\n",
39
+ )
40
+ else
41
+ rewrite_annotation(annotation, is_known: !!sorbet_replacement)
42
+ end
43
+
44
+ if sorbet_replacement
45
+ @rewriter << Source::Insert.new(insert_pos, "; #{sorbet_replacement}")
46
+ end
47
+ end
48
+
49
+ # @override
50
+ #: (Spoom::RBS::Signature, type_params: Array[::RBS::AST::TypeParam]) -> void
51
+ def rewrite_type_params_signature(signature, type_params:)
52
+ # Rewrite `#: [A, B]` into `# RBS_WRITTEN_ANNOTATION: [A, B]`
53
+ @rewriter << Source::Replace.new(
54
+ signature.location.start_offset,
55
+ signature.location.start_offset + 1, # the `#:` prefix
56
+ "# RBS_WRITTEN_ANNOTATION:",
57
+ )
58
+
59
+ # Rewrite each continuation line `#| B]` into `# RBS_WRITTEN_ANNOTATION: B]`
60
+ signature.continuation_locations.each do |location|
61
+ @rewriter << Source::Replace.new(
62
+ location.start_offset,
63
+ location.start_offset + 1, # the `#|` continuation prefix
64
+ "# RBS_WRITTEN_ANNOTATION:",
65
+ )
66
+ end
67
+ end
68
+
69
+ # @override
70
+ #: (String type_member, parent_node: PrismTypes::anyScopeNode, insert_pos: Integer) -> void
71
+ def insert_type_member(type_member, parent_node:, insert_pos:)
72
+ @rewriter << Source::Insert.new(insert_pos, "; #{type_member}")
73
+ end
74
+
75
+ # @override
76
+ #: (Spoom::RBS::Annotation, is_known: bool) -> void
77
+ def rewrite_annotation(annotation, is_known:)
78
+ annotation_start = annotation.location.start_offset + 1 # skip past the `#`
79
+ text = is_known ? " RBS_REWRITTEN_ANNOTATION:" : " RBS_IGNORED_UNKNOWN_ANNOTATION:"
80
+ @rewriter << Source::Insert.new(annotation_start, text)
81
+ end
82
+
83
+ # @override
84
+ #: (String mixin_name, into: Prism::Node, at: Integer) -> void
85
+ def extend_with(mixin_name, into:, at:)
86
+ insert_pos = at
87
+
88
+ @rewriter << Source::Insert.new(insert_pos, "; extend #{mixin_name}")
89
+ end
90
+
91
+ # @override
92
+ #: (of: String, to_height_of: Spoom::RBS::Comment) -> String
93
+ def pad_out_line_count(of:, to_height_of:)
94
+ original_line_count = to_height_of.location.end_line - to_height_of.location.start_line + 1
95
+ replacement_line_count = of.count("\n")
96
+ needed_padding_lines = original_line_count - replacement_line_count
97
+ return of if needed_padding_lines == 0
98
+
99
+ if needed_padding_lines < 0
100
+ raise <<~MSG
101
+ Replacement content has more lines than the original content.
102
+ Original:
103
+ #{to_height_of.string}
104
+ Replacement content:
105
+ #{of}
106
+ MSG
107
+ end
108
+
109
+ of + "\n" * needed_padding_lines
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end