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 989a9061..9ffc0143 100644 --- a/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb +++ b/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb @@ -28,16 +28,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) - new(ruby_contents, file:, max_line_length:, overloads_strategy:).rewrite + new(ruby_contents, file:, max_line_length:, overloads_strategy:, erase_generic_types:).rewrite end end - #: (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) @@ -47,6 +49,8 @@ def initialize(ruby_contents, file:, max_line_length: nil, overloads_strategy: : @max_line_length = max_line_length @overloads_strategy = overloads_strategy + @erase_generic_types = erase_generic_types + @type_translator = RBI::RBS::TypeTranslator.new(erase_generic_types:) #: RBI::RBS::TypeTranslator end # @override @@ -133,11 +137,11 @@ def visit_attr(node) name = node.arguments&.arguments&.first #: as Prism::SymbolNode sig.params << RBI::SigParam.new( name.slice[1..-1], #: as String - RBI::RBS::TypeTranslator.translate(attr_type), + @type_translator.translate(attr_type), ) end - sig.return_type = RBI::RBS::TypeTranslator.translate(attr_type) + sig.return_type = @type_translator.translate(attr_type) apply_member_annotations(comments.method_annotations, sig) @@ -174,7 +178,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) @@ -259,7 +263,7 @@ def apply_class_annotations(node) "final!" when /^@requires_ancestor: / srb_type = ::RBS::Parser.parse_type(annotation.string.delete_prefix("@requires_ancestor: ")) - rbs_type = RBI::RBS::TypeTranslator.translate(srb_type) + rbs_type = @type_translator.translate(srb_type) "requires_ancestor { #{rbs_type} }" else next @@ -288,6 +292,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 @@ -304,12 +319,12 @@ def apply_class_annotations(node) if type_param.upper_bound || type_param.default_type if type_param.upper_bound - rbs_type = RBI::RBS::TypeTranslator.translate(type_param.upper_bound) + rbs_type = @type_translator.translate(type_param.upper_bound) type_member = "#{type_member} {{ upper: #{rbs_type} }}" end if type_param.default_type - rbs_type = RBI::RBS::TypeTranslator.translate(type_param.default_type) + rbs_type = @type_translator.translate(type_param.default_type) type_member = "#{type_member} {{ fixed: #{rbs_type} }}" end end @@ -417,7 +432,7 @@ def apply_type_aliases(comments) next unless decls.size == 1 && decls.first.is_a?(::RBS::AST::Declarations::TypeAlias) rbs_type = decls.first - sorbet_type = RBI::RBS::TypeTranslator.translate(rbs_type.type) + sorbet_type = @type_translator.translate(rbs_type.type) alias_name = ::RBS::TypeName.new( namespace: rbs_type.name.namespace, diff --git a/test/spoom/cli/srb/sigs_test.rb b/test/spoom/cli/srb/sigs_test.rb index 831bb886..24d47b78 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 02e04064..b248793d 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,262 @@ class << self ) end + def test_translate_to_rbi_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_erase_generic_types + contents = <<~RB + #: [E] + class Box + #: -> void + def initialize + @elems = [] #: Array[E] + end + end + + #: -> Box[Integer] + def box_of_int + Box.new + end + RB + + assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents, erase_generic_types: true)) + class Box + E = T.type_alias { T.anything } + + sig { void } + def initialize + @elems = [] #: Array[E] + end + end + + sig { returns(Box) } + def box_of_int + Box.new + end + RB + end + + def test_translate_to_rbi_erase_generic_types_in_class_member_sigs + contents = <<~RB + #: [E] + class Stack + #: -> void + def initialize + @items = [] #: Array[E] + end + + #: (E) -> void + def push(item) + @items << item + end + + #: -> E? + def pop + @items.pop + end + + #: E + attr_reader :last_pushed + end + RB + + assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents, erase_generic_types: true)) + class Stack + E = T.type_alias { T.anything } + + sig { void } + def initialize + @items = [] #: Array[E] + end + + sig { params(item: E).void } + def push(item) + @items << item + end + + sig { returns(::T.nilable(E)) } + def pop + @items.pop + end + + sig { returns(E) } + attr_reader :last_pushed + end + RB + end + + def test_translate_to_rbi_erase_generic_types_in_generic_method_sigs + contents = <<~RB + class Utils + #: [T] (Array[T]) -> T? + def self.first_or_nil(arr) + arr.first + end + + #: [T] (Array[T], T) -> Array[T] + def self.append(arr, item) + arr + [item] + end + end + RB + + assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents, erase_generic_types: true)) + class Utils + sig { params(arr: Array).returns(::T.nilable(::T.anything)) } + def self.first_or_nil(arr) + arr.first + end + + sig { params(arr: Array, item: ::T.anything).returns(Array) } + def self.append(arr, item) + arr + [item] + end + end + RB + end + + def test_translate_to_rbi_erase_generic_types_with_class_and_method_type_params + contents = <<~RB + #: [E] + class Container + #: [T] (E, T) -> void + def store(element, extra); end + end + RB + + assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents, erase_generic_types: true)) + class Container + E = T.type_alias { T.anything } + + sig { params(element: E, extra: ::T.anything).void } + def store(element, extra); end + end + RB + end + + def test_translate_to_rbi_erase_generic_types_in_attr_writer_and_accessor + contents = <<~RB + #: [E] + class Box + #: E + attr_writer :value + + #: E + attr_accessor :other + end + RB + + assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents, erase_generic_types: true)) + class Box + E = T.type_alias { T.anything } + + sig { params(value: E).returns(E) } + attr_writer :value + + sig { returns(E) } + attr_accessor :other + end + RB + end + + def test_translate_to_rbi_erase_generic_types_with_multiple_class_type_params + contents = <<~RB + #: [K, V] + class Map + #: (K) -> V? + def get(key); end + end + RB + + assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents, erase_generic_types: true)) + class Map + K = T.type_alias { T.anything } + + V = T.type_alias { T.anything } + + sig { params(key: K).returns(::T.nilable(V)) } + def get(key); end + end + RB + end + + def test_translate_to_rbi_erase_generic_types_in_nested_generic_classes + contents = <<~RB + #: [E] + class Outer + #: (E) -> void + def outer_m(x); end + + #: [F] + class Inner + #: (F) -> void + def inner_m(y); end + end + + #: (E) -> void + def after_inner(z); end + end + RB + + assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents, erase_generic_types: true)) + class Outer + E = T.type_alias { T.anything } + + sig { params(x: E).void } + def outer_m(x); end + + class Inner + F = T.type_alias { T.anything } + + sig { params(y: F).void } + def inner_m(y); end + end + + sig { params(z: E).void } + def after_inner(z); end + end + RB + end + + def test_translate_to_rbi_erase_generic_types_in_nested_generic_and_block + contents = <<~RB + #: [E] + class Holder + #: (Hash[String, E]) -> void + def consume(h); end + + #: () { (E) -> void } -> void + def each(&blk); end + end + RB + + assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents, erase_generic_types: true)) + class Holder + E = T.type_alias { T.anything } + + sig { params(h: Hash).void } + def consume(h); end + + sig { params(blk: ::T.proc.params(arg0: E).void).void } + def each(&blk); end + end + RB + end + def test_translate_to_rbi_in_block assert_rewrites_rbs( from: <<~RUBY, @@ -961,13 +1217,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.new( ruby_contents, file: "test.rb", max_line_length: max_line_length, overloads_strategy: overloads_strategy, + erase_generic_types: erase_generic_types, ).rewrite end