Skip to content

Fix anonymous parameter modeling and merging#624

Merged
Morriar merged 4 commits into
mainfrom
at-fix-anonymous-params
Jul 17, 2026
Merged

Fix anonymous parameter modeling and merging#624
Morriar merged 4 commits into
mainfrom
at-fix-anonymous-params

Conversation

@Morriar

@Morriar Morriar commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes Shopify/spoom#928.

This updates RBI’s parameter model to represent Ruby anonymous parameters faithfully instead of approximating anonymity through parameter names.

Ruby supports anonymous rest, keyword rest, and block parameters:

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

Prism represents these with name == nil. RBI now preserves that information in the model instead of assigning synthetic names.

Previous behavior

Before this change, every RBI::Param had a non-nil name, and anonymity was tracked through an @anonymous ivar derived from the name:

@anonymous = name.start_with?("_")

That meant names starting with _ were treated as anonymous, even though they are still real Ruby parameter names.

However, this was name-based, not syntax-based. True Ruby anonymous parameters could not be represented directly because the model required a string name. The parser had to synthesize names instead:

def foo(*); end   # RestParam name became "*args"
def foo(**); end  # KwRestParam name became "**kwargs"
def foo(&); end   # BlockParam name became ""

Merge behavior was also somewhat surprising:

  • _-prefixed names acted as wildcards for parameters of the same kind.
  • Anonymous compatibility was all-or-nothing at the method level.
  • Mixed anonymous/named parameter lists could conflict even when individual params were otherwise compatible.
  • Merge could prefer/adopt parameter names from the other side depending on which side had signatures.
  • The result could be asymmetric depending on which tree was merged into which.

What changed

  • RBI::Param is now more abstract and no longer stores a required name.
  • Named parameter classes own non-nil names:
    • ReqParam
    • OptParam
    • KwParam
    • KwOptParam
  • Optionally-anonymous parameter classes own nilable names:
    • RestParam
    • KwRestParam
    • BlockParam
  • Anonymous params no longer carry an @anonymous ivar.
    • Anonymous-ness is derived from the parameter shape and whether the name is nil.
  • Parser preserves nil names for anonymous Ruby params:
    • *
    • **
    • &
  • Sig generation still uses printable projection names for anonymous params:
    • anonymous rest → "*"
    • anonymous kwrest → "**"
    • anonymous block → "&"
  • Sig parsing/printing handles those quoted projection names correctly.

Merge behavior

Method merging now treats anonymous params as compatible with named params only when the parameter kind is compatible.

Anonymous params are not a wildcard for arbitrary parameter shapes. They only mean “this parameter has no name.”

For example:

  • RestParam(nil) is compatible with RestParam("args")
  • KwRestParam(nil) is compatible with KwRestParam("kwargs")
  • BlockParam(nil) is compatible with BlockParam("block")
  • RestParam(nil) is not compatible with ReqParam("arg")

When one side has an anonymous parameter and the other side has a named parameter of the same kind, merge prefers the non-anonymous name.

Example:

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

merged with:

sig { params(args: Integer, kwargs: T::Hash[Symbol, String], block: T.proc.void).void }
def foo(*args, **kwargs, &block); end

produces:

sig { params(args: Integer, kwargs: T::Hash[Symbol, String], block: T.proc.void).void }
def foo(*args, **kwargs, &block); end

This also updates sig merging so anonymous sig projection names are renamed to match the final merged method parameters.

Breaking changes

This is a breaking API change for consumers of the RBI model.

Previously, callers could generally assume every parameter had a non-nil name-like value. That is no longer true:

  • RBI::Param itself no longer stores a required name.
  • RestParam#name, KwRestParam#name, and BlockParam#name can now be nil.
  • Anonymous parameters are no longer represented by synthesized names.
  • _-prefixed parameter names are no longer considered anonymous by default.
  • Code that previously compared anonymous params by placeholder names must now handle nil names or call anonymous?.

Merge behavior changed as well:

  • anonymous-vs-named params of the same kind can now merge
  • the merged method prefers the non-anonymous name
  • anonymous params are not considered compatible across different parameter kinds
  • _-prefixed named params no longer act as anonymous wildcards

This makes the model more accurate, but downstream code assuming anonymous params have stable synthetic names, or assuming _-prefixed names are anonymous, will need to be updated.

Comment thread lib/rbi/rbs_printer.rb Outdated
Comment thread lib/rbi/model.rb Outdated
@Morriar
Morriar force-pushed the at-fix-anonymous-params branch from 833bfe5 to 4d0c0b0 Compare July 16, 2026 15:53
end

class Param
# @override

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Override of what? This class has no subclass

@Morriar Morriar Jul 17, 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.

It's an override of Node#compatible_with?.

Param and the other nodes in this files are all subclasses of Node, though they are just reopenings so we didn't specify the superclass again.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

though they are just reopenings so we didn't specify the superclass again.

We should never do that. If you are reopening keep the inheritance correct.

Morriar added 2 commits July 17, 2026 09:25
Signed-off-by: Alexandre Terrasa <alexandre.terrasa@shopify.com>
Signed-off-by: Alexandre Terrasa <alexandre.terrasa@shopify.com>
@Morriar
Morriar force-pushed the at-fix-anonymous-params branch from 4d0c0b0 to be2225d Compare July 17, 2026 13:25
Morriar added 2 commits July 17, 2026 10:42
Signed-off-by: Alexandre Terrasa <alexandre.terrasa@shopify.com>
Signed-off-by: Alexandre Terrasa <alexandre.terrasa@shopify.com>
@Morriar
Morriar force-pushed the at-fix-anonymous-params branch from be2225d to 290fc6f Compare July 17, 2026 14:42
@Morriar
Morriar merged commit bf4906e into main Jul 17, 2026
11 checks passed
@Morriar
Morriar deleted the at-fix-anonymous-params branch July 17, 2026 15:04

@amomchilov amomchilov 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.

Forgot to submit my draft review. 🤦

Small comments.

end
SigParam.new(name, sig_param.type)
end
sig.params.replace(sig_params)

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.

Can use map! instead

Comment thread lib/rbi/model.rb
def initialize(name, value, loc: nil, comments: nil, &block)
super(name, loc: loc, comments: comments)
super(loc: loc, comments: comments)
@name = name.to_s #: String

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.

It's already a string, likewise below

Suggested change
@name = name.to_s #: String
@name = name

Comment thread lib/rbi/parser.rb
when Prism::SymbolNode
node.value.to_s
else
node_string!(node).delete_suffix(":")

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.

BTW This breaks for unusual sigs, like params("a": Integer)

I have a wip to fix this across the repo

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RBS Rewriting produces invalid Ruby for anonymous block parameters

4 participants