Repository navigation
Parameter type includes values assigned via Hash#[]= on local variable #362
Copy link
Copy link
Open
Description
Steps to reproduce
Analyze the following file with TypeProf.
def foo(options)
options[:name] = "str"
nil
end
foo(Hash.new)Expected behavior
class Object
def foo: (Hash[untyped, untyped]) -> nil
endParameter types should only reflect the types passed from call sites, not values assigned inside the method.
Actual behavior
$ bin/typeprof reproduce.rb
# TypeProf 0.31.1
# reproduce.rb
class Object
def foo: (Hash[:name, String]) -> nil
endSystem configuration
Ruby version: ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin25]
TypeProf version: 753de9b
Real-world use cases
I noticed this issue when analyzing Action Pack with TypeProf.
The minimum steps required to actually analyze Action Pack are as follows:
$ bin/typeprof tmp/rails/actionpack/test/controller/routing_test.rb \
tmp/rails/actionpack/lib/action_dispatch/routing/route_set.rb \
tmp/rails/actionpack/lib/action_dispatch/journey/formatter.rb > ap.rbs
$ cat ap.rbs | grep url_for | wc -c
13936
$ cat ap.rbs | grep url_for
def url_for: (Hash[:_recall | :action | :anchor | :controller | :domain | :foo | :format | :host | :id | :name | :only_path | :original_script_name | :params | :password | :path | :path_params | :person | :port | :protocol | :script_name | :subdomain | :tld_length | :trailing_slash | :use_route | :user | Range[Integer], (Hash[:_recall | :action | :anchor | :controller | :domain | :foo | :format | :host | :id | :name | :only_path | :original_script_name | :params | :password | :path | :path_params | :person | :port | :protocol | :script_name | :subdomain | :tld_length | :trailing_slash | :use_route | :user | Range[Integer], (Hash[:_recall | :action | :anchor | :controller | :domain | :foo | :format | :host | :id | :name | :only_path | :original_script_name | :params | :password | :path | :path_params | :person | :port | :protocol | :script_name | :subdomain | :tld_length | :trailing_slash | :use_route | :user | Range[Integer], (Hash[:_recall | :action | :anchor | :controller | :domain | :foo | :format | :host | :id | :name | :only_path | :original_script_name | :params | :password | :path | :path_params | :person | :port | :protocol | :script_name | :subdomain | :tld_length | :trailing_slash | :use_route | :user | Range[Integer], (Hash[:_recall | :action | :anchor | :controller | :domain | :foo | :format | :host | :id | :name | :only_path | :original_script_name | :params | :password | :path | :path_params | :person | :port | :protocol | :script_name | :subdomain | :tld_length | :trailing_slash | :use_route | :user | Range[Integer], (Hash[:_recall | :action | :anchor | :controller | :domain | :foo | :format | :host | :id | :name | :only_path | :original_script_name | :params | :password | :path | :path_params | :person | :port | :protocol | :script_name | :subdomain | :tld_length | :trailing_slash | :use_route | :user | Range[Integer], (Hash[:_recall | :action | :anchor | :controller | :domain | :foo | :format | :host | :id | :name | :only_path | :original_script_name | :params | :password | :path | :path_params | :person | :port | :protocol | :script_name | :subdomain | :tld_length | :trailing_slash | :use_route | :user | Range[Integer], (Hash[:_recall | :action | :anchor | :controller | :domain | :foo | :format | :host | :id | :name | :only_path | :original_script_name | :params | :password | :path | :path_params | :person | :port | :protocol | :script_name | :subdomain | :tld_length | :trailing_slash | :use_route | :user | Range[Integer], String] | Hash[:_recall | :action | :anchor | :controller | :domain | :foo | :format | :host | :id | :name | :only_path | :original_script_name | :params | :password | :path | :path_params | :person | :port | :protocol | :script_name | :subdomain | :tld_length | :trailing_slash | :use_route | :user | Range[Integer], untyped] | String)?] | Hash[:_recall | :action | :anchor | :controller | :domain | :foo | :format | :host | :id | :name | :only_path | :original_script_name | :params | :password | :path | :path_params | :person | :port | :protocol | :script_name | :subdomain | :tld_length | :trailing_slash | :use_route | :user | Range[Integer], String] | Hash[:_recall | :action | :anchor | :controller | :domain | :foo | :format | :host | :id | :name | :only_path | :original_script_name | :params | :password | ...(omittedThe parameter type contains a recursive structure such as Hash[:_recall | ..., Hash[:_recall | ..., Hash[:_recall, ...].
I have identified the cause of this as Hash#[]=.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
mame commentedon Feb 9, 2026
Hmm. I am curious what pattern of ActionPack's code is causing the problem. Give me a time to look into it.
If we fix it, we'll probably need to handle
Array#[]=similarly.sinsoku commentedon Feb 15, 2026
@mame
I stripped down the Action Pack code to create a minimal reproduction of this issue:
Running TypeProf on this produces the following RBS:
The deeply nested Hash types are caused by the parameter type pollution from
Hash#[]=.With the fix in #363, the output becomes:
sinsoku commentedon Feb 15, 2026
A patch has been implemented that also supports
Array#[]=.sinsoku@d7b9774
I plan to make a separate PR after merging #363.
mame commentedon Feb 25, 2026
Sorry for the late response. I am still considering this and want to share my current thoughts.
The current behavior is somewhat intentional. For example, in a pattern like:
I want to make TypeProf infer
def foo: (key1: Integer, key2: String) -> ....This relies on
[]=feeding type information back into the parameter's type.In the minimal reproduction you provided,
options[:params] = result[:data]is essentiallyoptions[:params] = options, so the recursive inference result is somewhat expected.That said, I understand that overly verbose inference results are a real problem in practice, especially with Rails code where hash values get passed around and mutated across many methods. I'm still thinking about how to balance the intentional backflow behavior with preventing type explosion in complex codebases.