-
-
Notifications
You must be signed in to change notification settings - Fork 102
Recognize hash as optional arg when optional keyword is present #366
Recognize hash as optional arg when optional keyword is present #366
Conversation
Great start at fixing rspec-mocks#1266 I started trying to write a spec for it but got distracted. What I would like to see here though is this being seperate cases demonstrating it in addition to our existing examples, as I want to ensure we don't regress other paths |
Thanks, I will rework the specs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JonRowe I've updated the PR. I've used the specs from a method with optional keyword arguments
as a base and worked them out to handle the case with an optional argument.
During testing, I found that Ruby will treat each symbol key as a keyword argument if the method signature contains an optional keyword args. It will treat non-symbol arguments as a separate Hash. These are the reasons that I've worked the code in could_contain_kw_args?
and split_args
. See below some code samples how Ruby treats the hash arguments:
def foo(x, y = {}, z: 1) puts "#{y}, #{z}"; end
foo 1, 'a' => 2, 'b' => 2
{"a"=>2, "b"=>2}, 1
foo 1, 'a' => 2, :z => 3
{"a"=>2}, 3
foo 1, 'a' => 2, :b => 3
ArgumentError (unknown keyword: b)
foo 1, 'a' => 2, :b => 3, :z => 4
ArgumentError (unknown keyword: b)
foo 1, z: 3
{}, 3
foo 'a' => 1, 'b' => 2, z: 3
{}, 1
Waiting for your comments and possible edge cases and platforms that might behave differently.
@@ -85,6 +85,10 @@ def has_kw_args_in?(args) | |||
# contain keyword arguments? | |||
def could_contain_kw_args?(args) | |||
return false if args.count <= min_non_kw_args | |||
if args.count <= max_non_kw_args && Hash === args.last |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I've added Hash === args.last
is that we use could_contain_kw_args?
in https://github.com/rspec/rspec-support/blob/master/lib/rspec/support/method_signature_verifier.rb#L383 and I'm guarding against the matcher.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough, this could be inlined into one if though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No major feedback, just a few nits
@@ -85,6 +85,10 @@ def has_kw_args_in?(args) | |||
# contain keyword arguments? | |||
def could_contain_kw_args?(args) | |||
return false if args.count <= min_non_kw_args | |||
if args.count <= max_non_kw_args && Hash === args.last |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough, this could be inlined into one if though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JonRowe I've applied the style changes.
Given a method foo(a, b = {}, x: false) it should recognize that calling foo(1, 'a' => 1, 'b' => 2) is a valid method call. In case when the method has optional arguments and keyword arguments the ruby interpreter will treat the last Hash as keyword arguments if it contains Symbol keys. In the example above calling foo(1, a: 1) will raise an ArgumentError.
If you pass key-value pairs to a method only the keys that are Symbols will be treated as keyword arguments. If there are optional arguments the rest of the keys will be passed to the optional one. Given a method foo(x = {}, y: 1): * foo('a' => 1) => will work * foo('a' => 1, :y => 2) => will work * foo('a' => 1, :b => 2) => will raise ArgumentError unknown keyword: b
@JonRowe, not sure if you have received notification, everything is passing now. |
@@ -85,6 +85,10 @@ def has_kw_args_in?(args) | |||
# contain keyword arguments? | |||
def could_contain_kw_args?(args) | |||
return false if args.count <= min_non_kw_args | |||
return false if args.count <= max_non_kw_args && | |||
Hash === args.last && | |||
args.last.keys.none? { |x| x.is_a?(Symbol) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did this exceed the line length/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, 125 characters in total.
last.keys | ||
else | ||
args << non_kw_args | ||
last.select { |k, _| k.is_a?(Symbol) }.keys |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you saying with this that Ruby will mix keyword args and hash keys?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you provide an anonymous Hash as the last argument only the symbols from that argument will be treated as keyword arguments, the rest will be interpreted as positional one.
def foo(x = {}, y:); puts "#{x}, #{y}"; end
foo 'a' => 1, y: 2
=> {"a"=>1}, 2
foo x: 3, y: 2
=> ArgumentError (unknown keyword: x)
def bar(x:, y:); end
bar 'x' => 1
=> ArgumentError (wrong number of arguments (given 1, expected 0; required keywords: x, y))
bar x: 1, y: 2, z: 3
=> ArgumentError (unknown keyword: z)
I'm trying to cover the behavior in this spec
Sorry for the delay @edzhelyov my cognitive load has been a bit too high to deal with this until now |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JonRowe no problem, review whenever you can, I'm grateful that you are helping me with moving that forward.
I've moved the symbol arguments logic into has_kw_args_in?
, seems a more appropriate place. What do you think?
@@ -85,6 +85,10 @@ def has_kw_args_in?(args) | |||
# contain keyword arguments? | |||
def could_contain_kw_args?(args) | |||
return false if args.count <= min_non_kw_args | |||
return false if args.count <= max_non_kw_args && | |||
Hash === args.last && | |||
args.last.keys.none? { |x| x.is_a?(Symbol) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, 125 characters in total.
last.keys | ||
else | ||
args << non_kw_args | ||
last.select { |k, _| k.is_a?(Symbol) }.keys |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you provide an anonymous Hash as the last argument only the symbols from that argument will be treated as keyword arguments, the rest will be interpreted as positional one.
def foo(x = {}, y:); puts "#{x}, #{y}"; end
foo 'a' => 1, y: 2
=> {"a"=>1}, 2
foo x: 3, y: 2
=> ArgumentError (unknown keyword: x)
def bar(x:, y:); end
bar 'x' => 1
=> ArgumentError (wrong number of arguments (given 1, expected 0; required keywords: x, y))
bar x: 1, y: 2, z: 3
=> ArgumentError (unknown keyword: z)
I'm trying to cover the behavior in this spec
Thanks so much for your hard work! |
…ument Recognize hash as optional arg when optional keyword is present
…ecognize-hash-as-optional-argument Recognize hash as optional arg when optional keyword is present --- This commit was imported from rspec/rspec-support@9601929.
…ecognize-hash-as-optional-argument Recognize hash as optional arg when optional keyword is present --- This commit was imported from rspec/rspec-support@9601929.
I hit an issue in one of the projects that I work that I've reported in rspec-mocks#1266.
In short, given
foo(a, b = {}, x: false)
should recognize a valid method call with args:foo(1, a: 1, b: 2)
.I've opted to extend the spec for optional keywords. I'm feeling lost in the specs, so please, give me some pointers if you need the specs re-worked.
I'm OK if you want to change the implementation or try to catch edge cases involving splats.