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 @@ -92,51 +92,154 @@ def visit_call_node(node)
#: (Prism::CallNode) -> void
def visit_attr(node)
comments = node_rbs_comments(node)
return if comments.empty?
signatures = comments.signatures
return if signatures.empty?

return if comments.signatures.empty?
if signatures.size != 1
raise Error, "RBS signatures for attributes do not support overloads"
end

signatures = apply_overloads_strategy(
comments.signatures,
method_name: node.message.to_s,
location: "#{@file}:#{node.location.start_line}",
)
signature = signatures.first
return unless signature

known_annotations = nil #: Array[Spoom::RBS::Annotation]?
attr_name_nodes = node.arguments&.arguments || []

signatures.each do |signature|
attr_type = ::RBS::Parser.parse_type(signature.string)
sig = RBI::Sig.new
if node.message == "attr_writer" && attr_name_nodes.size != 1
raise Error, "RBS signatures for attr_writer require exactly one argument"
end

if node.message == "attr_writer"
if node.arguments&.arguments&.size != 1
raise Error, "AttrWriter must have exactly one name"
end
first_attr_name_node, *additional_attr_name_nodes = attr_name_nodes
return unless first_attr_name_node

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
rbs_type = ::RBS::Parser.parse_type(signature.string)
attr_type = @type_translator.translate(rbs_type)
first_attr_sig = build_attr_sig(node, first_attr_name_node, attr_type)
known_annotations = apply_member_annotations(comments.method_annotations, first_attr_sig)

sig.return_type = @type_translator.translate(attr_type)
# Format the signature with the correct indentation.
indent = node.location.start_column
first_attr_sig_source = format_attr_sig(first_attr_sig, indent:)
first_attr_sig_source.concat("\n")

known_annotations = apply_member_annotations(comments.method_annotations, sig)
# Replace the RBS comment in place to preserve the source layout.
@rewriter << Source::Replace.new(
signature.location.start_offset,
signature.location.end_offset,
pad_out_line_count(of: first_attr_sig_source, to_height_of: signature),
)

@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),
# Split multi-name calls so each additional attribute gets its own signature.
if additional_attr_name_nodes.any?
rewrite_multi_name_attr(
node,
first_attr_name_node,
additional_attr_name_nodes,
attr_type,
annotations: comments.method_annotations,
)
rescue ::RBS::ParsingError, ::RBI::Error
# Ignore signatures with errors
next
end

if known_annotations
rewrite_member_annotations(comments.method_annotations, known: known_annotations)
# Handle member annotations like `# @without_runtime`.
rewrite_member_annotations(comments.method_annotations, known: known_annotations)
rescue ::RBS::ParsingError, ::RBI::Error
# Ignore signatures with errors
end

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

if node.message == "attr_writer"
name = attr_name_node #: as Prism::SymbolNode
sig.params << RBI::SigParam.new(
name.slice[1..-1], #: as String
attr_type,
)
end

sig
end

#: (RBI::Sig, indent: Integer) -> String
def format_attr_sig(sig, indent:)
sig.string(indent:, max_line_length: @max_line_length)
.delete_prefix(" " * indent)
.chomp
end

# `attr_reader(:a, :b)` becomes a single attr call: `attr_reader(:a)`.
#: (Prism::CallNode, Prism::Node) -> String
def single_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

# Split a multi-name attr into one call per name. The first signature is already written above the call,
# so only the remaining names need signatures here.
#: (
#| Prism::CallNode,
#| Prism::Node,
#| Array[Prism::Node],
#| RBI::Type,
#| annotations: Array[Spoom::RBS::Annotation]
#| ) -> void
def rewrite_multi_name_attr(node, first_attr_name_node, additional_attr_name_nodes, attr_type, annotations:)
indent = node.location.start_column
# Replacing the original call removes every attr, so include the first attr in its replacement.
located_statements = [
[single_attr_call_source(node, first_attr_name_node), first_attr_name_node.location.start_line],
]

additional_attr_name_nodes.each do |attr_name_node|
sig = build_attr_sig(node, attr_name_node, attr_type)
apply_member_annotations(annotations, sig)
located_statements << [format_attr_sig(sig, indent:), attr_name_node.location.start_line]
located_statements << [single_attr_call_source(node, attr_name_node), attr_name_node.location.start_line]
end

# Replacing the original call removes its trailing comments, so keep them with the final attr call.
last_attr_name_node = additional_attr_name_nodes.last #: as Prism::Node
last_attr_source = located_statements.last.first
indented_newline = "\n#{" " * indent}"
@comments.each do |comment|
next unless comment.location.start_offset > last_attr_name_node.location.end_offset
next unless comment.location.start_offset < node.location.end_offset

separator = if comment.location.start_line == last_attr_name_node.location.end_line
" "
else
indented_newline
end
last_attr_source.concat(separator).concat(comment.slice)
end

replace_multi_name_attr(node, located_statements:)
end

# Replace the original multi-name call with the generated single-name calls and signatures, one per line.
# @overridable
#: (Prism::CallNode, located_statements: Array[[String, Integer]]) -> void
def replace_multi_name_attr(node, located_statements:)
indent = " " * node.location.start_column
statements = located_statements.map { |source, _line| source }

@rewriter << Source::Replace.new(
node.location.start_offset,
# Prism ends are exclusive; Source::Replace ends are inclusive.
node.location.end_offset - 1,
statements.join("\n#{indent}"),
)
end

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

# Keep each attr on its original line so Sorbet errors point to the correct source.
# @override
#: (Prism::CallNode, located_statements: Array[[String, Integer]]) -> void
def replace_multi_name_attr(node, located_statements:)
replacement = String.new
current_line = node.location.start_line
indent = " " * node.location.start_column

located_statements.each do |source, source_line|
line_gap = source_line - current_line
if line_gap.positive?
replacement.concat("\n" * line_gap).concat(indent)
elsif !replacement.empty?
replacement.concat("; ")
end

replacement.concat(source)
current_line = source_line
end

remaining_lines = node.location.end_line - node.location.start_line - replacement.count("\n")
replacement.concat("\n" * remaining_lines)

# Remove the indentation when moving the first attr off the original call line.
start_offset = node.location.start_offset
if located_statements.first&.last != node.location.start_line
start_offset = adjust_to_line_start(start_offset)
end

@rewriter << Source::Replace.new(
start_offset,
# Prism ends are exclusive; Source::Replace ends are inclusive.
node.location.end_offset - 1,
replacement,
)
end

# @override
#: (Spoom::RBS::Annotation, is_known: bool) -> void
def rewrite_annotation(annotation, is_known:)
Expand Down
46 changes: 46 additions & 0 deletions rbi/spoom.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ class Spoom::Coverage::Cards::Card < ::Spoom::Coverage::Template
sig { params(template: ::String, title: T.nilable(::String), body: T.nilable(::String)).void }
def initialize(template: T.unsafe(nil), title: T.unsafe(nil), body: T.unsafe(nil)); end

sig { returns(T.nilable(::String)) }
def body; end

sig { returns(T.nilable(::String)) }
Expand Down Expand Up @@ -2354,7 +2355,10 @@ class Spoom::Location
sig { override.params(other: ::BasicObject).returns(T.nilable(::Integer)) }
def <=>(other); end

sig { returns(T.nilable(::Integer)) }
def end_column; end

sig { returns(T.nilable(::Integer)) }
def end_line; end

sig { returns(::String) }
Expand All @@ -2363,6 +2367,7 @@ class Spoom::Location
sig { params(other: ::Spoom::Location).returns(T::Boolean) }
def include?(other); end

sig { returns(T.nilable(::Integer)) }
def start_column; end

sig { returns(T.nilable(::Integer)) }
Expand Down Expand Up @@ -2843,16 +2848,19 @@ class Spoom::Poset::Element
sig { returns(T::Array[E]) }
def descendants; end

sig { returns(T::Set[Spoom::Poset::Element[E]]) }
def dfroms; end

sig { returns(T::Set[Spoom::Poset::Element[E]]) }
def dtos; end

sig { returns(T::Set[Spoom::Poset::Element[E]]) }
def froms; end

sig { returns(T::Array[E]) }
def parents; end

sig { returns(T::Set[Spoom::Poset::Element[E]]) }
def tos; end

sig { returns(E) }
Expand Down Expand Up @@ -2966,9 +2974,14 @@ class Spoom::Sorbet::Config
sig { void }
def initialize; end

sig { returns(T::Array[::String]) }
def allowed_extensions; end

def allowed_extensions=(_arg0); end

sig { returns(T::Array[::String]) }
def ignore; end

def ignore=(_arg0); end

sig { returns(T::Boolean) }
Expand All @@ -2983,7 +2996,10 @@ class Spoom::Sorbet::Config
def paths; end

def paths=(_arg0); end

sig { returns(T::Array[::String]) }
def typed_overrides; end

def typed_overrides=(_arg0); end

private
Expand Down Expand Up @@ -3050,6 +3066,7 @@ class Spoom::Sorbet::Errors::Error
sig { params(other: T.untyped).returns(::Integer) }
def <=>(other); end

sig { returns(T.nilable(::Integer)) }
def code; end

sig { returns(T.nilable(::String)) }
Expand All @@ -3061,6 +3078,7 @@ class Spoom::Sorbet::Errors::Error
sig { returns(T.nilable(::Integer)) }
def line; end

sig { returns(T.nilable(::String)) }
def message; end

sig { returns(T::Array[::String]) }
Expand Down Expand Up @@ -3409,6 +3427,9 @@ class Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::BaseTranslator < ::Spoo
sig { params(comments: T::Array[::Prism::Comment]).void }
def apply_type_aliases(comments); end

sig { params(node: ::Prism::CallNode, attr_name_node: ::Prism::Node, attr_type: ::RBI::Type).returns(::RBI::Sig) }
def build_attr_sig(node, attr_name_node, attr_type); end

sig { params(comments: T::Array[::Prism::Comment]).returns(T::Array[::Spoom::RBS::TypeAlias]) }
def collect_type_aliases(comments); end

Expand All @@ -3422,6 +3443,9 @@ class Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::BaseTranslator < ::Spoo
end
def extend_with(mixin_name, into:, at:); end

sig { params(sig: ::RBI::Sig, indent: ::Integer).returns(::String) }
def format_attr_sig(sig, indent:); end

sig do
abstract
.params(
Expand All @@ -3435,6 +3459,9 @@ class Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::BaseTranslator < ::Spoo
sig { overridable.params(of: ::String, to_height_of: ::Spoom::RBS::Comment).returns(::String) }
def pad_out_line_count(of:, to_height_of:); end

sig { overridable.params(node: ::Prism::CallNode, located_statements: T::Array[[::String, ::Integer]]).void }
def replace_multi_name_attr(node, located_statements:); end

sig { overridable.params(annotation: ::Spoom::RBS::Annotation, is_known: T::Boolean).void }
def rewrite_annotation(annotation, is_known:); end

Expand All @@ -3447,9 +3474,23 @@ class Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::BaseTranslator < ::Spoo
sig { params(annotations: T::Array[::Spoom::RBS::Annotation], known: T::Array[::Spoom::RBS::Annotation]).void }
def rewrite_member_annotations(annotations, known:); end

sig do
params(
node: ::Prism::CallNode,
first_attr_name_node: ::Prism::Node,
additional_attr_name_nodes: T::Array[::Prism::Node],
attr_type: ::RBI::Type,
annotations: T::Array[::Spoom::RBS::Annotation]
).void
end
def rewrite_multi_name_attr(node, first_attr_name_node, additional_attr_name_nodes, attr_type, annotations:); end

sig { abstract.params(signature: ::Spoom::RBS::Signature, type_params: T::Array[::RBS::AST::TypeParam]).void }
def rewrite_type_params_signature(signature, type_params:); end

sig { params(node: ::Prism::CallNode, attr_name_node: ::Prism::Node).returns(::String) }
def single_attr_call_source(node, attr_name_node); end

sig { params(node: ::Prism::CallNode).void }
def visit_attr(node); end
end
Expand Down Expand Up @@ -3538,6 +3579,9 @@ class Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::LineMatchingTranslator
sig { override.params(of: ::String, to_height_of: ::Spoom::RBS::Comment).returns(::String) }
def pad_out_line_count(of:, to_height_of:); end

sig { override.params(node: ::Prism::CallNode, located_statements: T::Array[[::String, ::Integer]]).void }
def replace_multi_name_attr(node, located_statements:); end

sig { override.params(annotation: ::Spoom::RBS::Annotation, is_known: T::Boolean).void }
def rewrite_annotation(annotation, is_known:); end

Expand Down Expand Up @@ -3815,6 +3859,7 @@ class Spoom::Source::Delete < ::Spoom::Source::Edit
sig { override.returns([::Integer, ::Integer]) }
def range; end

sig { returns(::Integer) }
def to; end

sig { override.returns(::String) }
Expand Down Expand Up @@ -3869,6 +3914,7 @@ class Spoom::Source::Replace < ::Spoom::Source::Edit
sig { returns(::String) }
def text; end

sig { returns(::Integer) }
def to; end

sig { override.returns(::String) }
Expand Down
Loading
Loading