-
Notifications
You must be signed in to change notification settings - Fork 5.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Module#ruby2_keywords for passing keyword arguments through regular splats #2449
Conversation
Does
Can there be situations where one would want to convert for one call site but not another? |
aadcbe1
to
e0677b7
Compare
Not directly. Calls to methods that do not accept keyword arguments (e.g. pass_keywords def foo(*args)
bar(*args)
end
def bar(*args, **kw)
end
foo({a: 1}) You will get a warning when However, this does fix the warning if you do
This only changes behavior to treat the final positional argument as keywords for call sites meeting all of the following:
Possibly, and this may not be able to handle every possible case. This should hopefully handle 99% of the delegating argument cases, until people no longer need to support Ruby <2.7. Since first posted, I fixed an issue where passing empty keyword splat would not work correctly. When using During testing, I found an issue where methods created with |
…r splats In Ruby <=2.6, delegation was often done using: ```ruby def foo(*args, &block) bar(*args, &block) end ``` With the keyword argument separation changes recently added, this now causes a warning if bar accepts keyword arguments, as that will break in Ruby 3. You need to switch to delegating keyword arguments: ```ruby def foo(*args, **kw, &block) bar(*args, **kw, &block) end ``` This works and fixes the delegation. However, it is not backwards compatible with older versions of Ruby, as if bar does not accept keyword arguments, it will be passed an empty positional hash if no keywords are passed to foo. Also, there will be a hash to keyword warning if no keywords are passed to foo, and the final positional argument is a hash, even though behavior in terms of arguments passed to bar will not change in Ruby 3, as the keyword will be converted back to a hash. For backwards compatibility with older versions of Ruby, this allows you do to: ```ruby class Module private def pass_keywords(*) end unless respond_to?(:pass_keywords, true) end class Foo def foo(*args, &block) bar(*args, &block) end pass_keywords :foo end ``` Then if you call foo with keyword arguments, bar will be called with keywords arguments, but if you call foo with a positional hash argument, bar will be called with a positional hash argument. If bar accepts keyword arguments, the hash will be converted to keywords in bar, and a warning will be emitted, as behavior will change in Ruby 3 to pass the hash as a positional argument. This implements such support using a VM frame flag. If a method has been flagged as passing keywords (only allowed for methods that accept an argument splat and do not accept keywords), and it is called with keywords, it sets the flag on the next VM frame. If that frame flag is currently set, calls with an argument splat and no keywords where the last argument is a hash are converted into passing the hash as keywords.
Empty keyword hashes are normally removed, but that breaks the passing of empty keyword hashes when pass_keywords is used. When pass_keywords is used, empty keyword hashes will still be present in args. This allows the target method to correctly receive empty keywords passsed to the delegate method. Add a bunch more tests for different types of methods. These tests found a couple issues. The main issue is that this code didn't work correctly with cfuncs, because of the way cfuncs checked for empty keyword splats. To handle that correctly, change CALLER_SETUP_ARG so that it doesn't remove empty keyword splats, and move the removing of empty keyword splats to CALLER_REMOVE_EMPTY_KW_SPLAT. Some code paths will now call both methods, others will only call CALLER_SETUP_ARG. The cfunc code can then check for the removal of a keyword splat by checking calling->kw_splat after it is added by CALLER_SETUP_ARG and before it is removed by CALLER_REMOVE_EMPTY_KW_SPLAT.
e0677b7
to
71debd2
Compare
I'm confused about the proposed solution: class Foo
def foo(*args, &block)
bar(*args, &block)
end
ruby2_keywords :foo if respond_to?(:ruby2_keywords, true)
end Will that work in Ruby 3? I would assume not, because you mentioned earlier that What is the complete code for delegation, that works on 2.6, 2.7 and 3.0, without warnings? |
I will shortly be submitting a pull request that switches the approach used from a VM frame flag to a hash object flag, which is less invasive and handles some more common cases, such as storing the args in one method (that uses |
The hash-flag approach in #2477 was merged instead of this. |
In Ruby <=2.6, delegation was often done using:
With the keyword argument separation changes recently added, this now
causes a warning if bar accepts keyword arguments, as that will break
in Ruby 3. You need to switch to delegating keyword arguments:
This works and fixes the delegation. However, it is not backwards
compatible with older versions of Ruby, as if bar does not accept
keyword arguments, it will be passed an empty positional hash if
no keywords are passed to foo. Also, there will be a hash to
keyword warning if no keywords are passed to foo, and the final
positional argument is a hash, even though behavior in terms of
arguments passed to bar will not change in Ruby 3, as the keyword
will be converted back to a hash.
For backwards compatibility with older versions of Ruby, this
allows you do to:
Then if you call foo with keyword arguments, bar will be called with
keywords arguments, but if you call foo with a positional hash
argument, bar will be called with a positional hash argument. If bar
accepts keyword arguments, the hash will be converted to keywords
in bar, and a warning will be emitted, as behavior will change in
Ruby 3 to pass the hash as a positional argument.
This implements such support using a VM frame flag. If a method has
been flagged as passing keywords (only allowed for methods that
accept an argument splat and do not accept keywords), and it is
called with keywords, it sets the flag on the next VM frame. If that
frame flag is currently set, calls with an argument splat and no
keywords where the last argument is a hash are converted into
passing the hash as keywords.
The second commit uses pass_keywords in lib/delegate to fix keyword
argument separation warnings when the target method accepts keywords
and keywords are passed to the delegate method.