Summary
Spoom::Sorbet::Translate.rbs_comments_to_sorbet_sigs translates each #: line preceding a method into a separate sig do … end block. For methods that use overloaded RBS annotations (multiple #: lines on a single method — a valid and common RBS pattern, e.g. for the with-block / without-block split on enumerable each methods), this produces two sig calls back-to-back. When the translated source is then loaded under sorbet-runtime — which is what tapioca's tapioca/rbs/rewriter.rb does via RequireHooks.source_transform during bin/tapioca gem — T::Private::Methods._declare_sig_internal raises:
RuntimeError: You called sig twice without declaring a method in between
…and gem load aborts. This makes bin/tapioca gem <name> fail for any gem that uses overloaded #: annotations. The one I hit it on is herb (see Herb::DiffResult#each, lines 17-18).
Minimal reproducer
Standalone — only spoom and sorbet-runtime, no tapioca or herb required:
require "spoom"
require "sorbet-runtime"
source = <<~RUBY
# typed: true
class Foo
#: () { (Integer) -> void } -> void
#: () -> Enumerator[Integer, void]
def each(&block)
return [1, 2, 3].each unless block
[1, 2, 3].each(&block)
end
end
RUBY
translated = Spoom::Sorbet::Translate.rbs_comments_to_sorbet_sigs(source, file: "(repro)")
puts translated
Module.include(T::Sig)
Object.class_eval(translated, "(repro-translated)")
Spoom's output
# typed: true
class Foo
sig { params(block: ::T.proc.params(arg0: Integer).void).void }
sig { returns(::T::Enumerator[Integer, void]) }
def each(&block)
return [1, 2, 3].each unless block
[1, 2, 3].each(&block)
end
end
Runtime failure
RuntimeError: You called sig twice without declaring a method in between
from sorbet-runtime/lib/types/private/methods/_methods.rb:55:in '_declare_sig_internal'
from sorbet-runtime/lib/types/private/methods/_methods.rb:40:in 'declare_sig'
from sorbet-runtime/lib/types/sig.rb:28:in 'T::Sig#sig'
from (repro-translated):4
Why this matters
Tapioca's rbs/rewriter.rb installs the RequireHooks transform globally for **/*.rb during gem RBI generation. Any gem in the bundle that uses overloaded #: annotations will abort tapioca's gem load. The blast radius scales with RBS adoption — and overloads are exactly the case where RBS is most expressive vs. Sorbet's single-sig syntax.
Concrete real-world example: I'm trying to migrate packwerk from better_html (deprecated) to herb, and bin/tapioca gem herb fails on load with the trace above. The relevant herb source is one block-overloaded each — a textbook RBS overload pattern.
Possible remediations
A few options for RBSCommentsToSorbetSigs, roughly in order of "amount of work":
- Emit only the last sig when a method has multiple
#: annotations. This matches Sorbet's existing "last sig wins" behavior for overloaded sigs (see Shopify/tapioca#1789). Loses overload fidelity in the rewritten source but unblocks gem load.
- Raise
Spoom::Sorbet::Translate::Error when overloads are detected. Tapioca's rewriter already rescues this and falls back to the original source — so the #: lines would stay as inert comments and the gem would load without runtime sigs. Tapioca's generator would produce an untyped (but otherwise valid) RBI.
- Collapse the overloads into a single
sig with appropriately-typed params/returns (e.g. accept T.nilable(T.proc...) for the optional block; return type becomes T.any(T::Enumerator[...], void)). Closest in spirit to the original RBS but a substantial implementation lift.
(1) seems like the smallest viable fix and would unblock everyone immediately; (2) is even smaller but produces less useful RBIs. Happy to send a PR for whichever route you'd prefer.
Environment
- spoom 1.7.13
- sorbet-runtime 0.6.13184
- tapioca 0.19.1 (downstream)
- Ruby 3.4.9
Summary
Spoom::Sorbet::Translate.rbs_comments_to_sorbet_sigstranslates each#:line preceding a method into a separatesig do … endblock. For methods that use overloaded RBS annotations (multiple#:lines on a single method — a valid and common RBS pattern, e.g. for the with-block / without-block split on enumerableeachmethods), this produces twosigcalls back-to-back. When the translated source is then loaded undersorbet-runtime— which is what tapioca'stapioca/rbs/rewriter.rbdoes viaRequireHooks.source_transformduringbin/tapioca gem—T::Private::Methods._declare_sig_internalraises:…and gem load aborts. This makes
bin/tapioca gem <name>fail for any gem that uses overloaded#:annotations. The one I hit it on isherb(seeHerb::DiffResult#each, lines 17-18).Minimal reproducer
Standalone — only
spoomandsorbet-runtime, no tapioca or herb required:Spoom's output
Runtime failure
Why this matters
Tapioca's
rbs/rewriter.rbinstalls theRequireHookstransform globally for**/*.rbduring gem RBI generation. Any gem in the bundle that uses overloaded#:annotations will abort tapioca's gem load. The blast radius scales with RBS adoption — and overloads are exactly the case where RBS is most expressive vs. Sorbet's single-sig syntax.Concrete real-world example: I'm trying to migrate packwerk from
better_html(deprecated) toherb, andbin/tapioca gem herbfails on load with the trace above. The relevant herb source is one block-overloadedeach— a textbook RBS overload pattern.Possible remediations
A few options for
RBSCommentsToSorbetSigs, roughly in order of "amount of work":#:annotations. This matches Sorbet's existing "last sig wins" behavior for overloaded sigs (see Shopify/tapioca#1789). Loses overload fidelity in the rewritten source but unblocks gem load.Spoom::Sorbet::Translate::Errorwhen overloads are detected. Tapioca's rewriter already rescues this and falls back to the original source — so the#:lines would stay as inert comments and the gem would load without runtime sigs. Tapioca's generator would produce an untyped (but otherwise valid) RBI.sigwith appropriately-typed params/returns (e.g. acceptT.nilable(T.proc...)for the optional block; return type becomesT.any(T::Enumerator[...], void)). Closest in spirit to the original RBS but a substantial implementation lift.(1) seems like the smallest viable fix and would unblock everyone immediately; (2) is even smaller but produces less useful RBIs. Happy to send a PR for whichever route you'd prefer.
Environment