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

Ruby 3.0.3 and keyword arguments #1350

Open
enthusiasmus opened this issue Jan 26, 2022 · 9 comments
Open

Ruby 3.0.3 and keyword arguments #1350

enthusiasmus opened this issue Jan 26, 2022 · 9 comments

Comments

@enthusiasmus
Copy link

Subject of the issue

When using ruby 3.0.3 manually defined chained rspec matchers with keyword parameters don't forwards the arguments correctly

Your environment

  • Ruby version: 3.0.3
  • rspec-expectations version: 3.10.2

Description

Checkout the file: https://github.com/rspec/rspec-expectations/blob/main/lib/rspec/matchers/dsl.rb
Go to row 304 and 305, they currently look like this:

          define_user_override(method_name, definition) do |*args, &block|
            super(*args, &block)

and accordingly to this explanation for ruby 3 they should look like

          define_user_override(method_name, definition) do |*args, **kwargs, &block|
            super(*args, **kwargs, &block)

Am I overseeing something?
Thanks in advance!
Best regards,
Lukas

@pirj
Copy link
Member

pirj commented Jan 26, 2022

don't forwards the arguments correctly

Can you please provide a reproducible example?

and accordingly to this explanation for ruby 3

Check this an this for more insight.

@JonRowe
Copy link
Member

JonRowe commented Jan 26, 2022

Ruby keyword arguments are tricky, there is no correct way to capture them and maintain compatibility with Ruby 2, the best approach is to add ruby2_keywords to methods that might be passed keyword arguments, which I thought we'd mostly done, to retain the older behaviour that exposed them as a hash, using **kwargs internally isn't possible for us with this version of RSpec as we support older versions of Ruby that don't understand that syntax, and in any case doesn't replicate the ruby 2 behaviour entirely (we've tried conditionally applying that syntax and it doens't behave the same).

In any case can you provide a snippet of how you encountered this issue (not our code, but your code that triggered it) because in many cases if you're creating a custom matcher or using blocks you can work around this by simply capturing the keywords using **kwargs yourself

@dblock
Copy link
Contributor

dblock commented Feb 27, 2022

Could this be the cause of rspec/rspec-mocks#1505?

@13k
Copy link

13k commented Mar 15, 2022

spec/support/kwargs_chain_matcher.rb

RSpec::Matchers.define :work_with_ruby3_kwargs do
  match do |actual|
    true
  end

  chain :but do |value, does: true|
    true
  end
end

spec/spec_helper.rb

require_relative "support/kwargs_chain_matcher"
# ...

spec/kwargs_chain_spec.rb

RSpec.describe "ruby 3 kwargs chain matcher" do
  it "should work" do
    expect("kwargs chain matcher").to work_with_ruby3_kwargs.but("it", does: :not)
  end
end
bundle exec rspec spec/kwargs_chain_spec.rb
F

Failures:

  1) ruby 3 kwargs chain matcher should work
     Failure/Error:
       chain :but do |value, does: true|
         true
       end
     
     ArgumentError:
       wrong number of arguments (given 2, expected 1)
     # ./spec/support/kwargs_chain_matcher.rb:8:in `block (2 levels) in <top (required)>'
     # ./spec/kwargs_chain_spec.rb:5:in `block (2 levels) in <top (required)>'

Finished in 0.0034 seconds (files took 0.13517 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/kwargs_chain_spec.rb:4 # ruby 3 kwargs chain matcher should work

@13k
Copy link

13k commented Mar 15, 2022

capturing **kwargs in the proc also doesn't work:

  chain :also_capturing_but do |value, **kwargs|
    true
  end
  context "when capturing kwargs" do
    it "should work" do
      expect("kwargs chain matcher").to work_with_ruby3_kwargs.also_capturing_but("it", does: :not)
    end
  end
  2) ruby 3 kwargs chain matcher when capturing kwargs should work
     Failure/Error:
       chain :also_capturing_but do |value, **kwargs|
         true
       end
     
     ArgumentError:
       wrong number of arguments (given 2, expected 1)
     # ./spec/support/kwargs_chain_matcher.rb:12:in `block (2 levels) in <top (required)>'
     # ./spec/kwargs_chain_spec.rb:10:in `block (3 levels) in <top (required)>'

@johncarney
Copy link

johncarney commented Mar 29, 2023

FWIW, I'm encountering this problem as I am trying to migrate a number of apps to Ruby 3 due to the imminent EOL of Ruby 2. In the short term, I'm probably going to monkey-patch around it as follows, but it's not ideal, obviously.

module RSpec
  module Matchers
    module DSL
      module Macros
        def chain(method_name, *attr_names, &definition)
          unless block_given? ^ attr_names.any?
            raise ArgumentError, "You must pass either a block or some attribute names (but not both) to `chain`."
          end

          definition = assign_attributes(attr_names) if attr_names.any?

          define_user_override(method_name, definition) do |*args, **kwargs, &block|
            super(*args, **kwargs, &block)
            @chained_method_clauses.push([method_name, [*args, kwargs]])
            self
          end
        end
      end
    end
  end
end

I haven't properly tested the above, so it might break on certain use-cases.

@pirj
Copy link
Member

pirj commented Mar 30, 2023

A quick fix for both examples with chain above is to add this at dsl.rb:310:

            define_user_override(method_name, definition) do |*args, &block|
              super(*args, &block)
              @chained_method_clauses.push([method_name, args])
              self
            end
+           ruby2_keywords method_name if respond_to?(:ruby2_keywords, true)
          end

A PR is welcome with this and a spec covering the case with chain.

@mathieujobin
Copy link

@JonRowe

... internally isn't possible for us with this version of RSpec as we support older versions of Ruby that don't understand that syntax, ...

it should be able to support it if we drop support for 2.6 and below, keeping 2.7 for another year.

I'm not sure those are still all needed.

- 2.6
- 2.5
- 2.4
- 2.3
- 2.2

Also maybe its time to bump

s.required_ruby_version = '>= 1.8.7' # rubocop:disable Gemspec/RequiredRubyVersion

@JonRowe
Copy link
Member

JonRowe commented May 2, 2023

@mathieujobin our plans are to do just that in the next major version of RSpec, but theres a few things to organise to get that onde.

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

No branches or pull requests

7 participants