diff --git a/.ruby-version b/.ruby-version index fcdb2e10..f9892605 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -4.0.0 +3.4.4 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..57b20347 100644 --- a/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb +++ b/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb @@ -184,6 +184,16 @@ def rewrite_def(def_node, comments) sig = translator.result + # When the method uses Ruby 3.1+ anonymous block forwarding (`&` with no name), + # the RBI translator names the block param `&block`, producing `&block:` inside + # `params()` which is a syntax error. Detect that case and rename it to `block` + # (without the `&`) so the generated sig is valid Ruby. + if anonymous_block_param?(def_node) + sig.params.each do |param| + param.name = param.name.delete_prefix("&") if param.name.start_with?("&") + end + end + apply_member_annotations(comments.method_annotations, sig) # Sorbet runtime doesn't support `sig` on `method_added` or @@ -348,6 +358,14 @@ def apply_member_annotations(annotations, sig) end end + # Returns true if the def node uses an anonymous block parameter (`&` with no name), + # i.e. Ruby 3.1+ anonymous block forwarding like `def foo(&); end`. + #: (Prism::DefNode) -> bool + def anonymous_block_param?(def_node) + block_param = def_node.parameters&.block + block_param.is_a?(Prism::BlockParameterNode) && block_param.name.nil? + end + #: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode, Regexp) -> bool def already_extends?(node, constant_regex) node.child_nodes.any? do |c| @@ -438,4 +456,4 @@ def apply_type_aliases(comments) end end end -end +end \ No newline at end of file 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 77342b9b..eb8c38d7 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 @@ -890,6 +890,45 @@ def rbs_comments_to_sorbet_sigs(ruby_contents, max_line_length: nil, overloads_s overloads_strategy: overloads_strategy, ).rewrite end + + def test_rbs_comments_to_sorbet_sigs_anonymous_block_param + res = Spoom::Sorbet::Translate.rbs_comments_to_sorbet_sigs(<<~RUBY, file: "test.rb") + # typed: true + class Foo + #: (String) ?{ (String) -> void } -> String + def bar(request, &); end + end + RUBY + + assert_equal(<<~RUBY, res) + # typed: true + class Foo + sig { params(request: String, block: ::T.nilable(::T.proc.params(arg0: String).void)).returns(String) } + def bar(request, &); end + end + RUBY + + # Must also be valid Ruby + assert RubyVM::InstructionSequence.compile(res) + end + + def test_rbs_comments_to_sorbet_sigs_named_block_param_unchanged + res = Spoom::Sorbet::Translate.rbs_comments_to_sorbet_sigs(<<~RUBY, file: "test.rb") + # typed: true + class Foo + #: (String) ?{ (String) -> void } -> String + def bar(request, &block); end + end + RUBY + + assert_equal(<<~RUBY, res) + # typed: true + class Foo + sig { params(request: String, block: ::T.nilable(::T.proc.params(arg0: String).void)).returns(String) } + def bar(request, &block); end + end + RUBY + end end end end