Skip to content

Commit 1352c0f

Browse files
committed
Split typed multi-name attrs into per-name sigs
Rewrite typed multi-name `attr_reader`, `attr_writer`, and `attr_accessor` calls so each generated method has its own pending Sorbet `sig`. Keep line-matched output on the original attr line, and cover annotations plus parenthesized multi-name attrs in the translator tests. Assisted-By: devx/eb25d669-b4ee-4e28-9b69-129b45e1e178
1 parent 1710923 commit 1352c0f

3 files changed

Lines changed: 210 additions & 19 deletions

File tree

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

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ def visit_attr(node)
9595

9696
return if comments.signatures.empty?
9797

98+
attr_name_nodes = node.arguments&.arguments || []
99+
return if attr_name_nodes.empty?
100+
101+
sig_strings_by_attr_name = attr_name_nodes.map { [] } #: Array[Array[String]]
102+
98103
signatures = apply_overloads_strategy(
99104
comments.signatures,
100105
method_name: node.message.to_s,
@@ -105,39 +110,103 @@ def visit_attr(node)
105110

106111
signatures.each do |signature|
107112
attr_type = ::RBS::Parser.parse_type(signature.string)
108-
sig = RBI::Sig.new
113+
first_sig_string = nil #: String?
109114

110-
if node.message == "attr_writer"
111-
if node.arguments&.arguments&.size != 1
112-
raise Error, "AttrWriter must have exactly one name"
113-
end
114-
115-
name = node.arguments&.arguments&.first #: as Prism::SymbolNode
116-
sig.params << RBI::SigParam.new(
117-
name.slice[1..-1], #: as String
118-
@type_translator.translate(attr_type),
119-
)
120-
end
115+
attr_name_nodes.each_with_index do |attr_name_node, index|
116+
sig = build_attr_sig(node, attr_name_node, attr_type)
121117

122-
sig.return_type = @type_translator.translate(attr_type)
118+
known_annotations = apply_member_annotations(comments.method_annotations, sig)
123119

124-
known_annotations = apply_member_annotations(comments.method_annotations, sig)
120+
sig_string = sig.string(max_line_length: @max_line_length)
121+
first_sig_string = sig_string if index == 0
122+
sig_strings_by_attr_name.fetch(index) << sig_string.chomp
123+
end
125124

126125
@rewriter << Source::Replace.new(
127126
signature.location.start_offset,
128127
signature.location.end_offset,
129-
pad_out_line_count(of: sig.string(max_line_length: @max_line_length), to_height_of: signature),
128+
pad_out_line_count(of: first_sig_string.to_s, to_height_of: signature),
130129
)
131130
rescue ::RBS::ParsingError, ::RBI::Error
132131
# Ignore signatures with errors
133132
next
134133
end
135134

135+
if attr_name_nodes.size > 1 && sig_strings_by_attr_name.first&.any?
136+
rewrite_multi_name_attr(node, attr_name_nodes:, sig_strings_by_attr_name:)
137+
end
138+
136139
if known_annotations
137140
rewrite_member_annotations(comments.method_annotations, known: known_annotations)
138141
end
139142
end
140143

144+
#: (Prism::CallNode, Prism::Node, untyped) -> RBI::Sig
145+
def build_attr_sig(node, attr_name_node, attr_type)
146+
sig = RBI::Sig.new
147+
sig.return_type = @type_translator.translate(attr_type)
148+
149+
if node.message == "attr_writer"
150+
sig.params << RBI::SigParam.new(
151+
attr_param_name(attr_name_node),
152+
@type_translator.translate(attr_type),
153+
)
154+
end
155+
156+
sig
157+
end
158+
159+
#: (Prism::Node) -> String
160+
def attr_param_name(node)
161+
case node
162+
when Prism::SymbolNode, Prism::StringNode
163+
node.unescaped
164+
else
165+
raise Error, "AttrWriter names must be symbol or string literals"
166+
end
167+
end
168+
169+
#: (Prism::CallNode, Prism::Node) -> String
170+
def attr_call_source(node, attr_name_node)
171+
receiver = node.receiver
172+
call = if receiver
173+
"#{receiver.slice}#{node.call_operator}#{node.message}"
174+
else
175+
node.message.to_s
176+
end
177+
178+
if node.opening_loc
179+
"#{call}(#{attr_name_node.slice})"
180+
else
181+
"#{call} #{attr_name_node.slice}"
182+
end
183+
end
184+
185+
# @overridable
186+
#: (
187+
#| Prism::CallNode,
188+
#| attr_name_nodes: Array[Prism::Node],
189+
#| sig_strings_by_attr_name: Array[Array[String]]
190+
#| ) -> void
191+
def rewrite_multi_name_attr(node, attr_name_nodes:, sig_strings_by_attr_name:)
192+
indent = " " * node.location.start_column
193+
194+
lines = [attr_call_source(node, attr_name_nodes.fetch(0))]
195+
196+
attr_name_nodes.drop(1).each_with_index do |attr_name_node, index|
197+
sig_strings_by_attr_name.fetch(index + 1).each do |sig_string|
198+
lines << "#{indent}#{sig_string}"
199+
end
200+
lines << "#{indent}#{attr_call_source(node, attr_name_node)}"
201+
end
202+
203+
@rewriter << Source::Replace.new(
204+
node.location.start_offset,
205+
node.location.end_offset - 1,
206+
lines.join("\n"),
207+
)
208+
end
209+
141210
#: (Prism::DefNode, Spoom::RBS::Comments) -> void
142211
def rewrite_def(def_node, comments)
143212
return if comments.empty?

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,27 @@ def insert_type_member(type_member, parent_node:, insert_pos:)
7272
@rewriter << Source::Insert.new(insert_pos, "; #{type_member}")
7373
end
7474

75+
# @override
76+
#: (
77+
#| Prism::CallNode,
78+
#| attr_name_nodes: Array[Prism::Node],
79+
#| sig_strings_by_attr_name: Array[Array[String]]
80+
#| ) -> void
81+
def rewrite_multi_name_attr(node, attr_name_nodes:, sig_strings_by_attr_name:)
82+
pieces = [attr_call_source(node, attr_name_nodes.fetch(0))]
83+
84+
attr_name_nodes.drop(1).each_with_index do |attr_name_node, index|
85+
pieces.concat(sig_strings_by_attr_name.fetch(index + 1))
86+
pieces << attr_call_source(node, attr_name_node)
87+
end
88+
89+
@rewriter << Source::Replace.new(
90+
node.location.start_offset,
91+
node.location.end_offset - 1,
92+
pieces.join("; "),
93+
)
94+
end
95+
7596
# @override
7697
#: (Spoom::RBS::Annotation, is_known: bool) -> void
7798
def rewrite_annotation(annotation, is_known:)

test/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs_test.rb

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -317,21 +317,122 @@ class A
317317
to_pretty_format_for_humans: <<~RUBY,
318318
class A
319319
sig { returns(Integer) }
320-
attr_accessor :a, :b
320+
attr_accessor :a
321+
sig { returns(Integer) }
322+
attr_accessor :b
321323
322324
sig { returns(Integer) }
323-
attr_reader :c, :d
325+
attr_reader :c
326+
sig { returns(Integer) }
327+
attr_reader :d
324328
325329
sig { params(e: Integer).returns(Integer) }
326330
attr_writer :e
327331
end
328332
RUBY
329333

330-
to_line_matched_format_for_machines: :same_as_pretty_output,
334+
to_line_matched_format_for_machines: <<~RUBY,
335+
class A
336+
sig { returns(Integer) }
337+
attr_accessor :a; sig { returns(Integer) }; attr_accessor :b
338+
339+
sig { returns(Integer) }
340+
attr_reader :c; sig { returns(Integer) }; attr_reader :d
341+
342+
sig { params(e: Integer).returns(Integer) }
343+
attr_writer :e
344+
end
345+
RUBY
346+
)
347+
end
348+
349+
def test_translate_to_rbi_attr_writer_sigs_with_multiple_names
350+
assert_rewrites_rbs(
351+
from: <<~RUBY,
352+
class A
353+
#: Integer
354+
attr_writer :a, :b
355+
end
356+
RUBY
357+
358+
to_pretty_format_for_humans: <<~RUBY,
359+
class A
360+
sig { params(a: Integer).returns(Integer) }
361+
attr_writer :a
362+
sig { params(b: Integer).returns(Integer) }
363+
attr_writer :b
364+
end
365+
RUBY
366+
367+
to_line_matched_format_for_machines: <<~RUBY,
368+
class A
369+
sig { params(a: Integer).returns(Integer) }
370+
attr_writer :a; sig { params(b: Integer).returns(Integer) }; attr_writer :b
371+
end
372+
RUBY
373+
)
374+
end
375+
376+
def test_translate_to_rbi_attr_sigs_with_annotations_and_multiple_names
377+
assert_rewrites_rbs(
378+
from: <<~RUBY,
379+
class A
380+
# @without_runtime
381+
#: Integer
382+
attr_accessor :a, :b
383+
end
384+
RUBY
385+
386+
to_pretty_format_for_humans: <<~RUBY,
387+
class A
388+
# @without_runtime
389+
::T::Sig::WithoutRuntime.sig { returns(Integer) }
390+
attr_accessor :a
391+
::T::Sig::WithoutRuntime.sig { returns(Integer) }
392+
attr_accessor :b
393+
end
394+
RUBY
395+
396+
to_line_matched_format_for_machines: <<~RUBY,
397+
class A
398+
# RBS_REWRITTEN_ANNOTATION: @without_runtime
399+
::T::Sig::WithoutRuntime.sig { returns(Integer) }
400+
attr_accessor :a; ::T::Sig::WithoutRuntime.sig { returns(Integer) }; attr_accessor :b
401+
end
402+
RUBY
403+
)
404+
end
405+
406+
def test_translate_to_rbi_parenthesized_attr_sigs_with_multiple_names
407+
assert_rewrites_rbs(
408+
from: <<~RUBY,
409+
class A
410+
#: Integer
411+
attr_reader(:a, :b, :c)
412+
end
413+
RUBY
414+
415+
to_pretty_format_for_humans: <<~RUBY,
416+
class A
417+
sig { returns(Integer) }
418+
attr_reader(:a)
419+
sig { returns(Integer) }
420+
attr_reader(:b)
421+
sig { returns(Integer) }
422+
attr_reader(:c)
423+
end
424+
RUBY
425+
426+
to_line_matched_format_for_machines: <<~RUBY,
427+
class A
428+
sig { returns(Integer) }
429+
attr_reader(:a); sig { returns(Integer) }; attr_reader(:b); sig { returns(Integer) }; attr_reader(:c)
430+
end
431+
RUBY
331432
)
332433
end
333434

334-
def test_translate_to_rbi_attr_sigs_raises_on_writer_with_multiple_names
435+
def test_translate_to_rbi_attr_writer_sigs_raises_on_dynamic_names
335436
contents = <<~RB
336437
#: Integer
337438
attr_writer :a, b

0 commit comments

Comments
 (0)