Skip to content

Commit eef924f

Browse files
authored
Merge pull request #943 from Shopify/06-08-fix-spoom-heredoc-handling
Preserve heredoc body when translating multi-line `T.let` assertions
2 parents e3c9125 + 3c764a1 commit eef924f

3 files changed

Lines changed: 147 additions & 2 deletions

File tree

lib/spoom/sorbet/translate/sorbet_assertions_to_rbs_comments.rb

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,12 @@ def maybe_translate_assertion(node)
103103
# Otherwise, replace up to the end of the node
104104
end_offset = comment_end_offset || node.location.end_offset
105105

106+
heredoc_body = heredoc_body_within_range(value, end_offset)
107+
106108
replacement = if node.name == :bind
107109
"#{rbs_annotation}#{trailing_comment}"
108110
else
109-
"#{dedent_value(node, value)} #{rbs_annotation}#{trailing_comment}"
111+
"#{dedent_value(node, value)} #{rbs_annotation}#{trailing_comment}#{heredoc_body}"
110112
end
111113

112114
@rewriter << Source::Replace.new(start_offset, end_offset - 1, replacement)
@@ -212,6 +214,53 @@ def extract_trailing_comment(node)
212214
[" #{range.pack("C*")}", end_offset]
213215
end
214216

217+
#: (Prism::Node, Integer) -> String?
218+
def heredoc_body_within_range(node, replace_end_offset)
219+
heredoc_end = heredoc_end_offsets(node)
220+
.select { |offset| offset <= replace_end_offset }
221+
.max
222+
return unless heredoc_end
223+
224+
opener_line_end = adjust_to_line_end(node.location.end_offset)
225+
body_bytes = @ruby_bytes[(opener_line_end + 1)...heredoc_end] #: as !nil
226+
body = body_bytes.pack("C*")
227+
body.chomp! if @ruby_bytes[replace_end_offset] == LINE_BREAK
228+
"\n#{body}"
229+
end
230+
231+
#: (Prism::Node) -> Array[Integer]
232+
def heredoc_end_offsets(node)
233+
offsets = [] #: Array[Integer]
234+
235+
case node
236+
when Prism::StringNode, Prism::InterpolatedStringNode, Prism::XStringNode, Prism::InterpolatedXStringNode
237+
opening = node.opening_loc
238+
closing = node.closing_loc
239+
if opening && closing && opening.start_line != closing.start_line && opening.slice.start_with?("<<")
240+
offsets << closing.end_offset
241+
end
242+
end
243+
244+
node.child_nodes.each do |child|
245+
next unless child
246+
247+
offsets.concat(heredoc_end_offsets(child))
248+
end
249+
250+
offsets
251+
end
252+
253+
#: (Prism::Node) -> bool
254+
def string_literal?(node)
255+
case node
256+
when Prism::StringNode, Prism::InterpolatedStringNode,
257+
Prism::XStringNode, Prism::InterpolatedXStringNode
258+
true
259+
else
260+
false
261+
end
262+
end
263+
215264
#: (Prism::Node, Prism::Node) -> String
216265
def dedent_value(assign, value)
217266
if value.location.start_line == assign.location.start_line
@@ -243,7 +292,7 @@ def dedent_value(assign, value)
243292
# ```
244293
indent = value.location.start_line - assign.location.start_line
245294
lines = value.slice.lines
246-
if lines.size > 1
295+
if lines.size > 1 && !string_literal?(value)
247296
lines[1..]&.each_with_index do |line, i|
248297
lines[i + 1] = line.delete_prefix(" " * indent)
249298
end

rbi/spoom.rbi

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3265,9 +3265,18 @@ class Spoom::Sorbet::Translate::SorbetAssertionsToRBSComments < ::Spoom::Sorbet:
32653265
sig { params(node: ::Prism::Node).returns(T::Boolean) }
32663266
def has_rbs_annotation?(node); end
32673267

3268+
sig { params(node: ::Prism::Node, replace_end_offset: ::Integer).returns(T.nilable(::String)) }
3269+
def heredoc_body_within_range(node, replace_end_offset); end
3270+
3271+
sig { params(node: ::Prism::Node).returns(T::Array[::Integer]) }
3272+
def heredoc_end_offsets(node); end
3273+
32683274
sig { params(node: ::Prism::Node).returns(T::Boolean) }
32693275
def maybe_translate_assertion(node); end
32703276

3277+
sig { params(node: ::Prism::Node).returns(T::Boolean) }
3278+
def string_literal?(node); end
3279+
32713280
sig { params(node: T.nilable(::Prism::Node)).returns(T::Boolean) }
32723281
def t?(node); end
32733282

test/spoom/sorbet/translate/sorbet_assertions_to_rbs_comments_test.rb

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,93 @@ def test_translate_assigns_ignore_heredoc_values
264264
RB
265265
end
266266

267+
def test_translate_assigns_multiline_tlet_with_heredoc_values
268+
rb = <<~RB
269+
MSG = T.let(
270+
<<~MSG.gsub(/[[:space:]]+/, " ").strip,
271+
Do not use foo directly. Use bar instead.
272+
See this guide: https://example.com/docs
273+
MSG
274+
String,
275+
)
276+
277+
QUERY = T.let(
278+
<<~SQL.squish.freeze,
279+
SELECT id, name
280+
FROM users
281+
WHERE active = true
282+
SQL
283+
String,
284+
)
285+
RB
286+
287+
assert_equal(<<~RB, rbi_to_rbs(rb))
288+
MSG = <<~MSG.gsub(/[[:space:]]+/, " ").strip #: String
289+
Do not use foo directly. Use bar instead.
290+
See this guide: https://example.com/docs
291+
MSG
292+
293+
QUERY = <<~SQL.squish.freeze #: String
294+
SELECT id, name
295+
FROM users
296+
WHERE active = true
297+
SQL
298+
RB
299+
end
300+
301+
def test_translate_assigns_multiline_tlet_with_multiple_heredocs
302+
rb = <<~RB
303+
both = T.let(
304+
foo(<<~A, <<~B),
305+
first
306+
A
307+
second
308+
B
309+
String,
310+
)
311+
RB
312+
313+
assert_equal(<<~RB, rbi_to_rbs(rb))
314+
both = foo(<<~A, <<~B) #: String
315+
first
316+
A
317+
second
318+
B
319+
RB
320+
end
321+
322+
def test_translate_assigns_multiline_string_literal
323+
rb = <<~RB
324+
s = T.let(
325+
"first
326+
second",
327+
String,
328+
)
329+
RB
330+
331+
assert_equal(<<~RB, rbi_to_rbs(rb))
332+
s = "first
333+
second" #: String
334+
RB
335+
end
336+
337+
def test_translate_assigns_multiline_tlet_with_backtick_heredoc
338+
rb = <<~RB
339+
x = T.let(
340+
<<~`CMD`,
341+
echo hello
342+
CMD
343+
String,
344+
)
345+
RB
346+
347+
assert_equal(<<~RB, rbi_to_rbs(rb))
348+
x = <<~`CMD` #: String
349+
echo hello
350+
CMD
351+
RB
352+
end
353+
267354
def test_translate_assigns_does_not_match_bare_strings_has_heredoc
268355
rb = <<~RB
269356
a = T.let("<<~STR", String)

0 commit comments

Comments
 (0)