-
-
Notifications
You must be signed in to change notification settings - Fork 102
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
Combination of option hash and keyword arguments causes crashes #522
Comments
Thanks for reporting, and apologies that it took long to process. Apparently, there are two issues. One is in def has_kw_args_in?(args)
Hash === args.last &&
could_contain_kw_args?(args) &&
- (args.last.empty? || args.last.keys.any? { |x| x.is_a?(Symbol) })
+ (args.last.empty? || args.last.keys.any? { |x| x.is_a?(Symbol) }) &&
+ (args.last.keys & @allowed_kw_args).any?
end Except that it's not correct. When the last argument is passed in curly braces, it should not be treated as kwargs. > def my_func(x, y, options={}, kw: false)
> puts options, kw
> end
> my_func(1,2, foo: 1, kw: 1) # ArgumentError: unknown keyword: :foo
> my_func(1,2, {foo: 1, kw: 1})
{:foo=>1}
false We really have a way to distinguish between the two invocation options with So, the fix would be to employ it in def has_kw_args_in?(args)
Hash === args.last &&
Hash.ruby2_keywords_hash?(args.last) &&
could_contain_kw_args?(args) &&
(args.last.empty? || args.last.keys.any? { |x| x.is_a?(Symbol) })
end But this is not sufficient.
even though And this statement (from #366): # If the last argument is Hash, Ruby will treat only symbol keys as keyword arguments
# the rest will be grouped in another Hash and passed as positional argument. doesn't seem to be correct, at least in Ruby 3.0. Keep in mind, Would you like to dive deeper in this and send a PR? |
I can try to give it a shot but I haven't spent a lot of time in the guts of RSpec and I find it pretty confusing. :) I'll have time in a couple of weeks as things slow down at work, but if anyone is more familiar with this part of the code we may be able to get a faster / more useful fix earlier. |
Thanks very much for the direction! I will try to get to this in a week or two! |
@pirj I've started looking into it and I'm not sure we can use the Adding those fixes above makes this test case work, but only because AFAICT Unfortunately I don't think there is a good generic way of doing this other than actually checking the Ruby version as well as the ruby2_keywords status of the hash, because Ruby 2.7 and 3.0 just behave completely differently by default. |
Maybe this is something we'd need to introduce to the |
If there is no other option, that's fine.
In which cases?
I can't recall what the exact case was, but I debugged and for
Let's make it work first, we can shuffle things around as a final step. |
Hmm... the case that's failing is here: https://github.com/rspec/rspec-support/blob/main/spec/rspec/support/method_signature_verifier_spec.rb#L667 According to this, when passing |
The cases where it behaves differently are where you have an optional hash as the last argument as well as keyword hashes. If you pass a hash into that method: Ruby 3 default: Treat the hash as the last optional positional argument. if I'm understanding this correctly. Also, |
Could it be that whatever process in |
Can this be because PS edit |
I'm not sure. It seems like as long as some method is defined with ruby2_keywords it flags the hash with a special flag that should get passed around. And I'm not sure if that special method actually exists in the guts of whatever RSpec is doing to run these specific tests. |
The logic for accepting a hash as an argument or kwargs differs not only between Ruby 2 and Ruby 3, but what's unfortunate it's also different in Ruby 2.7, see https://eregon.me/blog/2019/11/10/the-delegation-challenge-of-ruby27.html |
Looks like this would have to turn into a matrix test for all 3 versions of Ruby (< 2.7, 2.7, >= 3.0) and we'd have to try all combinations. Fun! 😄 |
OK, I've been fighting with this for a while and I'm not sure this is actually solvable as written right now. 😢 The main change with Ruby 3.0 is that it differentiates between passing a hash and passing keyword arguments to a method with variable or optional keyword parameters. So def my_method(**kwargs); end
my_method(k: 1) # fine
my_method({k: 1}) # crashes The problem is that we're trying to validate the method signature based on the arguments that are passed in, to a method ( In order to make this actually work, So this has turned from a simple fix into a fairly significant rewrite @pirj thoughts? |
We did do this conditionally in other places and it didn't work as it had slightly different semantics, the Ruby core team told us that the way to do it was ruby2_keywords and co |
There's no such a thing as a simple fix when delegation and kwargs are involved, and multiple Ruby version support is a requirement 😄 |
@JonRowe the problem is that What would your recommendation be here? |
@JonRowe bump? |
But we're talking about
On Ruby 3.1, it will fail. But if you add ruby2_keywords def ours(*args) it would work just fine and will output |
Sorry, it's been a while since I looked at this. But I think the problem is that we are trying to inspect the |
Returning to the original problem with |
I can definitely do that. But that was the original approach I took, and which I couldn't figure out. I think rewriting the |
@pirj Added the test case. |
Any update to this? |
Proposing this as a fix, #594 |
Subject of the issue
When verified doubles are turned on, a particular combination of method arguments causes RSpec to crash - it gets confused with the method signature.
Your environment
Steps to reproduce
Run the following file:
Expected behavior
The spec should pass.
Actual behavior
Fails with the following error:
Note that if you replace the call by explicitly calling the keyword argument and not relying on the default, the spec passes:
MyModule.my_func(1, 2, { foo: 'bar', baz: 'eggs' }, kw: false)
The text was updated successfully, but these errors were encountered: