diff --git a/lib/spoom/sorbet/translate.rb b/lib/spoom/sorbet/translate.rb index deb8dd40..0ab73cae 100644 --- a/lib/spoom/sorbet/translate.rb +++ b/lib/spoom/sorbet/translate.rb @@ -53,9 +53,14 @@ 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?) -> String - def rbs_comments_to_sorbet_sigs(ruby_contents, file:, max_line_length: nil) - RBSCommentsToSorbetSigs.new(ruby_contents, file: file, max_line_length: max_line_length).rewrite + #: (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) + RBSCommentsToSorbetSigs.new( + ruby_contents, + file: file, + max_line_length: max_line_length, + overloads_strategy: overloads_strategy, + ).rewrite end # Converts all `T.let` and `T.cast` nodes to RBS comments in the given Ruby code. 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 edbe318b..4a7d9bbb 100644 --- a/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb +++ b/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb @@ -7,11 +7,19 @@ module Translate class RBSCommentsToSorbetSigs < Translator include Spoom::RBS::ExtractRBSComments - #: (String, file: String, ?max_line_length: Integer?) -> void - def initialize(ruby_contents, file:, max_line_length: nil) + 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) super(ruby_contents, file: file) + unless ALLOWED_OVERLOAD_STRATEGIES.include?(overloads_strategy) + raise ArgumentError, "Unknown overloads_strategy: #{overloads_strategy.inspect}. " \ + "Must be one of: #{ALLOWED_OVERLOAD_STRATEGIES.map(&:inspect).join(", ")}" + end + @max_line_length = max_line_length + @overloads_strategy = overloads_strategy end # @override @@ -80,7 +88,9 @@ def visit_attr(node) return if comments.signatures.empty? - comments.signatures.each do |signature| + signatures = apply_overloads_strategy(comments.signatures) + + signatures.each do |signature| attr_type = ::RBS::Parser.parse_type(signature.string) sig = RBI::Sig.new @@ -116,11 +126,13 @@ def rewrite_def(def_node, comments) return if comments.empty? return if comments.signatures.empty? + signatures = apply_overloads_strategy(comments.signatures) + builder = RBI::Parser::TreeBuilder.new(@ruby_contents, comments: [], file: @file) builder.visit(def_node) rbi_node = builder.tree.nodes.first #: as RBI::Method - comments.signatures.each do |signature| + signatures.each do |signature| begin method_type = ::RBS::Parser.parse_method_type(signature.string) rescue ::RBS::ParsingError @@ -153,6 +165,26 @@ def rewrite_def(def_node, comments) end end + #: (Array[RBS::Signature]) -> Array[RBS::Signature] + def apply_overloads_strategy(signatures) + return signatures if signatures.size <= 1 + + case @overloads_strategy + when :translate_all + signatures + when :translate_last + kept = signatures.first #: as RBS::Signature + signatures.drop(1).each do |signature| + from = adjust_to_line_start(signature.location.start_offset) + to = adjust_to_line_end(signature.location.end_offset) + @rewriter << Source::Delete.new(from, to) + end + [kept] + else # :raise + raise Error, "Method has multiple overloaded signatures" + end + end + #: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode) -> void def apply_class_annotations(node) comments = node_rbs_comments(node) diff --git a/rbi/spoom.rbi b/rbi/spoom.rbi index ea216e3d..7f0914ec 100644 --- a/rbi/spoom.rbi +++ b/rbi/spoom.rbi @@ -2851,8 +2851,15 @@ Spoom::Sorbet::Sigils::VALID_STRICTNESS = T.let(T.unsafe(nil), Array) module Spoom::Sorbet::Translate class << self - sig { params(ruby_contents: ::String, file: ::String, max_line_length: T.nilable(::Integer)).returns(::String) } - def rbs_comments_to_sorbet_sigs(ruby_contents, file:, max_line_length: T.unsafe(nil)); end + sig do + params( + ruby_contents: ::String, + file: ::String, + max_line_length: T.nilable(::Integer), + overloads_strategy: ::Symbol + ).returns(::String) + end + def rbs_comments_to_sorbet_sigs(ruby_contents, file:, max_line_length: T.unsafe(nil), overloads_strategy: T.unsafe(nil)); end sig do params( @@ -2890,8 +2897,15 @@ class Spoom::Sorbet::Translate::Error < ::Spoom::Error; end class Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs < ::Spoom::Sorbet::Translate::Translator include ::Spoom::RBS::ExtractRBSComments - sig { params(ruby_contents: ::String, file: ::String, max_line_length: T.nilable(::Integer)).void } - def initialize(ruby_contents, file:, max_line_length: T.unsafe(nil)); end + sig do + params( + ruby_contents: ::String, + file: ::String, + max_line_length: T.nilable(::Integer), + overloads_strategy: ::Symbol + ).void + end + def initialize(ruby_contents, file:, max_line_length: T.unsafe(nil), overloads_strategy: T.unsafe(nil)); end sig { override.params(node: ::Prism::CallNode).void } def visit_call_node(node); end @@ -2927,6 +2941,9 @@ class Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs < ::Spoom::Sorbet::Trans sig { params(annotations: T::Array[::Spoom::RBS::Annotation], sig: ::RBI::Sig).void } def apply_member_annotations(annotations, sig); end + sig { params(signatures: T::Array[::Spoom::RBS::Signature]).returns(T::Array[::Spoom::RBS::Signature]) } + def apply_overloads_strategy(signatures); end + sig { params(comments: T::Array[::Prism::Comment]).void } def apply_type_aliases(comments); end @@ -2940,6 +2957,8 @@ class Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs < ::Spoom::Sorbet::Trans def visit_attr(node); end end +Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::ALLOWED_OVERLOAD_STRATEGIES = T.let(T.unsafe(nil), Array) + class Spoom::Sorbet::Translate::SorbetAssertionsToRBSComments < ::Spoom::Sorbet::Translate::Translator sig do params( 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 bf0d2d8c..140c9b93 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 @@ -678,11 +678,81 @@ class Range RB end + def test_translate_overloads_translate_all_is_default + contents = <<~RB + class Foo + #: () { (Integer) -> void } -> void + #: () -> Enumerator[Integer, void] + def each(&block); end + end + RB + + assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents)) + class Foo + sig { params(block: ::T.proc.params(arg0: Integer).void).void } + sig { returns(::T::Enumerator[Integer, void]) } + def each(&block); end + end + RB + end + + def test_translate_overloads_translate_last + contents = <<~RB + class Foo + #: () { (Integer) -> void } -> void + #: () -> Enumerator[Integer, void] + def each(&block); end + end + RB + + assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents, overloads_strategy: :translate_last)) + class Foo + sig { returns(::T::Enumerator[Integer, void]) } + def each(&block); end + end + RB + end + + def test_translate_overloads_raise + contents = <<~RB + class Foo + #: () { (Integer) -> void } -> void + #: () -> Enumerator[Integer, void] + def each(&block); end + end + RB + + assert_raises(Translate::Error) do + rbs_comments_to_sorbet_sigs(contents, overloads_strategy: :raise) + end + end + + def test_translate_overloads_single_signature_unaffected + contents = <<~RB + class Foo + #: () -> void + def foo; end + end + RB + + assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents, overloads_strategy: :translate_last)) + class Foo + sig { void } + def foo; end + end + RB + end + private - #: (String, ?max_line_length: Integer?) -> String - def rbs_comments_to_sorbet_sigs(ruby_contents, max_line_length: nil) - Translate.rbs_comments_to_sorbet_sigs(ruby_contents, file: "test.rb", max_line_length: max_line_length) + #: (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) + Translate.rbs_comments_to_sorbet_sigs( + ruby_contents, + file: "test.rb", + max_line_length: max_line_length, + overloads_strategy: overloads_strategy, + ) end end end