From fc2a3fc89687127f76b48fed605fe723e2ecd026 Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Mon, 5 Oct 2026 13:43:45 -0700 Subject: [PATCH 1/2] Keep parameters and comments when translating abstract sigs --- README.md | 2 ++ .../translate/sorbet_sigs_to_rbs_comments.rb | 16 ++++++++++++---- .../sorbet_sigs_to_rbs_comments_test.rb | 18 ++++++++++++++---- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 475e426b..4a5e4b1e 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,8 @@ Translate signatures from RBI to RBS comments: spoom srb sigs translate ``` +Abstract-method translation preserves method parameters and trailing header comments. + Translate signatures from RBS comments to RBI: ```sh diff --git a/lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb b/lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb index 758ec344..65096da4 100644 --- a/lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb +++ b/lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb @@ -90,14 +90,22 @@ def visit_def_node(node) end if last_sigs.any? { |_, sig| sig.is_abstract } + header = node.rparen_loc || node.parameters&.location || node.name_loc + comment = @comments.find do |comment| + comment.location.start_line == header.end_line && + comment.location.start_offset >= header.end_offset && + comment.location.end_offset < node.location.end_offset + end + header_comment = comment ? " #{comment.location.slice}" : "" + @rewriter << Source::Replace.new( - node.rparen_loc&.end_offset || node.name_loc.end_offset, + header.end_offset, node.location.end_offset - 1, - if node.name.end_with?("=") + if node.name.end_with?("=") || (node.parameters && !node.rparen_loc) indent = " " * node.location.start_column - "\n#{indent} raise NotImplementedError, \"Abstract method called\"\n#{indent}end" + "#{header_comment}\n#{indent} raise NotImplementedError, \"Abstract method called\"\n#{indent}end" else - " = raise NotImplementedError, \"Abstract method called\"" + " = raise NotImplementedError, \"Abstract method called\"#{header_comment}" end, ) end diff --git a/test/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments_test.rb b/test/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments_test.rb index 83493327..dea6ea38 100644 --- a/test/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments_test.rb +++ b/test/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments_test.rb @@ -84,14 +84,18 @@ def bar; end end sig { abstract.params(x: Integer).void } - def baz(x) + def baz(x) # Keep the header comment end sig { abstract.params(x: Integer).void } def foo=(x); end sig { abstract.params(x: Integer).void } - def bar=(x) + def bar=(x) # Keep the setter comment + end + + sig { abstract.params(x: Integer, y: Integer).void } + def qux x, y # Keep the unparenthesized header comment end end RB @@ -112,7 +116,7 @@ def bar = raise NotImplementedError, "Abstract method called" # @abstract #: (Integer x) -> void - def baz(x) = raise NotImplementedError, "Abstract method called" + def baz(x) = raise NotImplementedError, "Abstract method called" # Keep the header comment # @abstract #: (Integer x) -> void @@ -122,7 +126,13 @@ def foo=(x) # @abstract #: (Integer x) -> void - def bar=(x) + def bar=(x) # Keep the setter comment + raise NotImplementedError, "Abstract method called" + end + + # @abstract + #: (Integer x, Integer y) -> void + def qux x, y # Keep the unparenthesized header comment raise NotImplementedError, "Abstract method called" end end From 8a6bf934c55990a076a908592984dc784fd8d7d6 Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Mon, 5 Oct 2026 13:44:02 -0700 Subject: [PATCH 2/2] Use forwarding super when translating abstract sigs --- README.md | 8 +++ .../translate/sorbet_sigs_to_rbs_comments.rb | 4 +- test/spoom/cli/srb/sigs_test.rb | 2 +- .../sorbet_sigs_to_rbs_comments_test.rb | 68 +++++++++++++++++-- 4 files changed, 73 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4a5e4b1e..cc525ec0 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,14 @@ spoom srb sigs translate Abstract-method translation preserves method parameters and trailing header comments. +Include abstract methods with `--translate-abstract-methods`: + +```sh +spoom srb sigs translate --translate-abstract-methods +``` + +Generated abstract bodies call bare `super`, forwarding arguments and blocks to an inherited implementation. This targets the new RBS abstract-method requirement in [Sorbet PR #10707](https://github.com/sorbet/sorbet/pull/10707); Sorbet versions that still require `raise` will reject these bodies. + Translate signatures from RBS comments to RBI: ```sh diff --git a/lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb b/lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb index 65096da4..5df9c7f7 100644 --- a/lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb +++ b/lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb @@ -103,9 +103,9 @@ def visit_def_node(node) node.location.end_offset - 1, if node.name.end_with?("=") || (node.parameters && !node.rparen_loc) indent = " " * node.location.start_column - "#{header_comment}\n#{indent} raise NotImplementedError, \"Abstract method called\"\n#{indent}end" + "#{header_comment}\n#{indent} super\n#{indent}end" else - " = raise NotImplementedError, \"Abstract method called\"#{header_comment}" + " = super#{header_comment}" end, ) end diff --git a/test/spoom/cli/srb/sigs_test.rb b/test/spoom/cli/srb/sigs_test.rb index 34ab6d8a..e045414d 100644 --- a/test/spoom/cli/srb/sigs_test.rb +++ b/test/spoom/cli/srb/sigs_test.rb @@ -449,7 +449,7 @@ def foo; end class A # @abstract #: -> void - def foo = raise NotImplementedError, "Abstract method called" + def foo = super end RB end diff --git a/test/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments_test.rb b/test/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments_test.rb index dea6ea38..3af06108 100644 --- a/test/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments_test.rb +++ b/test/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments_test.rb @@ -106,39 +106,95 @@ def qux x, y # Keep the unparenthesized header comment class Foo # @abstract #: -> void - def foo = raise NotImplementedError, "Abstract method called" + def foo = super class Bar # @abstract #: -> void - def bar = raise NotImplementedError, "Abstract method called" + def bar = super end # @abstract #: (Integer x) -> void - def baz(x) = raise NotImplementedError, "Abstract method called" # Keep the header comment + def baz(x) = super # Keep the header comment # @abstract #: (Integer x) -> void def foo=(x) - raise NotImplementedError, "Abstract method called" + super end # @abstract #: (Integer x) -> void def bar=(x) # Keep the setter comment - raise NotImplementedError, "Abstract method called" + super end # @abstract #: (Integer x, Integer y) -> void def qux x, y # Keep the unparenthesized header comment - raise NotImplementedError, "Abstract method called" + super end end RBS end + def test_translate_to_rbs_abstract_method_forwards_arguments_and_block + contents = <<~RB + sig do + abstract.params( + value: Integer, + rest: Integer, + label: String, + options: String, + block: T.proc.params(value: String).returns(String) + ).returns(String) + end + def render value, *rest, label:, **options, &block # Keep the header comment + end + RB + + translated = sorbet_sigs_to_rbs_comments(contents) + requirement = Module.new + requirement.module_eval(translated) + parent = Class.new do + def render(value, *rest, label:, **options, &block) + block.call("#{label}: #{([value] + rest).join(", ")}#{options.fetch(:suffix)}") + end + end + child = Class.new(parent) + child.include(requirement) + + assert_equal( + "[values: 1, 2, 3!]", + child.new.render(1, 2, 3, label: "values", suffix: "!") { |value| "[#{value}]" }, + ) + + missing_implementation = Class.new + missing_implementation.include(requirement) + assert_raises(NoMethodError) do + missing_implementation.new.render(1, label: "values", suffix: "!") { |value| value } + end + end + + def test_translate_to_rbs_abstract_setter_forwards_to_inherited_implementation + contents = <<~RB + sig { abstract.params(value: Integer).void } + def value= value # Keep the setter comment + end + RB + + requirement = Module.new + requirement.module_eval(sorbet_sigs_to_rbs_comments(contents)) + parent = Class.new { attr_accessor :value } + child = Class.new(parent) + child.include(requirement) + instance = child.new + instance.value = 42 + + assert_equal(42, instance.value) + end + def test_translate_to_rbs_skips_abstract_methods_without_runtime contents = <<~RB class Foo