From a78fe18cc840ef191e576fd69e58d2d7659a30b6 Mon Sep 17 00:00:00 2001 From: Julia Boutin Date: Thu, 28 May 2026 16:20:02 -0600 Subject: [PATCH] Add `erase_generic_types` option to RBS translators Previously, `RBS::TypeTranslator` and `RBS::MethodTypeTranslator` always translated generic types like `Foo[Bar]`. However, `sorbet-runtime` is unable to enforce them, so they are superfluous in runtime contexts This commit adds a default `false` `erase_generic_types` kwarg to both translators. When set to `true` generic types are dropped and the simple type is returned (`Foo[Bar]` to `Foo`) --- lib/rbi/rbs/method_type_translator.rb | 6 +- lib/rbi/rbs/type_translator.rb | 66 ++++++++++++--------- rbi/rbi.rbi | 6 ++ test/rbi/rbs/method_type_translator_test.rb | 45 +++++++++++++- test/rbi/rbs/type_translator_test.rb | 31 +++++++++- 5 files changed, 117 insertions(+), 37 deletions(-) diff --git a/lib/rbi/rbs/method_type_translator.rb b/lib/rbi/rbs/method_type_translator.rb index 308c5f57..e3aa08c3 100644 --- a/lib/rbi/rbs/method_type_translator.rb +++ b/lib/rbi/rbs/method_type_translator.rb @@ -48,8 +48,10 @@ def initialize(method, options: HumanReadableOptions.default) #: (::RBS::MethodType) -> void def visit(type) - type.type_params.each do |param| - result.type_params << param.name + unless @options.erase_generic_types? + type.type_params.each do |param| + result.type_params << param.name + end end visit_function_type(type.type) diff --git a/lib/rbi/rbs/type_translator.rb b/lib/rbi/rbs/type_translator.rb index 7c6e90ad..3299d6e6 100644 --- a/lib/rbi/rbs/type_translator.rb +++ b/lib/rbi/rbs/type_translator.rb @@ -67,7 +67,8 @@ def translate(type) when ::RBS::Types::Bases::Void Type.void when ::RBS::Types::ClassSingleton - type_parameter = type.args.first ? translate(type.args.first) : nil + type_arg = type.args.first + type_parameter = translate(type_arg) if type_arg && !@options.erase_generic_types? Type.class_of(Type.simple(type.name.to_s), type_parameter) when ::RBS::Types::ClassInstance translate_class_instance(type) @@ -107,7 +108,7 @@ def translate(type) when ::RBS::Types::UntypedFunction Type.proc.params(arg0: Type.untyped).returns(Type.untyped) when ::RBS::Types::Variable - Type.type_parameter(type.name) + @options.erase_generic_types? ? Type.anything : Type.type_parameter(type.name) else type #: absurd end @@ -129,10 +130,14 @@ def translate_type_alias(type) #: (::RBS::Types::ClassInstance) -> Type def translate_class_instance(type) - return Type.simple(type.name.to_s) if type.args.empty? + type_name = type.name.to_s + return Type.simple(type_name) if type.args.empty? - type_name = translate_t_generic_type(type.name.to_s) - Type.generic(type_name, *type.args.map { |arg| translate(arg) }) + if @options.erase_generic_types? + Type.simple(erase_t_generic_type(type_name)) + else + Type.generic(translate_t_generic_type(type_name), *type.args.map { |arg| translate(arg) }) + end end #: (::RBS::Types::Function) -> Type @@ -182,32 +187,35 @@ def translate_function(type) proc end + RUNTIME_GENERIC_TYPES = [ + "Array", + "Class", + "Enumerable", + "Enumerator", + "Enumerator::Chain", + "Enumerator::Lazy", + "Hash", + "Module", + "Set", + "Range", + ].freeze #: Array[String] + + GENERIC_TYPE_TO_SORBET_GENERIC_TYPE = RUNTIME_GENERIC_TYPES.flat_map do |type| + [[type, "::T::#{type}"], ["::#{type}", "::T::#{type}"]] + end.to_h.freeze #: Hash[String, String] + + SORBET_GENERIC_TYPE_TO_GENERIC_TYPE = RUNTIME_GENERIC_TYPES.flat_map do |type| + [["T::#{type}", "::#{type}"], ["::T::#{type}", "::#{type}"]] + end.to_h.freeze #: Hash[String, String] + #: (String type_name) -> String def translate_t_generic_type(type_name) - case type_name.delete_prefix("::") - when "Array" - "::T::Array" - when "Class" - "::T::Class" - when "Enumerable" - "::T::Enumerable" - when "Enumerator" - "::T::Enumerator" - when "Enumerator::Chain" - "::T::Enumerator::Chain" - when "Enumerator::Lazy" - "::T::Enumerator::Lazy" - when "Hash" - "::T::Hash" - when "Module" - "::T::Module" - when "Set" - "::T::Set" - when "Range" - "::T::Range" - else - type_name - end + GENERIC_TYPE_TO_SORBET_GENERIC_TYPE.fetch(type_name, type_name) + end + + #: (String type_name) -> String + def erase_t_generic_type(type_name) + SORBET_GENERIC_TYPE_TO_GENERIC_TYPE.fetch(type_name, type_name) end end end diff --git a/rbi/rbi.rbi b/rbi/rbi.rbi index 22e1ed05..0a47fc27 100644 --- a/rbi/rbi.rbi +++ b/rbi/rbi.rbi @@ -1447,6 +1447,9 @@ class RBI::RBS::TypeTranslator private + sig { params(type_name: ::String).returns(::String) } + def erase_t_generic_type(type_name); end + sig { params(type: ::RBS::Types::ClassInstance).returns(::RBI::Type) } def translate_class_instance(type); end @@ -1469,9 +1472,12 @@ class RBI::RBS::TypeTranslator end end +RBI::RBS::TypeTranslator::GENERIC_TYPE_TO_SORBET_GENERIC_TYPE = T.let(T.unsafe(nil), Hash) RBI::RBS::TypeTranslator::HumanReadableOptions = RBI::RBS::MethodTypeTranslator::HumanReadableOptions RBI::RBS::TypeTranslator::Options = RBI::RBS::MethodTypeTranslator::Options +RBI::RBS::TypeTranslator::RUNTIME_GENERIC_TYPES = T.let(T.unsafe(nil), Array) RBI::RBS::TypeTranslator::RbsType = T.type_alias { T.any(::RBS::Types::Alias, ::RBS::Types::Bases::Any, ::RBS::Types::Bases::Bool, ::RBS::Types::Bases::Bottom, ::RBS::Types::Bases::Class, ::RBS::Types::Bases::Instance, ::RBS::Types::Bases::Nil, ::RBS::Types::Bases::Self, ::RBS::Types::Bases::Top, ::RBS::Types::Bases::Void, ::RBS::Types::ClassInstance, ::RBS::Types::ClassSingleton, ::RBS::Types::Function, ::RBS::Types::Interface, ::RBS::Types::Intersection, ::RBS::Types::Literal, ::RBS::Types::Optional, ::RBS::Types::Proc, ::RBS::Types::Record, ::RBS::Types::Tuple, ::RBS::Types::Union, ::RBS::Types::UntypedFunction, ::RBS::Types::Variable) } +RBI::RBS::TypeTranslator::SORBET_GENERIC_TYPE_TO_GENERIC_TYPE = T.let(T.unsafe(nil), Hash) class RBI::RBSComment < ::RBI::Comment sig { params(other: ::Object).returns(T::Boolean) } diff --git a/test/rbi/rbs/method_type_translator_test.rb b/test/rbi/rbs/method_type_translator_test.rb index cba0a1fa..bc652658 100644 --- a/test/rbi/rbs/method_type_translator_test.rb +++ b/test/rbi/rbs/method_type_translator_test.rb @@ -195,12 +195,51 @@ def test_translate_generic_singleton assert_equal(Type.class_of(Type.simple("Foo"), Type.simple("Bar")), sig.return_type) end + def test_erase_generic_types_replaces_method_type_parameters + sig = translate( + "[T, U] (Array[T], T?, T | Integer, [T, Integer], singleton(Foo)[T]) { (T) -> U } -> U?", + Method.new("foo", params: [ + ReqParam.new("array"), + ReqParam.new("value"), + ReqParam.new("fallback"), + ReqParam.new("pair"), + ReqParam.new("klass"), + BlockParam.new("block"), + ]), + erase_generic_types: true, + ) + + assert_empty(sig.type_params) + assert_equal( + [ + SigParam.new("array", Type.simple("Array")), + SigParam.new("value", Type.nilable(Type.anything)), + SigParam.new("fallback", Type.any(Type.anything, Type.simple("Integer"))), + SigParam.new("pair", Type.tuple([Type.anything, Type.simple("Integer")])), + SigParam.new("klass", Type.class_of(Type.simple("Foo"))), + SigParam.new( + "block", + Type.proc + .params(arg0: Type.anything) + .returns(Type.anything), + ), + ], + sig.params, + ) + assert_equal(Type.nilable(Type.anything), sig.return_type) + end + private - #: (String, Method) -> RBI::Sig - def translate(rbs_string, method) + #: (String, Method, ?erase_generic_types: bool) -> RBI::Sig + def translate(rbs_string, method, erase_generic_types: false) node = ::RBS::Parser.parse_method_type(rbs_string, require_eof: true) - RBS::MethodTypeTranslator.translate(method, node) + + options = MethodTypeTranslator::HumanReadableOptions.new(erase_generic_types:) + + translator = RBS::MethodTypeTranslator.new(method, options:) + translator.visit(node) + translator.result end end end diff --git a/test/rbi/rbs/type_translator_test.rb b/test/rbi/rbs/type_translator_test.rb index 46dbd5da..a41c246a 100644 --- a/test/rbi/rbs/type_translator_test.rb +++ b/test/rbi/rbs/type_translator_test.rb @@ -94,6 +94,30 @@ def test_translate_class_singleton assert_equal(Type.class_of(Type.simple("Foo"), Type.simple("Bar")), translate("singleton(Foo)[Bar]")) end + def test_erase_generic_types_keeps_non_generics + assert_equal(Type.simple("Foo"), translate("Foo", erase_generic_types: true)) + assert_equal(Type.simple("::Foo::Bar"), translate("::Foo::Bar", erase_generic_types: true)) + end + + def test_erase_generic_types_removes_generic_types + simple_foo = Type.simple("Foo") + class_foo = Type.class_of(simple_foo) + simple_array = Type.simple("Array") + root_array = Type.simple("::Array") + + assert_equal(simple_foo, translate("Foo[Bar]", erase_generic_types: true)) + assert_equal(simple_foo, translate("Foo[Bar, ::Baz]", erase_generic_types: true)) + assert_equal(class_foo, translate("singleton(Foo)", erase_generic_types: true)) + assert_equal(class_foo, translate("singleton(Foo)[Bar]", erase_generic_types: true)) + assert_equal(simple_array, translate("Array[Foo]", erase_generic_types: true)) + assert_equal(root_array, translate("T::Array[Foo]", erase_generic_types: true)) + assert_equal(root_array, translate("::T::Array[Foo]", erase_generic_types: true)) + assert_equal(root_array, translate("::Array[Bar]", erase_generic_types: true)) + assert_equal(Type.simple("::Hash"), translate("::Hash[Foo, Bar]", erase_generic_types: true)) + assert_equal(Type.simple("::Foo"), translate("::Foo[Bar]", erase_generic_types: true)) + assert_equal(Type.simple("::Types::Foo"), translate("::Types::Foo[Bar]", erase_generic_types: true)) + end + def test_translate_interface assert_equal(Type.untyped, translate("_Foo")) end @@ -170,10 +194,11 @@ def test_translate_untyped_function private - #: (String) -> RBI::Type - def translate(rbs_string) + #: (String, ?erase_generic_types: bool) -> RBI::Type + def translate(rbs_string, erase_generic_types: false) node = ::RBS::Parser.parse_type(rbs_string, require_eof: true) - RBS::TypeTranslator.new.translate(node) + options = MethodTypeTranslator::HumanReadableOptions.new(erase_generic_types:) + RBS::TypeTranslator.new(options:).translate(node) end end end