Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ def visit_attr(node)

return if comments.signatures.empty?

attr_name_nodes = node.arguments&.arguments || []
return if attr_name_nodes.empty?

sig_strings_by_attr_name = attr_name_nodes.map { [] } #: Array[Array[String]]

signatures = apply_overloads_strategy(
comments.signatures,
method_name: node.message.to_s,
Expand All @@ -105,39 +110,103 @@ def visit_attr(node)

signatures.each do |signature|
attr_type = ::RBS::Parser.parse_type(signature.string)
sig = RBI::Sig.new
first_sig_string = nil #: String?

if node.message == "attr_writer"
if node.arguments&.arguments&.size != 1
raise Error, "AttrWriter must have exactly one name"
end

name = node.arguments&.arguments&.first #: as Prism::SymbolNode
sig.params << RBI::SigParam.new(
name.slice[1..-1], #: as String
@type_translator.translate(attr_type),
)
end
attr_name_nodes.each_with_index do |attr_name_node, index|
sig = build_attr_sig(node, attr_name_node, attr_type)

sig.return_type = @type_translator.translate(attr_type)
known_annotations = apply_member_annotations(comments.method_annotations, sig)

known_annotations = apply_member_annotations(comments.method_annotations, sig)
sig_string = sig.string(max_line_length: @max_line_length)
first_sig_string = sig_string if index == 0
sig_strings_by_attr_name.fetch(index) << sig_string.chomp
end

@rewriter << Source::Replace.new(
signature.location.start_offset,
signature.location.end_offset,
pad_out_line_count(of: sig.string(max_line_length: @max_line_length), to_height_of: signature),
pad_out_line_count(of: first_sig_string.to_s, to_height_of: signature),
)
rescue ::RBS::ParsingError, ::RBI::Error
# Ignore signatures with errors
next
end

if attr_name_nodes.size > 1 && sig_strings_by_attr_name.first&.any?
rewrite_multi_name_attr(node, attr_name_nodes:, sig_strings_by_attr_name:)
end

if known_annotations
rewrite_member_annotations(comments.method_annotations, known: known_annotations)
end
end

#: (Prism::CallNode, Prism::Node, untyped) -> RBI::Sig
def build_attr_sig(node, attr_name_node, attr_type)
sig = RBI::Sig.new
sig.return_type = @type_translator.translate(attr_type)

if node.message == "attr_writer"
sig.params << RBI::SigParam.new(
attr_param_name(attr_name_node),
@type_translator.translate(attr_type),
)
end

sig
end

#: (Prism::Node) -> String
def attr_param_name(node)
case node
when Prism::SymbolNode, Prism::StringNode
node.unescaped
else
raise Error, "AttrWriter names must be symbol or string literals"
end
end

#: (Prism::CallNode, Prism::Node) -> String
def attr_call_source(node, attr_name_node)
receiver = node.receiver
call = if receiver
"#{receiver.slice}#{node.call_operator}#{node.message}"
else
node.message.to_s
end

if node.opening_loc
"#{call}(#{attr_name_node.slice})"
else
"#{call} #{attr_name_node.slice}"
end
end

# @overridable
#: (
#| Prism::CallNode,
#| attr_name_nodes: Array[Prism::Node],
#| sig_strings_by_attr_name: Array[Array[String]]
#| ) -> void
def rewrite_multi_name_attr(node, attr_name_nodes:, sig_strings_by_attr_name:)
indent = " " * node.location.start_column

lines = [attr_call_source(node, attr_name_nodes.fetch(0))]

attr_name_nodes.drop(1).each_with_index do |attr_name_node, index|
sig_strings_by_attr_name.fetch(index + 1).each do |sig_string|
lines << "#{indent}#{sig_string}"
end
lines << "#{indent}#{attr_call_source(node, attr_name_node)}"
end

@rewriter << Source::Replace.new(
node.location.start_offset,
node.location.end_offset - 1,
lines.join("\n"),
)
end

#: (Prism::DefNode, Spoom::RBS::Comments) -> void
def rewrite_def(def_node, comments)
return if comments.empty?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,27 @@ def insert_type_member(type_member, parent_node:, insert_pos:)
@rewriter << Source::Insert.new(insert_pos, "; #{type_member}")
end

# @override
#: (
#| Prism::CallNode,
#| attr_name_nodes: Array[Prism::Node],
#| sig_strings_by_attr_name: Array[Array[String]]
#| ) -> void
def rewrite_multi_name_attr(node, attr_name_nodes:, sig_strings_by_attr_name:)
pieces = [attr_call_source(node, attr_name_nodes.fetch(0))]

attr_name_nodes.drop(1).each_with_index do |attr_name_node, index|
pieces.concat(sig_strings_by_attr_name.fetch(index + 1))
pieces << attr_call_source(node, attr_name_node)
end

@rewriter << Source::Replace.new(
node.location.start_offset,
node.location.end_offset - 1,
pieces.join("; "),
)
end

# @override
#: (Spoom::RBS::Annotation, is_known: bool) -> void
def rewrite_annotation(annotation, is_known:)
Expand Down
109 changes: 105 additions & 4 deletions test/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -317,21 +317,122 @@ class A
to_pretty_format_for_humans: <<~RUBY,
class A
sig { returns(Integer) }
attr_accessor :a, :b
attr_accessor :a
sig { returns(Integer) }
attr_accessor :b

sig { returns(Integer) }
attr_reader :c, :d
attr_reader :c
sig { returns(Integer) }
attr_reader :d

sig { params(e: Integer).returns(Integer) }
attr_writer :e
end
RUBY

to_line_matched_format_for_machines: :same_as_pretty_output,
to_line_matched_format_for_machines: <<~RUBY,
class A
sig { returns(Integer) }
attr_accessor :a; sig { returns(Integer) }; attr_accessor :b

sig { returns(Integer) }
attr_reader :c; sig { returns(Integer) }; attr_reader :d

sig { params(e: Integer).returns(Integer) }
attr_writer :e
end
RUBY
)
end

def test_translate_to_rbi_attr_writer_sigs_with_multiple_names
assert_rewrites_rbs(
from: <<~RUBY,
class A
#: Integer
attr_writer :a, :b
end
RUBY

to_pretty_format_for_humans: <<~RUBY,
class A
sig { params(a: Integer).returns(Integer) }
attr_writer :a
sig { params(b: Integer).returns(Integer) }
attr_writer :b
end
RUBY

to_line_matched_format_for_machines: <<~RUBY,
class A
sig { params(a: Integer).returns(Integer) }
attr_writer :a; sig { params(b: Integer).returns(Integer) }; attr_writer :b
end
RUBY
)
end

def test_translate_to_rbi_attr_sigs_with_annotations_and_multiple_names
assert_rewrites_rbs(
from: <<~RUBY,
class A
# @without_runtime
#: Integer
attr_accessor :a, :b
end
RUBY

to_pretty_format_for_humans: <<~RUBY,
class A
# @without_runtime
::T::Sig::WithoutRuntime.sig { returns(Integer) }
attr_accessor :a
::T::Sig::WithoutRuntime.sig { returns(Integer) }
attr_accessor :b
end
RUBY

to_line_matched_format_for_machines: <<~RUBY,
class A
# RBS_REWRITTEN_ANNOTATION: @without_runtime
::T::Sig::WithoutRuntime.sig { returns(Integer) }
attr_accessor :a; ::T::Sig::WithoutRuntime.sig { returns(Integer) }; attr_accessor :b
end
RUBY
)
end

def test_translate_to_rbi_parenthesized_attr_sigs_with_multiple_names
assert_rewrites_rbs(
from: <<~RUBY,
class A
#: Integer
attr_reader(:a, :b, :c)
end
RUBY

to_pretty_format_for_humans: <<~RUBY,
class A
sig { returns(Integer) }
attr_reader(:a)
sig { returns(Integer) }
attr_reader(:b)
sig { returns(Integer) }
attr_reader(:c)
end
RUBY

to_line_matched_format_for_machines: <<~RUBY,
class A
sig { returns(Integer) }
attr_reader(:a); sig { returns(Integer) }; attr_reader(:b); sig { returns(Integer) }; attr_reader(:c)
end
RUBY
)
end

def test_translate_to_rbi_attr_sigs_raises_on_writer_with_multiple_names
def test_translate_to_rbi_attr_writer_sigs_raises_on_dynamic_names
contents = <<~RB
#: Integer
attr_writer :a, b
Expand Down
Loading