Skip to content
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

Closed

Conversation

wasabigeek
Copy link
Contributor

@wasabigeek wasabigeek commented Sep 20, 2022

Enables strict keyword argument matching in Ruby >= 2.7, as sketched out in #544 and #446 (comment). Closes #446.

TODOs:

  • Add acceptance tests for existing behaviour
  • Add ruby2_keywords shim
  • add configuration
  • add ParametersMatchers::Hash and check for strict keyword arguments there
  • documentation
  • fix mocha inspect to display the correct invocation / expectation

@wasabigeek wasabigeek force-pushed the add-strict-keyword-arg-config branch from 0d58788 to 2a7f3b4 Compare September 25, 2022 14:08
@wasabigeek wasabigeek force-pushed the add-strict-keyword-arg-config branch from 2a7f3b4 to 1eac886 Compare September 25, 2022 14:26
@wasabigeek wasabigeek changed the title Spike configuration for strict keyword matching Support strict keyword argument matching Sep 25, 2022
Copy link
Member

@floehopper floehopper left a 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.

lib/mocha/configuration.rb Outdated Show resolved Hide resolved
test/unit/configuration_test.rb Show resolved Hide resolved
test/unit/configuration_test.rb Outdated Show resolved Hide resolved
test/unit/configuration_test.rb Show resolved Hide resolved

module Mocha
module ParameterMatchers
class PositionalOrKeywordHash < Base
Copy link
Contributor Author

@wasabigeek wasabigeek Sep 28, 2022

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 Hashes in the codebase to explicit reference the top-level Hash).

Also, should this be # @private?

Copy link
Member

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 Hashes in the codebase to explicit reference the top-level Hash).

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.

@wasabigeek wasabigeek marked this pull request as ready for review September 28, 2022 15:02
@wasabigeek wasabigeek force-pushed the add-strict-keyword-arg-config branch from 44255c2 to 29dee3c Compare September 28, 2022 15:30
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)
Copy link
Contributor Author

@wasabigeek wasabigeek Sep 28, 2022

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.

Copy link
Member

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?

Copy link
Contributor Author

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)

@floehopper
Copy link
Member

@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.

@wasabigeek
Copy link
Contributor Author

wasabigeek commented Sep 29, 2022

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.

@floehopper
Copy link
Member

@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?

@wasabigeek
Copy link
Contributor Author

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 😔

@wasabigeek
Copy link
Contributor Author

@floehopper, just pinging in case you missed the above 🙇‍♂️

@wasabigeek
Copy link
Contributor Author

Closing in favour of #562

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Work out whether any changes are needed for positional/keyword args in Ruby 3.0
2 participants