Skip to content

Commit 0bdd70e

Browse files
committed
Add LineMatchingTranslator
1 parent 7b33e02 commit 0bdd70e

7 files changed

Lines changed: 382 additions & 32 deletions

File tree

lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,4 @@ def rewrite_if_needed(
5555

5656
require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/options"
5757
require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/human_readable_translator"
58+
require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/line_matching_translator"

lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/base_translator.rb

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def visit_attr(node)
129129
@rewriter << Source::Replace.new(
130130
signature.location.start_offset,
131131
signature.location.end_offset,
132-
sig.string(max_line_length: @max_line_length),
132+
pad_out_line_count(of: sig.string(max_line_length: @max_line_length), to_height_of: signature),
133133
)
134134
rescue ::RBS::ParsingError, ::RBI::Error
135135
# Ignore signatures with errors
@@ -180,7 +180,7 @@ def rewrite_def(def_node, comments)
180180
@rewriter << Source::Replace.new(
181181
signature.location.start_offset,
182182
signature.location.end_offset,
183-
sig.string(max_line_length: @max_line_length),
183+
pad_out_line_count(of: sig.string(max_line_length: @max_line_length), to_height_of: signature),
184184
)
185185
end
186186
end
@@ -193,27 +193,26 @@ def apply_overloads_strategy(signatures, method_name:, location:)
193193
when :translate_all
194194
signatures
195195
when :translate_last
196-
kept = signatures.last #: as Spoom::RBS::Signature
197196
others = signatures[0...-1] #: as !nil
197+
others.each { |signature| rewrite_discarded_overload(signature) }
198198

199-
# Delete all the signatures we didn't keep
200-
others.each do |signature|
201-
from = adjust_to_line_start(signature.location.start_offset)
202-
to = adjust_to_line_end(signature.location.end_offset)
203-
@rewriter << Source::Delete.new(from, to)
204-
end
199+
kept = signatures.last #: as Spoom::RBS::Signature
205200
[kept]
206201
else # :raise
207202
raise Error, "Method `#{method_name}` at #{location} has multiple overloaded signatures"
208203
end
209204
end
210205

206+
# Called for every overloaded method sig that we discard because it wasn't the last one.
207+
# @abstract
208+
#: (Spoom::RBS::Signature) -> void
209+
def rewrite_discarded_overload(signature) = raise
210+
211211
#: (PrismTypes::anyScopeNode) -> void
212212
def apply_class_annotations(node)
213213
comments = node_rbs_comments(node)
214214
return if comments.empty?
215215

216-
indent = " " * (node.location.start_column + 2)
217216
insert_pos = case node
218217
when Prism::ClassNode
219218
(node.superclass || node.constant_path).location.end_offset
@@ -223,16 +222,14 @@ def apply_class_annotations(node)
223222
node.expression.location.end_offset
224223
end
225224

226-
class_annotations = comments.class_annotations
227-
if class_annotations.any?
225+
# Only translate (and `extend T::Helpers`) when there's at least one *known* class
226+
# annotation. A node with only unknown annotations (e.g. `@private`) is left untouched.
227+
if comments.class_annotations.any?
228228
unless already_extends?(node, /^(::)?T::Helpers$/)
229-
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}extend T::Helpers\n")
229+
extend_with("T::Helpers", into: node, at: insert_pos)
230230
end
231231

232-
class_annotations.reverse_each do |annotation|
233-
from = adjust_to_line_start(annotation.location.start_offset)
234-
to = adjust_to_line_end(annotation.location.end_offset)
235-
232+
comments.annotations.reverse_each do |annotation|
236233
content = case annotation.string
237234
when "@abstract"
238235
"abstract!"
@@ -247,15 +244,13 @@ def apply_class_annotations(node)
247244
rbs_type = @type_translator.translate(srb_type)
248245
"requires_ancestor { #{rbs_type} }"
249246
else
247+
apply_class_annotation(annotation, parent_node: node, insert_pos:, sorbet_replacement: nil)
250248
next
251249
end
252250

253-
@rewriter << Source::Delete.new(from, to)
254-
255-
newline = node.body.nil? ? "" : "\n"
256-
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{content}#{newline}")
251+
apply_class_annotation(annotation, parent_node: node, insert_pos:, sorbet_replacement: content)
257252
rescue ::RBS::ParsingError, ::RBI::Error
258-
# Ignore annotations with errors
253+
apply_class_annotation(annotation, parent_node: node, insert_pos:, sorbet_replacement: nil)
259254
next
260255
end
261256
end
@@ -267,14 +262,11 @@ def apply_class_annotations(node)
267262
next unless signature.string.start_with?("[")
268263

269264
type_params = ::RBS::Parser.parse_type_params(signature.string)
265+
rewrite_type_params_signature(signature, type_params:)
270266
next if type_params.empty?
271267

272-
from = adjust_to_line_start(signature.location.start_offset)
273-
to = adjust_to_line_end(signature.location.end_offset)
274-
@rewriter << Source::Delete.new(from, to)
275-
276268
unless already_extends?(node, /^(::)?T::Generic$/)
277-
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}extend T::Generic\n")
269+
extend_with("T::Generic", into: node, at: insert_pos)
278270
end
279271

280272
type_params.each do |type_param|
@@ -299,8 +291,7 @@ def apply_class_annotations(node)
299291
end
300292
end
301293

302-
newline = node.body.nil? ? "" : "\n"
303-
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{type_member}#{newline}")
294+
insert_type_member(type_member, parent_node: node, insert_pos:)
304295
rescue ::RBS::ParsingError, ::RBI::Error
305296
# Ignore signatures with errors
306297
next
@@ -309,6 +300,27 @@ def apply_class_annotations(node)
309300
end
310301
end
311302

303+
# @param is_known: true if this is an RBS annotation that we recognize
304+
# false for some other `@`-prefixed thing, like a documentation `@param` tag.
305+
# @abstract
306+
#: (
307+
#| Spoom::RBS::Annotation,
308+
#| parent_node: PrismTypes::anyScopeNode,
309+
#| insert_pos: Integer,
310+
#| sorbet_replacement: String?
311+
#| ) -> void
312+
def apply_class_annotation(annotation, parent_node:, insert_pos:, sorbet_replacement:) = raise
313+
314+
# Rewrites the `#: [...]` type params comment (e.g. delete it, or mark it as translated).
315+
# @abstract
316+
#: (Spoom::RBS::Signature, type_params: Array[::RBS::AST::TypeParam]) -> void
317+
def rewrite_type_params_signature(signature, type_params:) = raise
318+
319+
# Inserts a single `type_member` declaration into the class/module body.
320+
# @abstract
321+
#: (String type_member, parent_node: PrismTypes::anyScopeNode, insert_pos: Integer) -> void
322+
def insert_type_member(type_member, parent_node:, insert_pos:) = raise
323+
312324
#: (Array[Spoom::RBS::Annotation], RBI::Sig) -> void
313325
def apply_member_annotations(annotations, sig)
314326
annotations.each do |annotation|
@@ -329,10 +341,25 @@ def apply_member_annotations(annotations, sig)
329341
sig.is_overridable = true
330342
when "@without_runtime"
331343
sig.without_runtime = true
344+
else
345+
rewrite_annotation(annotation, is_known: false)
346+
next
332347
end
348+
349+
rewrite_annotation(annotation, is_known: true)
333350
end
334351
end
335352

353+
# @param is_known: true if this is an RBS annotation that we recognize
354+
# false for some other `@`-prefixed thing, like a documentation `@param` tag.
355+
# @overridable
356+
#: (Spoom::RBS::Annotation, is_known: bool) -> void
357+
def rewrite_annotation(annotation, is_known:) = nil # no-op
358+
359+
# @abstract
360+
#: (String mixin_name, into: PrismTypes::anyScopeNode, at: Integer) -> void
361+
def extend_with(mixin_name, into:, at:) = raise
362+
336363
#: (PrismTypes::anyScopeNode, Regexp) -> bool
337364
def already_extends?(node, constant_regex)
338365
node.child_nodes.any? do |c|
@@ -414,12 +441,22 @@ def apply_type_aliases(comments)
414441

415442
@rewriter << Source::Delete.new(from, to)
416443
content = "#{indent}#{alias_name} = T.type_alias { #{sorbet_type.to_rbi} }\n"
444+
content = pad_out_line_count(of: content, to_height_of: type_alias)
417445
@rewriter << Source::Insert.new(insert_pos, content)
418446
rescue ::RBS::ParsingError, ::RBI::Error
419447
# Ignore type aliases with errors
420448
next
421449
end
422450
end
451+
452+
# @overridable
453+
#: (of: String, to_height_of: Spoom::RBS::Comment) -> String
454+
def pad_out_line_count(of:, to_height_of:)
455+
replacement = of
456+
457+
# no-op implementation
458+
replacement
459+
end
423460
end
424461
end
425462
end

lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/human_readable_translator.rb

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,66 @@ module Sorbet
99
module Translate
1010
module RBSCommentsToSorbetSigs
1111
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+
content = sorbet_replacement
40+
newline = parent_node.body.nil? ? "" : "\n"
41+
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{content}#{newline}")
42+
end
43+
44+
# @override
45+
#: (Spoom::RBS::Signature, type_params: Array[::RBS::AST::TypeParam]) -> void
46+
def rewrite_type_params_signature(signature, type_params:)
47+
from = adjust_to_line_start(signature.location.start_offset)
48+
to = adjust_to_line_end(signature.location.end_offset)
49+
@rewriter << Source::Delete.new(from, to)
50+
end
51+
52+
# @override
53+
#: (String type_member, parent_node: PrismTypes::anyScopeNode, insert_pos: Integer) -> void
54+
def insert_type_member(type_member, parent_node:, insert_pos:)
55+
indent = " " * (parent_node.location.start_column + 2)
56+
newline = parent_node.body.nil? ? "" : "\n"
57+
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{type_member}#{newline}")
58+
end
59+
60+
# @override
61+
#: (String mixin_name, into: Prism::Node, at: Integer) -> void
62+
def extend_with(mixin_name, into:, at:)
63+
parent_node = into
64+
insert_pos = at
65+
66+
indent = " " * (parent_node.location.start_column + 2)
67+
# `extend` is always followed by an annotation or `type_member`, so it always needs a
68+
# trailing newline to separate them. Since it's never the last inserted line, that
69+
# trailing newline can't leave a blank line before `end` (unlike the lines that follow).
70+
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}extend #{mixin_name}\n")
71+
end
1272
end
1373
end
1474
end
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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+
end
20+
21+
# @override
22+
#: (
23+
#| Spoom::RBS::Annotation,
24+
#| parent_node: PrismTypes::anyScopeNode,
25+
#| insert_pos: Integer,
26+
#| sorbet_replacement: String?
27+
#| ) -> void
28+
def apply_class_annotation(annotation, parent_node:, insert_pos:, sorbet_replacement:)
29+
case annotation.string
30+
when /^@requires_ancestor: /
31+
@rewriter << Source::Replace.new(
32+
annotation.location.start_offset,
33+
annotation.location.end_offset,
34+
"# RBS_REWRITTEN_ANNOTATION: #{annotation.string}\n",
35+
)
36+
else
37+
rewrite_annotation(annotation, is_known: !!sorbet_replacement)
38+
end
39+
40+
if sorbet_replacement
41+
@rewriter << Source::Insert.new(insert_pos, "; #{sorbet_replacement}")
42+
end
43+
end
44+
45+
# @override
46+
#: (Spoom::RBS::Annotation, is_known: bool) -> void
47+
def rewrite_annotation(annotation, is_known:)
48+
annotation_start = annotation.location.start_offset + 1 # skip past the `#`
49+
text = is_known ? " RBS_REWRITTEN_ANNOTATION:" : " RBS_IGNORED_UNKNOWN_ANNOTATION:"
50+
@rewriter << Source::Insert.new(annotation_start, text)
51+
end
52+
53+
# @override
54+
#: (Spoom::RBS::Signature, type_params: Array[::RBS::AST::TypeParam]) -> void
55+
def rewrite_type_params_signature(signature, type_params:)
56+
# Rewrite `#: [A, B]` into `# RBS_WRITTEN_ANNOTATION: [A, B]`
57+
@rewriter << Source::Replace.new(
58+
signature.location.start_offset,
59+
signature.location.start_offset + 1, # the `#:` prefix
60+
"# RBS_WRITTEN_ANNOTATION:",
61+
)
62+
end
63+
64+
# @override
65+
#: (String type_member, parent_node: PrismTypes::anyScopeNode, insert_pos: Integer) -> void
66+
def insert_type_member(type_member, parent_node:, insert_pos:)
67+
@rewriter << Source::Insert.new(insert_pos, "; #{type_member}")
68+
end
69+
70+
# @override
71+
#: (String mixin_name, into: Prism::Node, at: Integer) -> void
72+
def extend_with(mixin_name, into:, at:)
73+
insert_pos = at
74+
75+
@rewriter << Source::Insert.new(insert_pos, "; extend #{mixin_name}")
76+
end
77+
78+
# @override
79+
#: (of: String, to_height_of: Spoom::RBS::Comment) -> String
80+
def pad_out_line_count(of:, to_height_of:)
81+
# #: (Spoom::RBS::Comment, String) -> String
82+
# def pad_out_line_count(original_rbs_comment, replacement)
83+
replacement = of
84+
original = to_height_of
85+
86+
original_line_count = original.location.end_line - original.location.start_line + 1
87+
replacement_line_count = replacement.count("\n")
88+
89+
needed_padding_lines = original_line_count - replacement_line_count
90+
91+
return replacement if needed_padding_lines == 0
92+
93+
if needed_padding_lines < 0
94+
raise <<~MSG
95+
Replacement content has more lines than the original content.
96+
Original:
97+
#{original.string}
98+
Replacement content:
99+
#{replacement}
100+
MSG
101+
end
102+
103+
replacement + "\n" * needed_padding_lines
104+
end
105+
end
106+
end
107+
end
108+
end
109+
end

lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/options.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ class << self
3131
end
3232
end
3333

34+
class LineMatchedRBIFormat < BaseRBIFormat # TODO: move to RBI gem
35+
@default = new #: LineMatchedRBIFormat
36+
class << self
37+
#: LineMatchedRBIFormat
38+
attr_reader :default
39+
end
40+
end
41+
3442
class Options
3543
#: Symbol
3644
attr_reader :overloads_strategy

0 commit comments

Comments
 (0)