diff --git a/lib/spoom/cli/srb/sigs.rb b/lib/spoom/cli/srb/sigs.rb index 078744bc..3c3b1270 100644 --- a/lib/spoom/cli/srb/sigs.rb +++ b/lib/spoom/cli/srb/sigs.rb @@ -22,6 +22,10 @@ class Sigs < Thor option :translate_generics, type: :boolean, desc: "Translate generics", default: false option :translate_helpers, type: :boolean, desc: "Translate helpers", default: false option :translate_abstract_methods, type: :boolean, desc: "Translate abstract methods", default: false + option :erase_generic_types, + type: :boolean, + desc: "Drop generic types when translating from RBS to RBI", + default: false def translate(*paths) from = options[:from] to = options[:to] @@ -65,6 +69,7 @@ def translate(*paths) contents, file: file, max_line_length: max_line_length, + erase_generic_types: options[:erase_generic_types], ) end end diff --git a/lib/spoom/sorbet/translate.rb b/lib/spoom/sorbet/translate.rb index bf815176..14ebf00b 100644 --- a/lib/spoom/sorbet/translate.rb +++ b/lib/spoom/sorbet/translate.rb @@ -53,13 +53,15 @@ def sorbet_sigs_to_rbs_comments( # Converts all the RBS comments in the given Ruby code to `sig` nodes. # It also handles type members and class annotations. - #: (String ruby_contents, file: String, ?max_line_length: Integer?, ?overloads_strategy: Symbol) -> String - def rbs_comments_to_sorbet_sigs(ruby_contents, file:, max_line_length: nil, overloads_strategy: :translate_all) + #: (String ruby_contents, file: String, ?max_line_length: Integer?, ?overloads_strategy: Symbol, ?erase_generic_types: bool) -> String + def rbs_comments_to_sorbet_sigs(ruby_contents, file:, max_line_length: nil, overloads_strategy: :translate_all, + erase_generic_types: false) RBSCommentsToSorbetSigs.rewrite_if_needed( ruby_contents, file: file, max_line_length: max_line_length, overloads_strategy: overloads_strategy, + erase_generic_types: erase_generic_types, ) end diff --git a/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb b/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb index eb652099..ee88a3e1 100644 --- a/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb +++ b/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb @@ -24,11 +24,18 @@ def contains_rbs_syntax?(source) Sigils.contains_valid_sigil?(source) && source.match?(RBS_REWRITE_PATTERN) end - #: (String ruby_contents, file: String, ?max_line_length: Integer?, ?overloads_strategy: Symbol) -> String - def rewrite_if_needed(ruby_contents, file:, max_line_length: nil, overloads_strategy: :translate_all) + #: (String ruby_contents, file: String, ?max_line_length: Integer?, ?overloads_strategy: Symbol, ?erase_generic_types: bool) -> String + def rewrite_if_needed(ruby_contents, file:, max_line_length: nil, overloads_strategy: :translate_all, + erase_generic_types: false) return ruby_contents unless contains_rbs_syntax?(ruby_contents) - HumanReadableTranslator.new(ruby_contents, file:, max_line_length:, overloads_strategy:).rewrite + HumanReadableTranslator.new( + ruby_contents, + file:, + max_line_length:, + overloads_strategy:, + erase_generic_types:, + ).rewrite end end end diff --git a/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/base_translator.rb b/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/base_translator.rb index 2528ffe5..0f7da9eb 100644 --- a/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/base_translator.rb +++ b/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/base_translator.rb @@ -11,8 +11,9 @@ class BaseTranslator < Translator ALLOWED_OVERLOAD_STRATEGIES = [:translate_all, :translate_last, :raise].freeze #: Array[Symbol] - #: (String, file: String, ?max_line_length: Integer?, ?overloads_strategy: Symbol) -> void - def initialize(ruby_contents, file:, max_line_length: nil, overloads_strategy: :translate_all) + #: (String, file: String, ?max_line_length: Integer?, ?overloads_strategy: Symbol, ?erase_generic_types: bool) -> void + def initialize(ruby_contents, file:, max_line_length: nil, overloads_strategy: :translate_all, + erase_generic_types: false) super(ruby_contents, file: file) unless ALLOWED_OVERLOAD_STRATEGIES.include?(overloads_strategy) @@ -22,7 +23,8 @@ def initialize(ruby_contents, file:, max_line_length: nil, overloads_strategy: : @max_line_length = max_line_length @overloads_strategy = overloads_strategy - @type_translator = RBI::RBS::TypeTranslator.new #: RBI::RBS::TypeTranslator + @erase_generic_types = erase_generic_types + @type_translator = RBI::RBS::TypeTranslator.new(erase_generic_types:) #: RBI::RBS::TypeTranslator end # @override @@ -150,7 +152,7 @@ def rewrite_def(def_node, comments) next end - translator = RBI::RBS::MethodTypeTranslator.new(rbi_node) + translator = RBI::RBS::MethodTypeTranslator.new(rbi_node, erase_generic_types: @erase_generic_types) begin translator.visit(method_type) @@ -264,6 +266,17 @@ def apply_class_annotations(node) to = adjust_to_line_end(signature.location.end_offset) @rewriter << Source::Delete.new(from, to) + if @erase_generic_types + type_params.each do |type_param| + @rewriter << Source::Insert.new( + insert_pos, + "\n#{indent}#{type_param.name} = ::T.type_alias { ::T.anything }\n", + ) + end + + next + end + unless already_extends?(node, /^(::)?T::Generic$/) @rewriter << Source::Insert.new(insert_pos, "\n#{indent}extend T::Generic\n") end @@ -404,7 +417,7 @@ def apply_type_aliases(comments) ) @rewriter << Source::Delete.new(from, to) - content = "#{indent}#{alias_name} = T.type_alias { #{sorbet_type.to_rbi} }\n" + content = "#{indent}#{alias_name} = ::T.type_alias { #{sorbet_type.to_rbi} }\n" @rewriter << Source::Insert.new(insert_pos, content) rescue ::RBS::ParsingError, ::RBI::Error # Ignore type aliases with errors diff --git a/test/spoom/cli/srb/sigs_test.rb b/test/spoom/cli/srb/sigs_test.rb index 831bb886..ecb74e1b 100644 --- a/test/spoom/cli/srb/sigs_test.rb +++ b/test/spoom/cli/srb/sigs_test.rb @@ -197,6 +197,46 @@ def foo(a, b = 42, *c, d:, e: 42, **f); end RB end + def test_translate_rbs_to_rbi_with_erase_generic_types + @project.write!("file.rb", <<~RB) + # typed: true + + #: [E] + class Box + #: -> Array[E] + def values + [] + end + + #: (E) -> void + def push(value) + end + end + RB + + result = @project.spoom("srb sigs translate --from rbs --to rbi --no-color --erase-generic-types") + + assert_empty(result.err) + assert(result.status) + + assert_equal(<<~RB, @project.read("file.rb")) + # typed: true + + class Box + E = ::T.type_alias { ::T.anything } + + sig { returns(Array) } + def values + [] + end + + sig { params(value: E).void } + def push(value) + end + end + RB + end + def test_translate_includes_rbi_files @project.write!("file.rb", <<~RB) sig { void } diff --git a/test/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs_test.rb b/test/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs_test.rb index 1deb6cf7..77efd53d 100644 --- a/test/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs_test.rb +++ b/test/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs_test.rb @@ -449,6 +449,126 @@ class << self ) end + def test_translate_to_rbi_preserves_generic_types + contents = <<~RB + #: -> Array[Integer] + def foo + [] + end + RB + + assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents)) + sig { returns(::T::Array[Integer]) } + def foo + [] + end + RB + end + + def test_translate_to_rbi_with_erase_generic_class_types + contents = <<~RB + #: [T, in A, out B, C < Numeric, D > Numeric, E = String] + class Result + #: (T, E?) -> void + def initialize(value, error) + @value = value + @error = error + end + + #: T + attr_reader :value + + #: E? + attr_writer :error + end + + #: (String) -> Result[Integer, String] + def parse_int(str) + Result.new(Integer(str), nil) #: Result[Integer, String] + end + RB + + assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents, erase_generic_types: true)) + class Result + T = ::T.type_alias { ::T.anything } + + A = ::T.type_alias { ::T.anything } + + B = ::T.type_alias { ::T.anything } + + C = ::T.type_alias { ::T.anything } + + D = ::T.type_alias { ::T.anything } + + E = ::T.type_alias { ::T.anything } + + sig { params(value: T, error: ::T.nilable(E)).void } + def initialize(value, error) + @value = value + @error = error + end + + sig { returns(T) } + attr_reader :value + + sig { params(error: ::T.nilable(E)).returns(::T.nilable(E)) } + attr_writer :error + end + + sig { params(str: String).returns(Result) } + def parse_int(str) + Result.new(Integer(str), nil) #: Result[Integer, String] + end + RB + end + + def test_translate_to_rbi_with_erase_generic_method_types + contents = <<~RB + class Factory + #: [T] (T?) -> T? + def self.identity(value) + value + end + + class << self + #: [U] (U) -> U + def wrap(value) + value + end + end + + #: [V] + class << self + #: -> V + def build; end + end + end + RB + + assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents, erase_generic_types: true)) + class Factory + sig { params(value: ::T.nilable(::T.anything)).returns(::T.nilable(::T.anything)) } + def self.identity(value) + value + end + + class << self + sig { params(value: ::T.anything).returns(::T.anything) } + def wrap(value) + value + end + end + + class << self + V = ::T.type_alias { ::T.anything } + + sig { returns(V) } + def build; end + end + end + RB + end + def test_translate_to_rbi_in_block assert_rewrites_rbs( from: <<~RUBY, @@ -567,8 +687,8 @@ def bar(a) to_pretty_format_for_humans: <<~RUBY, module Aliases - Foo = T.type_alias { ::T.any(Integer, String) } - MultiLine = T.type_alias { ::T.any(Integer, String) } + Foo = ::T.type_alias { ::T.any(Integer, String) } + MultiLine = ::T.type_alias { ::T.any(Integer, String) } end sig { params(a: Aliases::Foo).returns(Aliases::MultiLine) } @@ -592,8 +712,8 @@ def process_user(data) RUBY to_pretty_format_for_humans: <<~RUBY, - Foo::UserId = T.type_alias { Integer } - ::Bar::UserData = T.type_alias { { id: Foo::UserId, name: String } } + Foo::UserId = ::T.type_alias { Integer } + ::Bar::UserData = ::T.type_alias { { id: Foo::UserId, name: String } } sig { params(data: ::Bar::UserData).returns(Foo::UserId) } def process_user(data) @@ -618,7 +738,7 @@ def get_status to_pretty_format_for_humans: <<~RUBY, class Example - Status = T.type_alias { Symbol } + Status = ::T.type_alias { Symbol } sig { returns(Status) } def get_status @@ -641,7 +761,7 @@ def status; end to_pretty_format_for_humans: <<~RUBY, class Example - Status = T.type_alias { Symbol } + Status = ::T.type_alias { Symbol } sig { returns(Status) } def status; end end @@ -663,7 +783,7 @@ def double_items(items) RUBY to_pretty_format_for_humans: <<~RUBY, - List = T.type_alias { ::T::Array[Integer] } + List = ::T.type_alias { ::T::Array[Integer] } sig { params(items: List).returns(List) } def double_items(items) @@ -673,6 +793,30 @@ def double_items(items) ) end + def test_translate_type_alias_with_erase_generic_types + contents = <<~RB + class Box; end + + #: type boxed_string = Box[String] + + #: () -> boxed_string + def build_box + Box.new + end + RB + + assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents, erase_generic_types: true)) + class Box; end + + BoxedString = ::T.type_alias { Box } + + sig { returns(BoxedString) } + def build_box + Box.new + end + RB + end + def test_translate_type_alias_with_union assert_rewrites_rbs( from: <<~RUBY, @@ -685,7 +829,7 @@ def ensure_string(text) RUBY to_pretty_format_for_humans: <<~RUBY, - NullableString = T.type_alias { ::T.nilable(String) } + NullableString = ::T.type_alias { ::T.nilable(String) } sig { params(text: NullableString).returns(String) } def ensure_string(text) @@ -727,7 +871,7 @@ def foo RUBY to_pretty_format_for_humans: <<~RUBY, - MultiLine = T.type_alias { ::T.any(String, Integer) } + MultiLine = ::T.type_alias { ::T.any(String, Integer) } # foo bar baz #| | Symbol @@ -764,7 +908,7 @@ class Range to_pretty_format_for_humans: <<~RUBY, module Foo - SerializedRange = T.type_alias { [Integer, Integer] } + SerializedRange = ::T.type_alias { [Integer, Integer] } class Range end end @@ -983,13 +1127,15 @@ def foo; end private - #: (String, ?max_line_length: Integer?, ?overloads_strategy: Symbol) -> String - def rbs_comments_to_sorbet_sigs(ruby_contents, max_line_length: nil, overloads_strategy: :translate_all) + #: (String, ?max_line_length: Integer?, ?overloads_strategy: Symbol, ?erase_generic_types: bool) -> String + def rbs_comments_to_sorbet_sigs(ruby_contents, max_line_length: nil, overloads_strategy: :translate_all, + erase_generic_types: false) RBSCommentsToSorbetSigs::HumanReadableTranslator.new( ruby_contents, file: "test.rb", max_line_length: max_line_length, overloads_strategy: overloads_strategy, + erase_generic_types: erase_generic_types, ).rewrite end