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

Prevent block-only matchers from being used with value expectation target #1125

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ Bug Fixes:
* The `change` matcher now recognises an object has changed when its instance attributes
have changed. (Jon Rowe, #1132)

Bug Fixes:

* Prevent unsupported implicit block expectation syntax from being used.
(Phil Pirozhkov, #1125)

### 3.8.4 / 2019-06-10
[Full Changelog](http://github.com/rspec/rspec-expectations/compare/v3.8.3...v3.8.4)

Expand All @@ -31,7 +36,7 @@ Bug Fixes:
* Prevent composed `all` matchers from leaking into their siblings leading to duplicate
failures. (Jamie English, #1086)
* Prevent objects which change their hash on comparison from failing change checks.
(Phil Pirozhkov, #1110)
(Phil Pirozhkov, #1100)
* Issue an `ArgumentError` rather than a `NoMethodError` when `be_an_instance_of` and
`be_kind_of` matchers encounter objects not supporting those methods.
(Taichi Ishitani, #1107)
Expand Down
50 changes: 43 additions & 7 deletions lib/rspec/expectations/expectation_target.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@ def initialize(value)
end

# @private
def self.for(value, block)
def self.for(value, &block)
if UndefinedValue.equal?(value)
unless block
unless block_given?
raise ArgumentError, "You must pass either an argument or a block to `expect`."
end
BlockExpectationTarget.new(block)
elsif block
elsif block_given?
raise ArgumentError, "You cannot pass both an argument and a block to `expect`."
else
new(value)
ValueExpectationTarget.new(value)
end
end

Expand Down Expand Up @@ -90,6 +90,40 @@ def prevent_operator_matchers(verb)
include InstanceMethods
end

# @private
# Validates the provided matcher to ensure it supports block
# expectations, in order to avoid user confusion when they
# use a block thinking the expectation will be on the return
# value of the block rather than the block itself.
class ValueExpectationTarget < ExpectationTarget
def to(matcher=nil, message=nil, &block)
enforce_value_expectation(matcher)
super
end

def not_to(matcher=nil, message=nil, &block)
enforce_value_expectation(matcher)
super
end

private

def enforce_value_expectation(matcher)
return if supports_value_expectations?(matcher)

raise ExpectationNotMetError, "You must pass a block rather than an argument to `expect` to use the provided " \
"block expectation matcher (#{RSpec::Support::ObjectFormatter.format(matcher)})."
end

def supports_value_expectations?(matcher)
if matcher.respond_to?(:supports_value_expectations?)
matcher.supports_value_expectations?
else
true
end
end
end

# @private
# Validates the provided matcher to ensure it supports block
# expectations, in order to avoid user confusion when they
Expand Down Expand Up @@ -118,9 +152,11 @@ def enforce_block_expectation(matcher)
end

def supports_block_expectations?(matcher)
matcher.supports_block_expectations?
rescue NoMethodError
false
if matcher.respond_to?(:supports_block_expectations?)
Copy link
Member Author

Choose a reason for hiding this comment

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

This change is due to have matcher from rspec-collection_matchers that is sensitive to methods called on it, i.e. if we call supports_value_expectations? on have(3).players, it starts to think that it should match supports_value_expectations? association, not players.

The problem is with supports_value_expectations?, changing supports_block_expectations? for consistency.

matcher.supports_block_expectations?
else
false
end
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/rspec/expectations/syntax.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def enable_expect(syntax_host=::RSpec::Matchers)

syntax_host.module_exec do
def expect(value=::RSpec::Expectations::ExpectationTarget::UndefinedValue, &block)
::RSpec::Expectations::ExpectationTarget.for(value, block)
::RSpec::Expectations::ExpectationTarget.for(value, &block)
end
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/rspec/matchers/built_in/base_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ def supports_block_expectations?
false
end

# @private
def supports_value_expectations?
true
end

# @api private
def expects_call_stack_jump?
false
Expand Down
25 changes: 15 additions & 10 deletions lib/rspec/matchers/built_in/change.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ def supports_block_expectations?
true
end

# @private
def supports_value_expectations?
Copy link
Member Author

Choose a reason for hiding this comment

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

It's possible to tell if a matcher supports block expectations, but it's not possible to detect that it doesn't support value expectations, and will be sending call on a value target.

Copy link
Member

Choose a reason for hiding this comment

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

Seems reasonable!

false
end

private

def initialize(receiver=nil, message=nil, &block)
Expand Down Expand Up @@ -107,12 +112,10 @@ def raise_block_syntax_error
end

def positive_failure_reason
return "was not given a block" unless Proc === @event_proc
"is still #{@actual_before_description}"
end

def negative_failure_reason
return "was not given a block" unless Proc === @event_proc
"did change from #{@actual_before_description} " \
"to #{description_of change_details.actual_after}"
end
Expand Down Expand Up @@ -158,10 +161,14 @@ def supports_block_expectations?
true
end

# @private
def supports_value_expectations?
false
end

private

def failure_reason
return "was not given a block" unless Proc === @event_proc
"was changed by #{description_of @change_details.actual_delta}"
end
end
Expand Down Expand Up @@ -190,7 +197,6 @@ def description

# @private
def failure_message
return not_given_a_block_failure unless Proc === @event_proc
return before_value_failure unless @matches_before
return did_not_change_failure unless @change_details.changed?
after_value_failure
Expand All @@ -201,6 +207,11 @@ def supports_block_expectations?
true
end

# @private
def supports_value_expectations?
false
end

private

def perform_change(event_proc)
Expand Down Expand Up @@ -242,11 +253,6 @@ def did_change_failure
"did change from #{@actual_before_description} " \
"to #{description_of @change_details.actual_after}"
end

def not_given_a_block_failure
"expected #{@change_details.value_representation} to have changed " \
"#{change_description}, but was not given a block"
end
end

# @api private
Expand Down Expand Up @@ -278,7 +284,6 @@ def does_not_match?(event_proc)

# @private
def failure_message_when_negated
return not_given_a_block_failure unless Proc === @event_proc
return before_value_failure unless @matches_before
did_change_failure
end
Expand Down
14 changes: 14 additions & 0 deletions lib/rspec/matchers/built_in/compound.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,19 @@ def description
"#{matcher_1.description} #{conjunction} #{matcher_2.description}"
end

# @api private
def supports_block_expectations?
matcher_supports_block_expectations?(matcher_1) &&
matcher_supports_block_expectations?(matcher_2)
end

# @api private
def supports_value_expectations?
matcher_supports_value_expectations?(matcher_1) &&
matcher_supports_value_expectations?(matcher_2)
end

# @api private
def expects_call_stack_jump?
NestedEvaluator.matcher_expects_call_stack_jump?(matcher_1) ||
NestedEvaluator.matcher_expects_call_stack_jump?(matcher_2)
Expand Down Expand Up @@ -102,6 +110,12 @@ def matcher_supports_block_expectations?(matcher)
false
end

def matcher_supports_value_expectations?(matcher)
matcher.supports_value_expectations?
rescue NoMethodError
true
end

def matcher_is_diffable?(matcher)
matcher.diffable?
rescue NoMethodError
Expand Down
9 changes: 7 additions & 2 deletions lib/rspec/matchers/built_in/output.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,20 +94,25 @@ def supports_block_expectations?
true
end

# @api private
# Indicates this matcher matches against a block only.
# @return [False]
def supports_value_expectations?
false
end

private

def captured?
@actual.length > 0
end

def positive_failure_reason
return "was not a block" unless Proc === @block
return "output #{actual_output_description}" if @expected
"did not"
end

def negative_failure_reason
return "was not a block" unless Proc === @block
"output #{actual_output_description}"
end

Expand Down
7 changes: 6 additions & 1 deletion lib/rspec/matchers/built_in/raise_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ def supports_block_expectations?
true
end

# @private
def supports_value_expectations?
false
end

# @private
def expects_call_stack_jump?
true
end
Expand Down Expand Up @@ -199,7 +205,6 @@ def format_backtrace(backtrace)
end

def given_error
return " but was not given a block" unless Proc === @given_proc
return " but nothing was raised" unless @actual_error

backtrace = format_backtrace(@actual_error.backtrace)
Expand Down
9 changes: 6 additions & 3 deletions lib/rspec/matchers/built_in/throw_symbol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,23 @@ def description
end

# @api private
# Indicates this matcher matches against a block.
# @return [True]
def supports_block_expectations?
true
end

# @api private
def supports_value_expectations?
false
end

# @api private
def expects_call_stack_jump?
true
end

private

def actual_result
return "but was not a block" unless Proc === @block
"got #{caught}"
end

Expand Down
Loading