Skip to content
Open
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
103 changes: 103 additions & 0 deletions lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ def visit_def_node(node)
last_sigs.each do |node, sig|
next if sig.is_abstract && !@translate_abstract_methods

# RBS type variables shadow constants and class type members with the
# same name. Refuse colliding method type parameters so the user can
# rename them and keep names consistent.
raise_on_colliding_type_params!(sig)

preserve_multiline_signatures = !!(@preserve_multiline_signatures && sig.loc&.multiline?)

out = rbs_print(
Expand Down Expand Up @@ -388,6 +393,33 @@ def delete_extend_t_generics
@extend_t_generics.clear
end

#: (RBI::Sig) -> void
def raise_on_colliding_type_params!(sig)
return unless sig.type_params?

occupied = simple_type_names(sig) + class_type_member_names

sig.type_params.each do |type_param|
next unless occupied.any? { |name| name == type_param || name.start_with?("#{type_param}::") }

raise Error, "Type parameter `#{type_param}` collides with a constant or class type member. Rename the type parameter to avoid the collision."
end
end

#: (RBI::Sig) -> Array[String]
def simple_type_names(sig)
collector = SimpleTypeNameCollector.new
collector.visit_sig(sig)
collector.names
end

#: -> Array[String]
def class_type_member_names
@type_members.map do |member|
member.sub(/^(in|out)\s+/, "").split(/[\s<=]/).first #: as String
end
end

# Collects the last signatures visited and clears the current list
#: -> Array[[Prism::CallNode, RBI::Sig]]
def collect_last_sigs
Expand Down Expand Up @@ -417,6 +449,77 @@ def rbs_print(indent, preserve_multiline_signatures:, &block)
end
end.join + "\n"
end

class SigTypeWalker
#: (RBI::Sig) -> void
def visit_sig(sig)
sig.params.each do |param|
type = coerce_type(param.type)
param.instance_variable_set(:@type, type)
visit_type(type)
end
return_type = coerce_type(sig.return_type)
sig.return_type = return_type
visit_type(return_type)
end

#: (RBI::Type | String) -> RBI::Type
def coerce_type(type)
type.is_a?(String) ? RBI::Type.parse_string(type) : type
end

#: (RBI::Type) -> void
def visit_type(type)
case type
when RBI::Type::Simple
visit_simple(type)
when RBI::Type::TypeParameter
visit_type_parameter(type)
when RBI::Type::Generic
type.params.each { |param| visit_type(param) }
when RBI::Type::All, RBI::Type::Any, RBI::Type::Tuple
type.types.each { |inner| visit_type(inner) }
when RBI::Type::Nilable, RBI::Type::Class, RBI::Type::Module
visit_type(type.type)
when RBI::Type::ClassOf
visit_type(type.type)
type.type_parameters.each { |param| visit_type(param) }
when RBI::Type::TypeAlias
visit_type(type.aliased_type)
when RBI::Type::Proc
type.proc_params.each_value { |param| visit_type(param) }
if (proc_returns = type.proc_returns)
visit_type(proc_returns)
end
if (proc_bind = type.proc_bind)
visit_type(proc_bind)
end
when RBI::Type::Shape
type.types.each_value { |inner| visit_type(inner) }
end
end

#: (RBI::Type::Simple) -> void
def visit_simple(type); end

#: (RBI::Type::TypeParameter) -> void
def visit_type_parameter(type); end
end

class SimpleTypeNameCollector < SigTypeWalker
#: Array[String]
attr_reader :names

#: -> void
def initialize
@names = [] #: Array[String]
end

#: (RBI::Type::Simple) -> void
def visit_simple(type)
@names << type.name
end
end
end
end
end
Expand Down
76 changes: 76 additions & 0 deletions test/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,82 @@ def baz(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x,
RBS
end

def test_translate_type_parameter_that_collides_with_a_constant
contents = <<~RB
class Store
module Credential
module Multiple; end
end

sig do
type_parameters(:Credential)
.params(type: T.all(T::Module[T.type_parameter(:Credential)], T::Module[Credential::Multiple]))
.returns(T::Array[T.type_parameter(:Credential)])
end
def find(type)
[]
end
end
RB

error = assert_raises(Translate::Error) do
sorbet_sigs_to_rbs_comments(contents)
end
assert_equal(
"Type parameter `Credential` collides with a constant or class type member. Rename the type parameter to avoid the collision.",
error.message,
)
end

def test_translate_type_parameter_that_collides_with_a_class_type_member
contents = <<~RB
class Box
extend T::Generic

Elem = type_member

sig do
type_parameters(:Elem)
.params(value: T.type_parameter(:Elem))
.returns(Elem)
end
def wrap(value)
value
end
end
RB

error = assert_raises(Translate::Error) do
sorbet_sigs_to_rbs_comments(contents)
end
assert_equal(
"Type parameter `Elem` collides with a constant or class type member. Rename the type parameter to avoid the collision.",
error.message,
)
end

def test_translate_type_parameter_without_name_collision
contents = <<~RB
sig do
type_parameters(:U)
.params(value: T.type_parameter(:U))
.returns(T.type_parameter(:U))
end
def identity(value)
value
end
RB

assert_equal(<<~RBS, sorbet_sigs_to_rbs_comments(contents))
#: [U] (
#| U value
#| ) -> U
def identity(value)
value
end
RBS
end

private

#: (
Expand Down
Loading