-
-
Notifications
You must be signed in to change notification settings - Fork 161
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
Support strict keyword argument matching #554
Support strict keyword argument matching #554
Conversation
0d58788
to
2a7f3b4
Compare
2a7f3b4
to
1eac886
Compare
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.
This all looks good to me so far. I've made a few minor requests - I hope they all make sense. Can you combine the two commits into one and give a bit of context/motivation in the commit note so the commit stands alone. Thanks.
|
||
module Mocha | ||
module ParameterMatchers | ||
class PositionalOrKeywordHash < Base |
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.
Open to a better name. Note that naming this Hash
caused some constant lookup failures due to namespacing e.g. in HasEntry#parse_option
, so I thought it'd be safer to name this something else (instead of checking and changing all Hash
es in the codebase to explicit reference the top-level Hash
).
Also, should this be # @private
?
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.
Open to a better name. Note that naming this
Hash
caused some constant lookup failures due to namespacing e.g. inHasEntry#parse_option
, so I thought it'd be safer to name this something else (instead of checking and changing allHash
es in the codebase to explicit reference the top-levelHash
).
That makes sense to me and I can't immediately think of a better name. 👍
Also, should this be
# @private
?
Yes, I think the whole class could be marked as private for documentation purposes since I think it's only used internally. That way you could remove the method-level YARD annotations.
44255c2
to
29dee3c
Compare
def matches?(available_parameters) | ||
parameter, is_last_parameter = extract_parameter(available_parameters) | ||
if is_last_parameter && Mocha.configuration.strict_keyword_argument_matching? | ||
return false unless ::Hash.ruby2_keywords_hash?(parameter) == ::Hash.ruby2_keywords_hash?(@value) |
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.
I'd actually prefer that we don't couple to ruby2_keywords
here, but couldn't think of a significantly better alternative yet:
- The Hash is frozen, so we can't change any instance variable on it, e.g. to assign a different attribute. An alternative might be to wrap the Hash, but I'm not sure if I'd break other assumptions in the code.
- Explicitly separating keyword arguments is tricky for now because we don't want to introduce a breaking change (e.g. hash matchers like
has_value
will still do "loose matching"). A fair amount of matching logic also relies on iterating and shifting an array of params (e.g.any_parameters
).
Would suggest to revisit if/when we can turn on strict keyword matching by default.
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.
I'd actually prefer that we don't couple to
ruby2_keywords
here, but couldn't think of a significantly better alternative yet:
Sorry, my brain's a bit fried - what are the downsides of coupling to ruby2_keywords
?
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.
Mainly this statement:
This method will probably be removed at some point, as it exists only for backwards compatibility
I'd expect that it wouldn't be removed till Ruby 2 is end-of-life, but it still gives me a bit of pause, and would be nice if we could limit it to the "edges" of the code (i.e. invocation / expectation)
@wasabigeek I've tried to respond to all your questions, but I haven't had a chance to review all the changes yet. I'll try to get to that tomorrow. |
Tested it in a repo and it works, but the printed error message doesn't correctly distinguish between positional hashes / keyword args, example: # actual code
service = Service.new(a: params[:a], b: params[:b])
# test
Service.expects(:new).with(transformed_input).returns({ a: "123", b: "US" })
# unexpected invocation: Service.new(:a => "123", :b => "US")
# unsatisfied expectations:
# - expected exactly once, invoked never: Service.new(:a => "123", :b => "US") # error message has "stripped" the hash and made it look like a keyword arg Will take a closer look. |
@wasabigeek Thanks again for all your work. I've attempted to tidy the commits up and make a few very small tweaks in a new PR, #562. Can you have a look at this and check I haven't messed anything up or missed anything important. Also can I double-check that this should be OK to release as a minor version bump, since the new behaviour is opt-in? |
It shouldn't break existing behaviour 🤞 that said I've only tested it in a Ruby 3 repo! Would you be able to take a look at wasabigeek#2 and see if it makes sense? The way kwargs are printed when strict matching is enabled is a bit confusing at the moment 😔 |
@floehopper, just pinging in case you missed the above 🙇♂️ |
Closing in favour of #562 |
Enables strict keyword argument matching in Ruby >= 2.7, as sketched out in #544 and #446 (comment). Closes #446.
TODOs: