rspec-expectations 3.8.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +5 -0
  4. data/.document +5 -0
  5. data/.yardopts +6 -0
  6. data/Changelog.md +1156 -0
  7. data/LICENSE.md +25 -0
  8. data/README.md +305 -0
  9. data/lib/rspec/expectations.rb +82 -0
  10. data/lib/rspec/expectations/block_snippet_extractor.rb +253 -0
  11. data/lib/rspec/expectations/configuration.rb +215 -0
  12. data/lib/rspec/expectations/expectation_target.rb +127 -0
  13. data/lib/rspec/expectations/fail_with.rb +39 -0
  14. data/lib/rspec/expectations/failure_aggregator.rb +194 -0
  15. data/lib/rspec/expectations/handler.rb +170 -0
  16. data/lib/rspec/expectations/minitest_integration.rb +58 -0
  17. data/lib/rspec/expectations/syntax.rb +132 -0
  18. data/lib/rspec/expectations/version.rb +8 -0
  19. data/lib/rspec/matchers.rb +1034 -0
  20. data/lib/rspec/matchers/aliased_matcher.rb +116 -0
  21. data/lib/rspec/matchers/built_in.rb +52 -0
  22. data/lib/rspec/matchers/built_in/all.rb +86 -0
  23. data/lib/rspec/matchers/built_in/base_matcher.rb +193 -0
  24. data/lib/rspec/matchers/built_in/be.rb +288 -0
  25. data/lib/rspec/matchers/built_in/be_between.rb +77 -0
  26. data/lib/rspec/matchers/built_in/be_instance_of.rb +26 -0
  27. data/lib/rspec/matchers/built_in/be_kind_of.rb +20 -0
  28. data/lib/rspec/matchers/built_in/be_within.rb +72 -0
  29. data/lib/rspec/matchers/built_in/change.rb +428 -0
  30. data/lib/rspec/matchers/built_in/compound.rb +271 -0
  31. data/lib/rspec/matchers/built_in/contain_exactly.rb +302 -0
  32. data/lib/rspec/matchers/built_in/cover.rb +24 -0
  33. data/lib/rspec/matchers/built_in/eq.rb +40 -0
  34. data/lib/rspec/matchers/built_in/eql.rb +34 -0
  35. data/lib/rspec/matchers/built_in/equal.rb +81 -0
  36. data/lib/rspec/matchers/built_in/exist.rb +90 -0
  37. data/lib/rspec/matchers/built_in/has.rb +103 -0
  38. data/lib/rspec/matchers/built_in/have_attributes.rb +114 -0
  39. data/lib/rspec/matchers/built_in/include.rb +149 -0
  40. data/lib/rspec/matchers/built_in/match.rb +106 -0
  41. data/lib/rspec/matchers/built_in/operators.rb +128 -0
  42. data/lib/rspec/matchers/built_in/output.rb +200 -0
  43. data/lib/rspec/matchers/built_in/raise_error.rb +230 -0
  44. data/lib/rspec/matchers/built_in/respond_to.rb +165 -0
  45. data/lib/rspec/matchers/built_in/satisfy.rb +60 -0
  46. data/lib/rspec/matchers/built_in/start_or_end_with.rb +94 -0
  47. data/lib/rspec/matchers/built_in/throw_symbol.rb +132 -0
  48. data/lib/rspec/matchers/built_in/yield.rb +432 -0
  49. data/lib/rspec/matchers/composable.rb +171 -0
  50. data/lib/rspec/matchers/dsl.rb +527 -0
  51. data/lib/rspec/matchers/english_phrasing.rb +58 -0
  52. data/lib/rspec/matchers/expecteds_for_multiple_diffs.rb +73 -0
  53. data/lib/rspec/matchers/fail_matchers.rb +42 -0
  54. data/lib/rspec/matchers/generated_descriptions.rb +41 -0
  55. data/lib/rspec/matchers/matcher_delegator.rb +35 -0
  56. data/lib/rspec/matchers/matcher_protocol.rb +99 -0
  57. metadata +215 -0
  58. metadata.gz.sig +0 -0
@@ -0,0 +1,25 @@
1
+ The MIT License (MIT)
2
+ =====================
3
+
4
+ * Copyright © 2012 David Chelimsky, Myron Marston
5
+ * Copyright © 2006 David Chelimsky, The RSpec Development Team
6
+ * Copyright © 2005 Steven Baker
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining
9
+ a copy of this software and associated documentation files (the
10
+ "Software"), to deal in the Software without restriction, including
11
+ without limitation the rights to use, copy, modify, merge, publish,
12
+ distribute, sublicense, and/or sell copies of the Software, and to
13
+ permit persons to whom the Software is furnished to do so, subject to
14
+ the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be
17
+ included in all copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
22
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
23
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
24
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
25
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,305 @@
1
+ # RSpec Expectations [![Build Status](https://secure.travis-ci.org/rspec/rspec-expectations.svg?branch=master)](http://travis-ci.org/rspec/rspec-expectations) [![Code Climate](https://codeclimate.com/github/rspec/rspec-expectations.svg)](https://codeclimate.com/github/rspec/rspec-expectations)
2
+
3
+ RSpec::Expectations lets you express expected outcomes on an object in an
4
+ example.
5
+
6
+ ```ruby
7
+ expect(account.balance).to eq(Money.new(37.42, :USD))
8
+ ```
9
+
10
+ ## Install
11
+
12
+ If you want to use rspec-expectations with rspec, just install the rspec gem
13
+ and RubyGems will also install rspec-expectations for you (along with
14
+ rspec-core and rspec-mocks):
15
+
16
+ gem install rspec
17
+
18
+ Want to run against the `master` branch? You'll need to include the dependent
19
+ RSpec repos as well. Add the following to your `Gemfile`:
20
+
21
+ ```ruby
22
+ %w[rspec-core rspec-expectations rspec-mocks rspec-support].each do |lib|
23
+ gem lib, :git => "https://github.com/rspec/#{lib}.git", :branch => 'master'
24
+ end
25
+ ```
26
+
27
+ If you want to use rspec-expectations with another tool, like Test::Unit,
28
+ Minitest, or Cucumber, you can install it directly:
29
+
30
+ gem install rspec-expectations
31
+
32
+ ## Contributing
33
+
34
+ Once you've set up the environment, you'll need to cd into the working
35
+ directory of whichever repo you want to work in. From there you can run the
36
+ specs and cucumber features, and make patches.
37
+
38
+ NOTE: You do not need to use rspec-dev to work on a specific RSpec repo. You
39
+ can treat each RSpec repo as an independent project.
40
+
41
+ - [Build details](BUILD_DETAIL.md)
42
+ - [Code of Conduct](CODE_OF_CONDUCT.md)
43
+ - [Detailed contributing guide](CONTRIBUTING.md)
44
+ - [Development setup guide](DEVELOPMENT.md)
45
+
46
+ ## Basic usage
47
+
48
+ Here's an example using rspec-core:
49
+
50
+ ```ruby
51
+ RSpec.describe Order do
52
+ it "sums the prices of the items in its line items" do
53
+ order = Order.new
54
+ order.add_entry(LineItem.new(:item => Item.new(
55
+ :price => Money.new(1.11, :USD)
56
+ )))
57
+ order.add_entry(LineItem.new(:item => Item.new(
58
+ :price => Money.new(2.22, :USD),
59
+ :quantity => 2
60
+ )))
61
+ expect(order.total).to eq(Money.new(5.55, :USD))
62
+ end
63
+ end
64
+ ```
65
+
66
+ The `describe` and `it` methods come from rspec-core. The `Order`, `LineItem`, `Item` and `Money` classes would be from _your_ code. The last line of the example
67
+ expresses an expected outcome. If `order.total == Money.new(5.55, :USD)`, then
68
+ the example passes. If not, it fails with a message like:
69
+
70
+ expected: #<Money @value=5.55 @currency=:USD>
71
+ got: #<Money @value=1.11 @currency=:USD>
72
+
73
+ ## Built-in matchers
74
+
75
+ ### Equivalence
76
+
77
+ ```ruby
78
+ expect(actual).to eq(expected) # passes if actual == expected
79
+ expect(actual).to eql(expected) # passes if actual.eql?(expected)
80
+ expect(actual).not_to eql(not_expected) # passes if not(actual.eql?(expected))
81
+ ```
82
+
83
+ Note: The new `expect` syntax no longer supports the `==` matcher.
84
+
85
+ ### Identity
86
+
87
+ ```ruby
88
+ expect(actual).to be(expected) # passes if actual.equal?(expected)
89
+ expect(actual).to equal(expected) # passes if actual.equal?(expected)
90
+ ```
91
+
92
+ ### Comparisons
93
+
94
+ ```ruby
95
+ expect(actual).to be > expected
96
+ expect(actual).to be >= expected
97
+ expect(actual).to be <= expected
98
+ expect(actual).to be < expected
99
+ expect(actual).to be_within(delta).of(expected)
100
+ ```
101
+
102
+ ### Regular expressions
103
+
104
+ ```ruby
105
+ expect(actual).to match(/expression/)
106
+ ```
107
+
108
+ Note: The new `expect` syntax no longer supports the `=~` matcher.
109
+
110
+ ### Types/classes
111
+
112
+ ```ruby
113
+ expect(actual).to be_an_instance_of(expected) # passes if actual.class == expected
114
+ expect(actual).to be_a(expected) # passes if actual.kind_of?(expected)
115
+ expect(actual).to be_an(expected) # an alias for be_a
116
+ expect(actual).to be_a_kind_of(expected) # another alias
117
+ ```
118
+
119
+ ### Truthiness
120
+
121
+ ```ruby
122
+ expect(actual).to be_truthy # passes if actual is truthy (not nil or false)
123
+ expect(actual).to be true # passes if actual == true
124
+ expect(actual).to be_falsy # passes if actual is falsy (nil or false)
125
+ expect(actual).to be false # passes if actual == false
126
+ expect(actual).to be_nil # passes if actual is nil
127
+ expect(actual).to_not be_nil # passes if actual is not nil
128
+ ```
129
+
130
+ ### Expecting errors
131
+
132
+ ```ruby
133
+ expect { ... }.to raise_error
134
+ expect { ... }.to raise_error(ErrorClass)
135
+ expect { ... }.to raise_error("message")
136
+ expect { ... }.to raise_error(ErrorClass, "message")
137
+ ```
138
+
139
+ ### Expecting throws
140
+
141
+ ```ruby
142
+ expect { ... }.to throw_symbol
143
+ expect { ... }.to throw_symbol(:symbol)
144
+ expect { ... }.to throw_symbol(:symbol, 'value')
145
+ ```
146
+
147
+ ### Yielding
148
+
149
+ ```ruby
150
+ expect { |b| 5.tap(&b) }.to yield_control # passes regardless of yielded args
151
+
152
+ expect { |b| yield_if_true(true, &b) }.to yield_with_no_args # passes only if no args are yielded
153
+
154
+ expect { |b| 5.tap(&b) }.to yield_with_args(5)
155
+ expect { |b| 5.tap(&b) }.to yield_with_args(Integer)
156
+ expect { |b| "a string".tap(&b) }.to yield_with_args(/str/)
157
+
158
+ expect { |b| [1, 2, 3].each(&b) }.to yield_successive_args(1, 2, 3)
159
+ expect { |b| { :a => 1, :b => 2 }.each(&b) }.to yield_successive_args([:a, 1], [:b, 2])
160
+ ```
161
+
162
+ ### Predicate matchers
163
+
164
+ ```ruby
165
+ expect(actual).to be_xxx # passes if actual.xxx?
166
+ expect(actual).to have_xxx(:arg) # passes if actual.has_xxx?(:arg)
167
+ ```
168
+
169
+ ### Ranges (Ruby >= 1.9 only)
170
+
171
+ ```ruby
172
+ expect(1..10).to cover(3)
173
+ ```
174
+
175
+ ### Collection membership
176
+
177
+ ```ruby
178
+ expect(actual).to include(expected)
179
+ expect(actual).to start_with(expected)
180
+ expect(actual).to end_with(expected)
181
+
182
+ expect(actual).to contain_exactly(individual, items)
183
+ # ...which is the same as:
184
+ expect(actual).to match_array(expected_array)
185
+ ```
186
+
187
+ #### Examples
188
+
189
+ ```ruby
190
+ expect([1, 2, 3]).to include(1)
191
+ expect([1, 2, 3]).to include(1, 2)
192
+ expect([1, 2, 3]).to start_with(1)
193
+ expect([1, 2, 3]).to start_with(1, 2)
194
+ expect([1, 2, 3]).to end_with(3)
195
+ expect([1, 2, 3]).to end_with(2, 3)
196
+ expect({:a => 'b'}).to include(:a => 'b')
197
+ expect("this string").to include("is str")
198
+ expect("this string").to start_with("this")
199
+ expect("this string").to end_with("ring")
200
+ expect([1, 2, 3]).to contain_exactly(2, 3, 1)
201
+ expect([1, 2, 3]).to match_array([3, 2, 1])
202
+ ```
203
+
204
+ ## `should` syntax
205
+
206
+ In addition to the `expect` syntax, rspec-expectations continues to support the
207
+ `should` syntax:
208
+
209
+ ```ruby
210
+ actual.should eq expected
211
+ actual.should be > 3
212
+ [1, 2, 3].should_not include 4
213
+ ```
214
+
215
+ See [detailed information on the `should` syntax and its usage.](https://github.com/rspec/rspec-expectations/blob/master/Should.md)
216
+
217
+ ## Compound Matcher Expressions
218
+
219
+ You can also create compound matcher expressions using `and` or `or`:
220
+
221
+ ``` ruby
222
+ expect(alphabet).to start_with("a").and end_with("z")
223
+ expect(stoplight.color).to eq("red").or eq("green").or eq("yellow")
224
+ ```
225
+
226
+ ## Composing Matchers
227
+
228
+ Many of the built-in matchers are designed to take matchers as
229
+ arguments, to allow you to flexibly specify only the essential
230
+ aspects of an object or data structure. In addition, all of the
231
+ built-in matchers have one or more aliases that provide better
232
+ phrasing for when they are used as arguments to another matcher.
233
+
234
+ ### Examples
235
+
236
+ ```ruby
237
+ expect { k += 1.05 }.to change { k }.by( a_value_within(0.1).of(1.0) )
238
+
239
+ expect { s = "barn" }.to change { s }
240
+ .from( a_string_matching(/foo/) )
241
+ .to( a_string_matching(/bar/) )
242
+
243
+ expect(["barn", 2.45]).to contain_exactly(
244
+ a_value_within(0.1).of(2.5),
245
+ a_string_starting_with("bar")
246
+ )
247
+
248
+ expect(["barn", "food", 2.45]).to end_with(
249
+ a_string_matching("foo"),
250
+ a_value > 2
251
+ )
252
+
253
+ expect(["barn", 2.45]).to include( a_string_starting_with("bar") )
254
+
255
+ expect(:a => "food", :b => "good").to include(:a => a_string_matching(/foo/))
256
+
257
+ hash = {
258
+ :a => {
259
+ :b => ["foo", 5],
260
+ :c => { :d => 2.05 }
261
+ }
262
+ }
263
+
264
+ expect(hash).to match(
265
+ :a => {
266
+ :b => a_collection_containing_exactly(
267
+ a_string_starting_with("f"),
268
+ an_instance_of(Integer)
269
+ ),
270
+ :c => { :d => (a_value < 3) }
271
+ }
272
+ )
273
+
274
+ expect { |probe|
275
+ [1, 2, 3].each(&probe)
276
+ }.to yield_successive_args( a_value < 2, 2, a_value > 2 )
277
+ ```
278
+
279
+ ## Usage outside rspec-core
280
+
281
+ You always need to load `rspec/expectations` even if you only want to use one part of the library:
282
+
283
+ ```ruby
284
+ require 'rspec/expectations'
285
+ ```
286
+
287
+ Then simply include `RSpec::Matchers` in any class:
288
+
289
+ ```ruby
290
+ class MyClass
291
+ include RSpec::Matchers
292
+
293
+ def do_something(arg)
294
+ expect(arg).to be > 0
295
+ # do other stuff
296
+ end
297
+ end
298
+ ```
299
+
300
+ ## Also see
301
+
302
+ * [https://github.com/rspec/rspec](https://github.com/rspec/rspec)
303
+ * [https://github.com/rspec/rspec-core](https://github.com/rspec/rspec-core)
304
+ * [https://github.com/rspec/rspec-mocks](https://github.com/rspec/rspec-mocks)
305
+ * [https://github.com/rspec/rspec-rails](https://github.com/rspec/rspec-rails)
@@ -0,0 +1,82 @@
1
+ require 'rspec/support'
2
+ RSpec::Support.require_rspec_support "caller_filter"
3
+ RSpec::Support.require_rspec_support "warnings"
4
+ RSpec::Support.require_rspec_support "object_formatter"
5
+
6
+ require 'rspec/matchers'
7
+
8
+ RSpec::Support.define_optimized_require_for_rspec(:expectations) { |f| require_relative(f) }
9
+
10
+ %w[
11
+ expectation_target
12
+ configuration
13
+ fail_with
14
+ handler
15
+ version
16
+ ].each { |file| RSpec::Support.require_rspec_expectations(file) }
17
+
18
+ module RSpec
19
+ # RSpec::Expectations provides a simple, readable API to express
20
+ # the expected outcomes in a code example. To express an expected
21
+ # outcome, wrap an object or block in `expect`, call `to` or `to_not`
22
+ # (aliased as `not_to`) and pass it a matcher object:
23
+ #
24
+ # expect(order.total).to eq(Money.new(5.55, :USD))
25
+ # expect(list).to include(user)
26
+ # expect(message).not_to match(/foo/)
27
+ # expect { do_something }.to raise_error
28
+ #
29
+ # The last form (the block form) is needed to match against ruby constructs
30
+ # that are not objects, but can only be observed when executing a block
31
+ # of code. This includes raising errors, throwing symbols, yielding,
32
+ # and changing values.
33
+ #
34
+ # When `expect(...).to` is invoked with a matcher, it turns around
35
+ # and calls `matcher.matches?(<object wrapped by expect>)`. For example,
36
+ # in the expression:
37
+ #
38
+ # expect(order.total).to eq(Money.new(5.55, :USD))
39
+ #
40
+ # ...`eq(Money.new(5.55, :USD))` returns a matcher object, and it results
41
+ # in the equivalent of `eq.matches?(order.total)`. If `matches?` returns
42
+ # `true`, the expectation is met and execution continues. If `false`, then
43
+ # the spec fails with the message returned by `eq.failure_message`.
44
+ #
45
+ # Given the expression:
46
+ #
47
+ # expect(order.entries).not_to include(entry)
48
+ #
49
+ # ...the `not_to` method (also available as `to_not`) invokes the equivalent of
50
+ # `include.matches?(order.entries)`, but it interprets `false` as success, and
51
+ # `true` as a failure, using the message generated by
52
+ # `include.failure_message_when_negated`.
53
+ #
54
+ # rspec-expectations ships with a standard set of useful matchers, and writing
55
+ # your own matchers is quite simple.
56
+ #
57
+ # See [RSpec::Matchers](../RSpec/Matchers) for more information about the
58
+ # built-in matchers that ship with rspec-expectations, and how to write your
59
+ # own custom matchers.
60
+ module Expectations
61
+ # Exception raised when an expectation fails.
62
+ #
63
+ # @note We subclass Exception so that in a stub implementation if
64
+ # the user sets an expectation, it can't be caught in their
65
+ # code by a bare `rescue`.
66
+ # @api public
67
+ class ExpectationNotMetError < Exception
68
+ end
69
+
70
+ # Exception raised from `aggregate_failures` when multiple expectations fail.
71
+ #
72
+ # @note The constant is defined here but the extensive logic of this class
73
+ # is lazily defined when `FailureAggregator` is autoloaded, since we do
74
+ # not need to waste time defining that functionality unless
75
+ # `aggregate_failures` is used.
76
+ class MultipleExpectationsNotMetError < ExpectationNotMetError
77
+ end
78
+
79
+ autoload :BlockSnippetExtractor, "rspec/expectations/block_snippet_extractor"
80
+ autoload :FailureAggregator, "rspec/expectations/failure_aggregator"
81
+ end
82
+ end
@@ -0,0 +1,253 @@
1
+ module RSpec
2
+ module Expectations
3
+ # @private
4
+ class BlockSnippetExtractor # rubocop:disable Metrics/ClassLength
5
+ # rubocop should properly handle `Struct.new {}` as an inner class definition.
6
+
7
+ attr_reader :proc, :method_name
8
+
9
+ def self.try_extracting_single_line_body_of(proc, method_name)
10
+ lines = new(proc, method_name).body_content_lines
11
+ return nil unless lines.count == 1
12
+ lines.first
13
+ rescue Error
14
+ nil
15
+ end
16
+
17
+ def initialize(proc, method_name)
18
+ @proc = proc
19
+ @method_name = method_name.to_s.freeze
20
+ end
21
+
22
+ # Ideally we should properly handle indentations of multiline snippet,
23
+ # but it's not implemented yet since because we use result of this method only when it's a
24
+ # single line and implementing the logic introduces additional complexity.
25
+ def body_content_lines
26
+ raw_body_lines.map(&:strip).reject(&:empty?)
27
+ end
28
+
29
+ private
30
+
31
+ def raw_body_lines
32
+ raw_body_snippet.split("\n")
33
+ end
34
+
35
+ def raw_body_snippet
36
+ block_token_extractor.body_tokens.map(&:string).join
37
+ end
38
+
39
+ def block_token_extractor
40
+ @block_token_extractor ||= BlockTokenExtractor.new(method_name, source, beginning_line_number)
41
+ end
42
+
43
+ if RSpec.respond_to?(:world)
44
+ def source
45
+ raise TargetNotFoundError unless File.exist?(file_path)
46
+ RSpec.world.source_from_file(file_path)
47
+ end
48
+ else
49
+ RSpec::Support.require_rspec_support 'source'
50
+ def source
51
+ raise TargetNotFoundError unless File.exist?(file_path)
52
+ @source ||= RSpec::Support::Source.from_file(file_path)
53
+ end
54
+ end
55
+
56
+ def file_path
57
+ source_location.first
58
+ end
59
+
60
+ def beginning_line_number
61
+ source_location.last
62
+ end
63
+
64
+ def source_location
65
+ proc.source_location || raise(TargetNotFoundError)
66
+ end
67
+
68
+ Error = Class.new(StandardError)
69
+ TargetNotFoundError = Class.new(Error)
70
+ AmbiguousTargetError = Class.new(Error)
71
+
72
+ # @private
73
+ # Performs extraction of block body snippet using tokens,
74
+ # which cannot be done with node information.
75
+ BlockTokenExtractor = Struct.new(:method_name, :source, :beginning_line_number) do
76
+ attr_reader :state, :body_tokens
77
+
78
+ def initialize(*)
79
+ super
80
+ parse!
81
+ end
82
+
83
+ private
84
+
85
+ def parse!
86
+ @state = :initial
87
+
88
+ catch(:finish) do
89
+ source.tokens.each do |token|
90
+ invoke_state_handler(token)
91
+ end
92
+ end
93
+ end
94
+
95
+ def finish!
96
+ throw :finish
97
+ end
98
+
99
+ def invoke_state_handler(token)
100
+ __send__("#{state}_state", token)
101
+ end
102
+
103
+ def initial_state(token)
104
+ @state = :after_method_call if token.location == block_locator.method_call_location
105
+ end
106
+
107
+ def after_method_call_state(token)
108
+ @state = :after_opener if handle_opener_token(token)
109
+ end
110
+
111
+ def after_opener_state(token)
112
+ if handle_closer_token(token)
113
+ finish_or_find_next_block_if_incorrect!
114
+ elsif pipe_token?(token)
115
+ finalize_pending_tokens!
116
+ @state = :after_beginning_of_args
117
+ else
118
+ pending_tokens << token
119
+ handle_opener_token(token)
120
+ @state = :after_beginning_of_body unless token.type == :on_sp
121
+ end
122
+ end
123
+
124
+ def after_beginning_of_args_state(token)
125
+ @state = :after_beginning_of_body if pipe_token?(token)
126
+ end
127
+
128
+ def after_beginning_of_body_state(token)
129
+ if handle_closer_token(token)
130
+ finish_or_find_next_block_if_incorrect!
131
+ else
132
+ pending_tokens << token
133
+ handle_opener_token(token)
134
+ end
135
+ end
136
+
137
+ def pending_tokens
138
+ @pending_tokens ||= []
139
+ end
140
+
141
+ def finalize_pending_tokens!
142
+ pending_tokens.freeze.tap do
143
+ @pending_tokens = nil
144
+ end
145
+ end
146
+
147
+ def finish_or_find_next_block_if_incorrect!
148
+ body_tokens = finalize_pending_tokens!
149
+
150
+ if correct_block?(body_tokens)
151
+ @body_tokens = body_tokens
152
+ finish!
153
+ else
154
+ @state = :after_method_call
155
+ end
156
+ end
157
+
158
+ def handle_opener_token(token)
159
+ opener_token?(token).tap do |boolean|
160
+ opener_token_stack.push(token) if boolean
161
+ end
162
+ end
163
+
164
+ def opener_token?(token)
165
+ token.type == :on_lbrace || (token.type == :on_kw && token.string == 'do')
166
+ end
167
+
168
+ def handle_closer_token(token)
169
+ if opener_token_stack.last.closed_by?(token)
170
+ opener_token_stack.pop
171
+ opener_token_stack.empty?
172
+ else
173
+ false
174
+ end
175
+ end
176
+
177
+ def opener_token_stack
178
+ @opener_token_stack ||= []
179
+ end
180
+
181
+ def pipe_token?(token)
182
+ token.type == :on_op && token.string == '|'
183
+ end
184
+
185
+ def correct_block?(body_tokens)
186
+ return true if block_locator.body_content_locations.empty?
187
+ content_location = block_locator.body_content_locations.first
188
+ content_location.between?(body_tokens.first.location, body_tokens.last.location)
189
+ end
190
+
191
+ def block_locator
192
+ @block_locator ||= BlockLocator.new(method_name, source, beginning_line_number)
193
+ end
194
+ end
195
+
196
+ # @private
197
+ # Locates target block with node information (semantics), which tokens don't have.
198
+ BlockLocator = Struct.new(:method_name, :source, :beginning_line_number) do
199
+ def method_call_location
200
+ @method_call_location ||= method_ident_node.location
201
+ end
202
+
203
+ def body_content_locations
204
+ @body_content_locations ||= block_body_node.map(&:location).compact
205
+ end
206
+
207
+ private
208
+
209
+ def method_ident_node
210
+ method_call_node = block_wrapper_node.children.first
211
+ method_call_node.find do |node|
212
+ method_ident_node?(node)
213
+ end
214
+ end
215
+
216
+ def block_body_node
217
+ block_node = block_wrapper_node.children[1]
218
+ block_node.children.last
219
+ end
220
+
221
+ def block_wrapper_node
222
+ case candidate_block_wrapper_nodes.size
223
+ when 1
224
+ candidate_block_wrapper_nodes.first
225
+ when 0
226
+ raise TargetNotFoundError
227
+ else
228
+ raise AmbiguousTargetError
229
+ end
230
+ end
231
+
232
+ def candidate_block_wrapper_nodes
233
+ @candidate_block_wrapper_nodes ||= candidate_method_ident_nodes.map do |method_ident_node|
234
+ block_wrapper_node = method_ident_node.each_ancestor.find { |node| node.type == :method_add_block }
235
+ next nil unless block_wrapper_node
236
+ method_call_node = block_wrapper_node.children.first
237
+ method_call_node.include?(method_ident_node) ? block_wrapper_node : nil
238
+ end.compact
239
+ end
240
+
241
+ def candidate_method_ident_nodes
242
+ source.nodes_by_line_number[beginning_line_number].select do |node|
243
+ method_ident_node?(node)
244
+ end
245
+ end
246
+
247
+ def method_ident_node?(node)
248
+ node.type == :@ident && node.args.first == method_name
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end