Skip to content
This repository has been archived by the owner on Nov 30, 2024. It is now read-only.

Implement MatcherDelegator using the BlankSlate pattern #1434

Merged
merged 1 commit into from
Nov 15, 2023

Conversation

casperisfine
Copy link

@casperisfine casperisfine commented Nov 7, 2023

This allows to minimize the amount of method inherited by the delegator object, and protects RSpec from new methods being introduced by future Ruby versions or monkey patched in by user code or libraries.

On modern Rubies there is BasicObject for this purpose, that's what delegate.rb uses for instance, and is considered best practice for delegators and other proxy objects. However since RSpec still support Ruby 1.8 so it's not an option.

Additionally BasicObject doesn't inherit from Object so all constant resolutions in a BasicObject descendant must be fully qualified which is tedious, but more importantly changing this now would probably break third party code that inherit from MatcherDelegator.

@pirj
Copy link
Member

pirj commented Nov 8, 2023

Fails even on 2.2, but we don’t care much, since Rails 7.1 is Ruby 2.7+, right?

Please don’t hesitate to ping if you need me to approve CI.

@casperisfine
Copy link
Author

Thanks @pirj ! (Actually I'm now thinking I may be able to open a PR on my fork to get CI running without approval 🤦 ).

we don’t care much, since Rails 7.1 is Ruby 2.7+, right?

Well, Rails 7.2 is what trigger the issue at hand, but the same kind of issue can be caused by applications monkey patches, etc. And ideally I'd avoid too big of a if RUBY_VERSION >=.

@casperisfine
Copy link
Author

Oh, interesting, you couldn't copy methods in modules pre 2.2:

Failure/Error: define_method(method, ::Object.instance_method(method))

TypeError:
  bind argument must be a subclass of Object

I suppose the solution is to go with the "BlankSlate" pattern for all versions, that also avoid running into constant resolution issues.

@casperisfine casperisfine force-pushed the use-basic-object-for-delegator branch from 83bdc24 to bb9fb97 Compare November 8, 2023 06:28
This allows to minimize the amount of method inherited by
the delegator object, and protects RSpec from new methods
being introduced by future Ruby versions or monkey patched
in by user code or libraries.

On modern Rubies there is `BasicObject` for this purpose,
that's what `delegate.rb` uses for instance, and is considered
best practice for delegators and other proxy objects. However
since RSpec still support Ruby 1.8 so it's not an option.

Additionally `BasicObject` doesn't inherit from `Object` so
all constant resolutions in a `BasicObject` descendant must
be fully qualified which is tedious, but more importantly
changing this now would probably break third party code that
inherit from `MatcherDelegator`.
@casperisfine casperisfine force-pushed the use-basic-object-for-delegator branch from d74e12b to 3d97c7e Compare November 8, 2023 06:46
@casperisfine casperisfine changed the title Implement MatcherDelegator by inheriting from BasicObject Implement MatcherDelegator using the BlankSlate pattern Nov 8, 2023
@casperisfine
Copy link
Author

Ok, I pushed a new version that passes CI on my fork except for the with diff-lcs job, but I suspect it's a red herring. I think it may pass here.

:__id__, :__send__, :object_id,

# Methods that are explicitly undefined in some subclasses.
:==, :===,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I had to keep these two only to not break:

    # Decorator used for matchers that have special implementations of
    # operators like `==` and `===`.
    # @private
    class AliasedMatcherWithOperatorSupport < AliasedMatcher
      # We undef these so that they get delegated via `method_missing`.
      undef ==
      undef ===
    end

I think we could remove them from BaseDelegator and just make AliasedMatcherWithOperatorSupport and empty class.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

=== is the base method matchers use to match on, so if they are explicitly undefined its likely to fall back to delegated ones

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant by this comment, is that I left them in the BlankSlate to minimize the diff.

But IMO they should be removed from the BlankSlate and AliasedMatcherWithOperatorSupport should stop undefining them.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +19 to +20
:class, :respond_to?, :__method__, :method, :dup,
:clone, :initialize_dup, :initialize_copy, :initialize_clone,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that these are the methods required to pass the test suite, they don't all necessarily make sense to keep. e.g. method for instance is redundant with __method__ and a cleanup of the test suite would allow to get rid of it.

@casperisfine
Copy link
Author

Also, I'm unfamiliar with RSpec, but if there are other delegators of the same type, it may make sense to proactively convert them to a similar pattern.

@byroot
Copy link
Contributor

byroot commented Nov 8, 2023

the with diff-lcs job, but I suspect it's a red herring.

So it fails on that branch too, but I still think it's unrelated.

/active_support/core_ext/time/conversions.rb:62:in `<class:Time>': undefined method `deprecator' for ActiveSupport:Module (NoMethodError)
	from /home/runner/work/rspec-expectations/rspec-expectations/bundle/ruby/2.7.0/gems/activesupport-7.1.1/lib/active_support/core_ext/time/conversions.rb:7:in `<top (required)>'
	from /home/runner/work/rspec-expectations/rspec-expectations/bundle/ruby/2.7.0/gems/activesupport-7.1.1/lib/active_support/core_ext/object/json.rb:14:in `require'
	from /home/runner/work/rspec-expectations/rspec-expectations/bundle/ruby/2.7.0/gems/activesupport-7.1.1/lib/active_support/core_ext/object/json.rb:14:in `<top (required)>'
	from /home/runner/work/rspec-expectations/rspec-expectations/bundle/ruby/2.7.0/gems/activesupport-7.1.1/lib/active_support/json/encoding.rb:3:in `require'
	from /home/runner/work/rspec-expectations/rspec-expectations/bundle/ruby/2.7.0/gems/activesupport-7.1.1/lib/active_support/json/encoding.rb:3:in `<top (required)>'
	from /home/runner/work/rspec-expectations/rspec-expectations/bundle/ruby/2.7.0/gems/activesupport-7.1.1/lib/active_support/json.rb:4:in `require'
	from /home/runner/work/rspec-expectations/rspec-expectations/bundle/ruby/2.7.0/gems/activesupport-7.1.1/lib/active_support/json.rb:4:in `<top (required)>'
	from /home/runner/work/rspec-expectations/rspec-expectations/bundle/ruby/2.7.0/gems/protobuf-cucumber-3.10.8/lib/protobuf.rb:10:in `require'

This happens because before cherry-picking things inside Active Support you must first require "active_support", and the protobuf gem doesn't respect that: https://github.com/ruby-protobuf/protobuf/blob/b866fc5667226d0582f328ab24c648e578c5a380/lib/protobuf.rb#L7-L11

I'll open another PR there, but In the meantime I think this one is ready.

@byroot
Copy link
Contributor

byroot commented Nov 8, 2023

Upstream PR: ruby-protobuf/protobuf#431

@casperisfine
Copy link
Author

@pirj anything else I can do?

@casperisfine
Copy link
Author

👋 any news here?

@JonRowe JonRowe merged commit 2e8e800 into rspec:main Nov 15, 2023
27 of 29 checks passed
@JonRowe
Copy link
Member

JonRowe commented Nov 22, 2023

Thanks for this, I think this is a good improvement for us as a protection against monkey patching Object, I'm not quite sure when I'll release this yet as I'm debating the level of change this justifies and reflecting on where we could extend this logic too.

(Apologies for the delayed merge then delayed comment, I was originally letting Phil handle it whilst being busy with rspec-rails [I don't use Ruby or Rails in my day-job currently])

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

Successfully merging this pull request may close these issues.

4 participants