Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,16 @@ Translate signatures from RBI to RBS comments:
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
Expand Down
16 changes: 12 additions & 4 deletions lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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} super\n#{indent}end"
else
" = raise NotImplementedError, \"Abstract method called\""
" = super#{header_comment}"
end,
)
end
Expand Down
2 changes: 1 addition & 1 deletion test/spoom/cli/srb/sigs_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ def foo; end
class A
# @abstract
#: -> void
def foo = raise NotImplementedError, "Abstract method called"
def foo = super
end
RB
end
Expand Down
82 changes: 74 additions & 8 deletions test/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -102,33 +106,95 @@ def bar=(x)
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"
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)
raise NotImplementedError, "Abstract method called"
def bar=(x) # Keep the setter comment
super
end

# @abstract
#: (Integer x, Integer y) -> void
def qux x, y # Keep the unparenthesized header comment
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
Expand Down
Loading