Skip to content

Preserve anonymous parameters in generated RBIs - #2687

Open
Morriar wants to merge 6 commits into
mainfrom
at/preserve-anonymous-parameters
Open

Preserve anonymous parameters in generated RBIs#2687
Morriar wants to merge 6 commits into
mainfrom
at/preserve-anonymous-parameters

Conversation

@Morriar

@Morriar Morriar commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Ruby reflects anonymous splat, keyword splat, and block parameters using pseudo-names:

def foo(*, **, &); end

method(:foo).parameters
# => [[:rest, :*], [:keyrest, :**], [:block, :&]]

Tapioca was treating those names as invalid parameter names and replacing them with fallback names like _arg0, _arg1, and _arg2. That caused two issues:

  • Methods were generated with named parameters instead of preserving the original anonymous syntax.
  • RBS-translated signatures could lose parameter types because Sorbet keeps the signature types under the original names: "*", "**", and "&".

For example, this could generate invalid RBI like:

sig { params(_arg0: , _arg1: , _arg2: ).void }
def foo(*_arg0, **_arg1, &_arg2); end

This change preserves anonymous parameter names for signature lookup, while rendering them as anonymous RBI parameters:

sig { params("*": ::Integer, "**": ::String, "&": T.proc.void).void }
def foo(*, **, &); end

It also bumps the minimum rbi dependency to 0.4.1, since older versions don’t support anonymous RestParam, KwRestParam, and BlockParam nodes represented with nil names.

@Morriar
Morriar requested a review from a team as a code owner July 28, 2026 21:01
Comment thread lib/tapioca/gem/listeners/methods.rb Outdated
Comment on lines +131 to +134
anonymous_parameter = anonymous_parameter_name?(type, name)
name = fallback_arg_name unless anonymous_parameter || valid_parameter_name?(name)

[type, name]
[type, name, anonymous_parameter]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
anonymous_parameter = anonymous_parameter_name?(type, name)
name = fallback_arg_name unless anonymous_parameter || valid_parameter_name?(name)
[type, name]
[type, name, anonymous_parameter]
is_anonymous_parameter = anonymous_parameter_name?(type, name)
name = fallback_arg_name unless is_anonymous_parameter || valid_parameter_name?(name)
[type, name, is_anonymous_parameter]

Comment thread lib/tapioca/gem/listeners/methods.rb Outdated
)

sanitized_parameters.each do |type, name|
sanitized_parameters.each do |type, name, anonymous_parameter|

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sanitized_parameters.each do |type, name, anonymous_parameter|
sanitized_parameters.each do |type, name, is_anonymous_parameter|

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to thread the 3rd parameter through like this?

Could we instead modify the definition of name above, where anonymous_parameter is first set?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rc

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to pass something because the method param needs to be generated with a nil name but the signature param lookup needs the "*" name.

I change the tuple to use both names instead of passing the anonymous flag 👍

@Morriar
Morriar force-pushed the at/preserve-anonymous-parameters branch from 3a3dbeb to 095ee15 Compare July 28, 2026 21:36
@Morriar Morriar self-assigned this Jul 28, 2026
@Morriar Morriar added the enhancement New feature or request label Jul 28, 2026
Comment thread lib/tapioca/gem/listeners/methods.rb Outdated
else
false
end
end

@KaanOzkan KaanOzkan Jul 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have to fix this for DSL RBIs too in

def compile_method_parameters_to_rbi(method_def)

@Morriar Morriar Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Fixed in 1a1f4cc.

I moved the anonymous parameter check into RBIHelper and reused it from the DSL compiler so create_method_from_def preserves anonymous *, **, and & parameters too.

@Morriar
Morriar force-pushed the at/preserve-anonymous-parameters branch from f928e6b to 1a1f4cc Compare July 30, 2026 19:09
@Morriar
Morriar requested review from KaanOzkan and amomchilov July 30, 2026 19:14

@KaanOzkan KaanOzkan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just requires an expectation update

@stadia

stadia commented Aug 28, 2026

Copy link
Copy Markdown

Confirming this in the wild, in case a real-world data point helps with prioritisation.

Versions: tapioca 0.19.2, rbi 0.4.3, sorbet-static-and-runtime 0.6.13427, Ruby 3.4, Rails 8.

A gem we consume defines callback macros in a Rails concern using anonymous forwarding:

module ClassMethods
  def on_like_received(method_name = nil, **, &)
    register_social_callback(:on_like_received, method_name, **, &)
  end

  def on_delete_requested(*)
    set_callback(:on_delete_requested, *)
  end

  private

  # named params — generates a correct sig
  def register_social_callback(callback_name, method_name = nil, **options, &block)
    ...
  end
end

tapioca gem <name> emits:

sig { params(method_name: T.nilable(::Symbol), _arg1: , _arg2: ).returns(T.untyped) }

and srb tc then fails on the generated RBI itself:

sorbet/rbi/gems/<gem>@0.9.0-....rbi:1364: Malformed type declaration. Unknown type syntax. Expected a ClassName or T.<func>
    1364 |  sig { params(method_name: T.nilable(::Symbol), _arg1: , _arg2: ).returns(T.untyped) }

13 errors in that app, all of them inside generated gem RBIs — no user code involved.

Two details that match your diagnosis and might be useful as test cases:

  • It is specifically the anonymous parameters that lose their type. In the snippet above, method_name keeps T.nilable(::Symbol) in the same signature, and register_social_callback — same file, same module, but with named **options / &block — generates a perfectly valid sig. So the failure is per-parameter, not per-method.
  • It affects both the positional (*) and the keyword+block (**, &) forms, and it applies to private methods too, since those are emitted into the RBI as well.

Workaround for anyone hitting this before the fix lands: post-process the generated RBI, replacing the empty type _arg\d+: with _arg\d+: T.untyped. Worth noting that tapioca gem --verify compares the set of RBI files against Gemfile.lock rather than their contents, so patched files do not break that CI check.

We briefly considered asking the gem to rename its parameters, but def m(**, &) is valid, idiomatic Ruby 3.2+, so patching the consumer's RBI layer is clearly the right side to absorb this on. Would be great to see #2687 land.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants