Skip to content

Commit 2d05cc8

Browse files
authored
Merge pull request #940 from Shopify/Alex/line-matching
Line-matching rewriter
2 parents d02f7a3 + e7964c9 commit 2d05cc8

11 files changed

Lines changed: 1239 additions & 38 deletions

File tree

lib/spoom/rbs.rb

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,20 @@ def initialize(string, location)
6969
end
7070

7171
class Annotation < Comment; end
72-
class Signature < Comment; end
72+
73+
class Signature < Comment
74+
# Locations of the `#|` continuation comment lines that make up a multiline signature,
75+
# in addition to the `#:` line tracked by `location`.
76+
#: Array[Prism::Location]
77+
attr_reader :continuation_locations
78+
79+
#: (String, Prism::Location, ?continuation_locations: Array[Prism::Location]) -> void
80+
def initialize(string, location, continuation_locations: [])
81+
super(string, location)
82+
@continuation_locations = continuation_locations
83+
end
84+
end
85+
7386
class TypeAlias < Comment; end
7487

7588
module ExtractRBSComments
@@ -99,16 +112,18 @@ def node_rbs_comments(node)
99112
elsif string.start_with?("#: ")
100113
string = string.delete_prefix("#:").strip
101114
location = comment.location
115+
continuation_locations = [] #: Array[Prism::Location]
102116

103117
continuation_comments.reverse_each do |continuation_comment|
104118
string = "#{string}#{continuation_comment.slice.delete_prefix("#|")}"
105119
location = location.join(continuation_comment.location)
120+
continuation_locations << continuation_comment.location
106121
end
107122
continuation_comments.clear
108123

109124
next if string.start_with?("type ")
110125

111-
res.signatures.prepend(Signature.new(string, location))
126+
res.signatures.prepend(Signature.new(string, location, continuation_locations:))
112127
elsif string.start_with?("#|")
113128
continuation_comments << comment
114129
end

lib/spoom/sorbet/translate.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
require "spoom/source/rewriter"
77
require "spoom/sorbet/translate/translator"
8+
require "spoom/sorbet/translate/validator"
89
require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs"
910
require "spoom/sorbet/translate/sorbet_assertions_to_rbs_comments"
1011
require "spoom/sorbet/translate/sorbet_sigs_to_rbs_comments"

lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb

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

5555
require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/options"
5656
require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/human_readable_translator"
57+
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: 95 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ def visit_attr(node)
100100
location: "#{@file}:#{node.location.start_line}",
101101
)
102102

103+
known_annotations = nil #: Array[Spoom::RBS::Annotation]?
104+
103105
signatures.each do |signature|
104106
attr_type = ::RBS::Parser.parse_type(signature.string)
105107
sig = RBI::Sig.new
@@ -118,17 +120,21 @@ def visit_attr(node)
118120

119121
sig.return_type = @type_translator.translate(attr_type)
120122

121-
apply_member_annotations(comments.method_annotations, sig)
123+
known_annotations = apply_member_annotations(comments.method_annotations, sig)
122124

123125
@rewriter << Source::Replace.new(
124126
signature.location.start_offset,
125127
signature.location.end_offset,
126-
sig.string(max_line_length: @max_line_length),
128+
pad_out_line_count(of: sig.string(max_line_length: @max_line_length), to_height_of: signature),
127129
)
128130
rescue ::RBS::ParsingError, ::RBI::Error
129131
# Ignore signatures with errors
130132
next
131133
end
134+
135+
if known_annotations
136+
rewrite_member_annotations(comments.method_annotations, known: known_annotations)
137+
end
132138
end
133139

134140
#: (Prism::DefNode, Spoom::RBS::Comments) -> void
@@ -146,6 +152,8 @@ def rewrite_def(def_node, comments)
146152
builder.visit(def_node)
147153
rbi_node = builder.tree.nodes.first #: as RBI::Method
148154

155+
known_annotations = nil #: Array[Spoom::RBS::Annotation]?
156+
149157
signatures.each do |signature|
150158
begin
151159
method_type = ::RBS::Parser.parse_method_type(signature.string)
@@ -163,7 +171,7 @@ def rewrite_def(def_node, comments)
163171

164172
sig = translator.result
165173

166-
apply_member_annotations(comments.method_annotations, sig)
174+
known_annotations = apply_member_annotations(comments.method_annotations, sig)
167175

168176
# Sorbet runtime doesn't support `sig` on `method_added` or
169177
# `singleton_method_added`, so we always use `without_runtime` for them.
@@ -174,9 +182,13 @@ def rewrite_def(def_node, comments)
174182
@rewriter << Source::Replace.new(
175183
signature.location.start_offset,
176184
signature.location.end_offset,
177-
sig.string(max_line_length: @max_line_length),
185+
pad_out_line_count(of: sig.string(max_line_length: @max_line_length), to_height_of: signature),
178186
)
179187
end
188+
189+
if known_annotations
190+
rewrite_member_annotations(comments.method_annotations, known: known_annotations)
191+
end
180192
end
181193

182194
#: (Array[Spoom::RBS::Signature], method_name: String, location: String) -> Array[Spoom::RBS::Signature]
@@ -187,27 +199,26 @@ def apply_overloads_strategy(signatures, method_name:, location:)
187199
when :translate_all
188200
signatures
189201
when :translate_last
190-
kept = signatures.last #: as Spoom::RBS::Signature
191202
others = signatures[0...-1] #: as !nil
203+
others.each { |signature| rewrite_discarded_overload(signature) }
192204

193-
# Delete all the signatures we didn't keep
194-
others.each do |signature|
195-
from = adjust_to_line_start(signature.location.start_offset)
196-
to = adjust_to_line_end(signature.location.end_offset)
197-
@rewriter << Source::Delete.new(from, to)
198-
end
205+
kept = signatures.last #: as Spoom::RBS::Signature
199206
[kept]
200207
else # :raise
201208
raise Error, "Method `#{method_name}` at #{location} has multiple overloaded signatures"
202209
end
203210
end
204211

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+
205217
#: (PrismTypes::anyScopeNode) -> void
206218
def apply_class_annotations(node)
207219
comments = node_rbs_comments(node)
208220
return if comments.empty?
209221

210-
indent = " " * (node.location.start_column + 2)
211222
insert_pos = case node
212223
when Prism::ClassNode
213224
(node.superclass || node.constant_path).location.end_offset
@@ -217,16 +228,14 @@ def apply_class_annotations(node)
217228
node.expression.location.end_offset
218229
end
219230

220-
class_annotations = comments.class_annotations
221-
if class_annotations.any?
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?
222234
unless already_extends?(node, /^(::)?T::Helpers$/)
223-
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}extend T::Helpers\n")
235+
extend_with("T::Helpers", into: node, at: insert_pos)
224236
end
225237

226-
class_annotations.reverse_each do |annotation|
227-
from = adjust_to_line_start(annotation.location.start_offset)
228-
to = adjust_to_line_end(annotation.location.end_offset)
229-
238+
comments.annotations.reverse_each do |annotation|
230239
content = case annotation.string
231240
when "@abstract"
232241
"abstract!"
@@ -241,15 +250,13 @@ def apply_class_annotations(node)
241250
rbs_type = @type_translator.translate(srb_type)
242251
"requires_ancestor { #{rbs_type} }"
243252
else
253+
apply_class_annotation(annotation, parent_node: node, insert_pos:, sorbet_replacement: nil)
244254
next
245255
end
246256

247-
@rewriter << Source::Delete.new(from, to)
248-
249-
newline = node.body.nil? ? "" : "\n"
250-
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{content}#{newline}")
257+
apply_class_annotation(annotation, parent_node: node, insert_pos:, sorbet_replacement: content)
251258
rescue ::RBS::ParsingError, ::RBI::Error
252-
# Ignore annotations with errors
259+
apply_class_annotation(annotation, parent_node: node, insert_pos:, sorbet_replacement: nil)
253260
next
254261
end
255262
end
@@ -261,14 +268,11 @@ def apply_class_annotations(node)
261268
next unless signature.string.start_with?("[")
262269

263270
type_params = ::RBS::Parser.parse_type_params(signature.string)
271+
rewrite_type_params_signature(signature, type_params:)
264272
next if type_params.empty?
265273

266-
from = adjust_to_line_start(signature.location.start_offset)
267-
to = adjust_to_line_end(signature.location.end_offset)
268-
@rewriter << Source::Delete.new(from, to)
269-
270274
unless already_extends?(node, /^(::)?T::Generic$/)
271-
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}extend T::Generic\n")
275+
extend_with("T::Generic", into: node, at: insert_pos)
272276
end
273277

274278
type_params.each do |type_param|
@@ -293,8 +297,7 @@ def apply_class_annotations(node)
293297
end
294298
end
295299

296-
newline = node.body.nil? ? "" : "\n"
297-
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{type_member}#{newline}")
300+
insert_type_member(type_member, parent_node: node, insert_pos:)
298301
rescue ::RBS::ParsingError, ::RBI::Error
299302
# Ignore signatures with errors
300303
next
@@ -303,8 +306,31 @@ def apply_class_annotations(node)
303306
end
304307
end
305308

306-
#: (Array[Spoom::RBS::Annotation], RBI::Sig) -> void
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]
307331
def apply_member_annotations(annotations, sig)
332+
known = [] #: Array[Spoom::RBS::Annotation]
333+
308334
annotations.each do |annotation|
309335
case annotation.string
310336
when "@abstract"
@@ -323,10 +349,37 @@ def apply_member_annotations(annotations, sig)
323349
sig.is_overridable = true
324350
when "@without_runtime"
325351
sig.without_runtime = true
352+
else
353+
next
326354
end
355+
356+
known << annotation
327357
end
358+
359+
known
328360
end
329361

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+
330383
#: (PrismTypes::anyScopeNode, Regexp) -> bool
331384
def already_extends?(node, constant_regex)
332385
node.child_nodes.any? do |c|
@@ -408,12 +461,22 @@ def apply_type_aliases(comments)
408461

409462
@rewriter << Source::Delete.new(from, to)
410463
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)
411465
@rewriter << Source::Insert.new(insert_pos, content)
412466
rescue ::RBS::ParsingError, ::RBI::Error
413467
# Ignore type aliases with errors
414468
next
415469
end
416470
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
417480
end
418481
end
419482
end

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,62 @@ 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+
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
1268
end
1369
end
1470
end

0 commit comments

Comments
 (0)