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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent custom matcher is called twice without compound #1428

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

AlecksJohannes
Copy link

@AlecksJohannes AlecksJohannes commented Aug 13, 2023

Hey lovely folks 馃憢, I hope you all are doing well

This PR here is to address #1426 which is to prevent a custom matcher from calling twice without compound like .and or .or

@AlecksJohannes AlecksJohannes force-pushed the prevent_custom_matcher_called_twice branch 2 times, most recently from 3c736f1 to 8689f8e Compare August 13, 2023 15:05
@JonRowe
Copy link
Member

JonRowe commented Aug 16, 2023

Thanks for starting to tackle this, the issue as I see it is somehow this method is being responded to by the matcher, this is what needs to be fixed .matcher().matcher should cause a natural NoMethodError e.g.

     NoMethodError:
       undefined method `eq' for #<RSpec::Matchers::BuiltIn::Eq:0x00000001111948f0 @expected="ok">

               expect(ok).to eq(ok).eq

@AlecksJohannes AlecksJohannes force-pushed the prevent_custom_matcher_called_twice branch from 9f60afd to 269e7e1 Compare August 16, 2023 08:10
@AlecksJohannes
Copy link
Author

AlecksJohannes commented Aug 16, 2023

Thanks for starting to tackle this, the issue as I see it is somehow this method is being responded to by the matcher, this is what needs to be fixed .matcher().matcher should cause a natural NoMethodError e.g.

     NoMethodError:
       undefined method `eq' for #<RSpec::Matchers::BuiltIn::Eq:0x00000001111948f0 @expected="ok">

               expect(ok).to eq(ok).eq

Hello @JonRowe thank you, sure I can raise NoMethodError rather than using RSpec.warning. WDYT ?

P/S: #1428 (comment)

@JonRowe
Copy link
Member

JonRowe commented Aug 17, 2023

Hello @JonRowe thank you, sure I can raise NoMethodError rather than using RSpec.warning. WDYT ?

The fix should be to find where the method is being responded to and prevent it, I suspect its probably including a module it shouldn't do.

@AlecksJohannes
Copy link
Author

Hello @JonRowe thank you, sure I can raise NoMethodError rather than using RSpec.warning. WDYT ?

The fix should be to find where the method is being responded to and prevent it, I suspect its probably including a module it shouldn't do.

Hey there JonRowe, sorry I have updated my comment above.

@AlecksJohannes
Copy link
Author

AlecksJohannes commented Aug 17, 2023

I'll paste it here just to be sure

I believe I understand what you said and I agree with you too. I tried to do that but I am not sure about removing or adjusting this piece of code, I played around with our code before (the method was coming from include RSpec::Matchers in DSL::Matcher, I was able to extract it to different module) and make it to work exactly like you said but we need to deal with 馃憞

def respond_to_missing?(method, include_private=false)
   super || @matcher_execution_context.respond_to?(method, include_private)
end

def method_missing(method, *args, &block)
   if @matcher_execution_context.respond_to?(method)
      @matcher_execution_context.__send__ method, *args, &block
   else
       super(method, *args, &block)
   end
end

Otherwise, even calling matcher().matcher it will still work even though the method does not exist. Because of this blocker, I had to provide another solution

P/S: If we were somehow to adjust that method_missing I think you won't be able to call that custom matcher recursively which I think resulting in a breaking changes.

@pirj
Copy link
Member

pirj commented Sep 18, 2023

What concerns me the most, why are matchers available as methods on an i stance of a matcher in the first place?

@AlecksJohannes
Copy link
Author

AlecksJohannes commented Sep 18, 2023

Hello there @pirj 馃憢 I am not really sure I am not sure (maybe it could be recursive?) if I should do like what Jon said because it can result in breaking changes

This is what I found:

I checked and the matcher available because of this

https://github.com/rspec/rspec-expectations/blob/main/lib/rspec/matchers/dsl.rb#L75 here we call define_method name in Matchers::DSL

and later in RSpec::Matchers we https://github.com/rspec/rspec-expectations/blob/main/lib/rspec/matchers.rb#L242 extend Matchers::DSL

And then in class RSpec::Matchers::DSL::Matcher class we include RSpec::Matchers which has that defined method again https://github.com/rspec/rspec-expectations/blob/main/lib/rspec/matchers/dsl.rb#L438. Because of this circular mixins causes that custom matcher method available like everywhere, I tried to extract the code into different module and include it but still it doesn't work due to 馃憞

def respond_to_missing?(method, include_private=false)
   super || @matcher_execution_context.respond_to?(method, include_private)
end

def method_missing(method, *args, &block)
   if @matcher_execution_context.respond_to?(method)
      @matcher_execution_context.__send__ method, *args, &block
   else
       super(method, *args, &block)
   end
end

Even though RSpec::Matchers::DSL::Matcher does not have such method but it will call the method from @matcher_execution_context because of method_missing logic

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.

None yet

3 participants