matchers 0.1.0.pre.test

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of matchers might be problematic. Click here for more details.

Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/lib/matcher/assertions.rb +21 -0
  3. data/lib/matcher/base.rb +189 -0
  4. data/lib/matcher/builder.rb +74 -0
  5. data/lib/matcher/chain.rb +60 -0
  6. data/lib/matcher/debug.rb +48 -0
  7. data/lib/matcher/errors/and_error.rb +86 -0
  8. data/lib/matcher/errors/boolean_collector.rb +51 -0
  9. data/lib/matcher/errors/element_error.rb +24 -0
  10. data/lib/matcher/errors/empty_error.rb +23 -0
  11. data/lib/matcher/errors/error.rb +39 -0
  12. data/lib/matcher/errors/error_collector.rb +99 -0
  13. data/lib/matcher/errors/nested_error.rb +96 -0
  14. data/lib/matcher/errors/or_error.rb +86 -0
  15. data/lib/matcher/expression_cache.rb +57 -0
  16. data/lib/matcher/expression_labeler.rb +91 -0
  17. data/lib/matcher/expressions/array_expression.rb +45 -0
  18. data/lib/matcher/expressions/block.rb +153 -0
  19. data/lib/matcher/expressions/call.rb +338 -0
  20. data/lib/matcher/expressions/call_error.rb +45 -0
  21. data/lib/matcher/expressions/constant.rb +53 -0
  22. data/lib/matcher/expressions/expression.rb +147 -0
  23. data/lib/matcher/expressions/expression_building.rb +258 -0
  24. data/lib/matcher/expressions/expression_walker.rb +73 -0
  25. data/lib/matcher/expressions/hash_expression.rb +59 -0
  26. data/lib/matcher/expressions/proc_expression.rb +92 -0
  27. data/lib/matcher/expressions/range_expression.rb +58 -0
  28. data/lib/matcher/expressions/recorder.rb +86 -0
  29. data/lib/matcher/expressions/rescue_last_error_expression.rb +44 -0
  30. data/lib/matcher/expressions/set_expression.rb +45 -0
  31. data/lib/matcher/expressions/string_expression.rb +53 -0
  32. data/lib/matcher/expressions/symbol_proc.rb +53 -0
  33. data/lib/matcher/expressions/variable.rb +85 -0
  34. data/lib/matcher/hash_stack.rb +53 -0
  35. data/lib/matcher/list.rb +102 -0
  36. data/lib/matcher/markers/optional.rb +80 -0
  37. data/lib/matcher/markers/others.rb +28 -0
  38. data/lib/matcher/matcher_cache.rb +18 -0
  39. data/lib/matcher/matchers/all_matcher.rb +60 -0
  40. data/lib/matcher/matchers/always_matcher.rb +28 -0
  41. data/lib/matcher/matchers/any_matcher.rb +70 -0
  42. data/lib/matcher/matchers/array_matcher.rb +35 -0
  43. data/lib/matcher/matchers/block_matcher.rb +59 -0
  44. data/lib/matcher/matchers/boolean_matcher.rb +35 -0
  45. data/lib/matcher/matchers/dig_matcher.rb +146 -0
  46. data/lib/matcher/matchers/each_matcher.rb +52 -0
  47. data/lib/matcher/matchers/each_pair_matcher.rb +119 -0
  48. data/lib/matcher/matchers/equal_matcher.rb +197 -0
  49. data/lib/matcher/matchers/equal_set_matcher.rb +99 -0
  50. data/lib/matcher/matchers/expression_matcher.rb +73 -0
  51. data/lib/matcher/matchers/filter_matcher.rb +111 -0
  52. data/lib/matcher/matchers/hash_matcher.rb +223 -0
  53. data/lib/matcher/matchers/imply_matcher.rb +81 -0
  54. data/lib/matcher/matchers/imply_some_matcher.rb +112 -0
  55. data/lib/matcher/matchers/index_by_matcher.rb +175 -0
  56. data/lib/matcher/matchers/inline_matcher.rb +99 -0
  57. data/lib/matcher/matchers/keys_matcher.rb +121 -0
  58. data/lib/matcher/matchers/kind_of_matcher.rb +35 -0
  59. data/lib/matcher/matchers/lazy_all_matcher.rb +68 -0
  60. data/lib/matcher/matchers/lazy_any_matcher.rb +68 -0
  61. data/lib/matcher/matchers/let_matcher.rb +73 -0
  62. data/lib/matcher/matchers/map_matcher.rb +129 -0
  63. data/lib/matcher/matchers/matcher_building.rb +5 -0
  64. data/lib/matcher/matchers/negated_array_matcher.rb +38 -0
  65. data/lib/matcher/matchers/negated_each_matcher.rb +36 -0
  66. data/lib/matcher/matchers/negated_each_pair_matcher.rb +38 -0
  67. data/lib/matcher/matchers/negated_imply_some_matcher.rb +46 -0
  68. data/lib/matcher/matchers/negated_matcher.rb +23 -0
  69. data/lib/matcher/matchers/negated_project_matcher.rb +31 -0
  70. data/lib/matcher/matchers/never_matcher.rb +29 -0
  71. data/lib/matcher/matchers/one_matcher.rb +70 -0
  72. data/lib/matcher/matchers/optional_matcher.rb +38 -0
  73. data/lib/matcher/matchers/parse_float_matcher.rb +86 -0
  74. data/lib/matcher/matchers/parse_integer_matcher.rb +98 -0
  75. data/lib/matcher/matchers/parse_iso8601_matcher.rb +92 -0
  76. data/lib/matcher/matchers/parse_json_matcher.rb +95 -0
  77. data/lib/matcher/matchers/project_matcher.rb +68 -0
  78. data/lib/matcher/matchers/raises_matcher.rb +124 -0
  79. data/lib/matcher/matchers/range_matcher.rb +47 -0
  80. data/lib/matcher/matchers/reference_matcher.rb +111 -0
  81. data/lib/matcher/matchers/reference_matcher_collection.rb +57 -0
  82. data/lib/matcher/matchers/regexp_matcher.rb +84 -0
  83. data/lib/matcher/messages/expected_phrasing.rb +342 -0
  84. data/lib/matcher/messages/message.rb +102 -0
  85. data/lib/matcher/messages/message_builder.rb +35 -0
  86. data/lib/matcher/messages/message_rules.rb +223 -0
  87. data/lib/matcher/messages/namespaced_message_builder.rb +19 -0
  88. data/lib/matcher/messages/phrasing.rb +57 -0
  89. data/lib/matcher/messages/standard_message_builder.rb +105 -0
  90. data/lib/matcher/once_before.rb +18 -0
  91. data/lib/matcher/optional_chain.rb +24 -0
  92. data/lib/matcher/patterns/ast_mapping.rb +42 -0
  93. data/lib/matcher/patterns/capture_hole.rb +33 -0
  94. data/lib/matcher/patterns/constant_hole.rb +14 -0
  95. data/lib/matcher/patterns/hole.rb +30 -0
  96. data/lib/matcher/patterns/method_hole.rb +58 -0
  97. data/lib/matcher/patterns/pattern.rb +92 -0
  98. data/lib/matcher/patterns/pattern_building.rb +39 -0
  99. data/lib/matcher/patterns/pattern_capture.rb +11 -0
  100. data/lib/matcher/patterns/pattern_match.rb +29 -0
  101. data/lib/matcher/patterns/variable_hole.rb +14 -0
  102. data/lib/matcher/reporter.rb +98 -0
  103. data/lib/matcher/rules/message_factory.rb +25 -0
  104. data/lib/matcher/rules/message_rule.rb +18 -0
  105. data/lib/matcher/rules/message_rule_context.rb +24 -0
  106. data/lib/matcher/rules/rule_builder.rb +29 -0
  107. data/lib/matcher/rules/rule_set.rb +57 -0
  108. data/lib/matcher/rules/transform_builder.rb +24 -0
  109. data/lib/matcher/rules/transform_mapping.rb +5 -0
  110. data/lib/matcher/rules/transform_rule.rb +21 -0
  111. data/lib/matcher/state.rb +40 -0
  112. data/lib/matcher/testing/error_builder.rb +62 -0
  113. data/lib/matcher/testing/error_checker.rb +496 -0
  114. data/lib/matcher/testing/error_testing.rb +37 -0
  115. data/lib/matcher/testing/pattern_testing.rb +11 -0
  116. data/lib/matcher/testing/pattern_testing_scope.rb +34 -0
  117. data/lib/matcher/testing.rb +102 -0
  118. data/lib/matcher/undefined.rb +10 -0
  119. data/lib/matcher/utils/mapping_utils.rb +61 -0
  120. data/lib/matcher/utils.rb +72 -0
  121. data/lib/matcher/version.rb +5 -0
  122. data/lib/matcher.rb +337 -0
  123. metadata +167 -0
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ module ExpressionBuilding
5
+ attr_reader :assigns
6
+
7
+ def self.init(builder, build_session)
8
+ builder.instance_exec do
9
+ @expression_cache = ExpressionCache.current(build_session)
10
+ end
11
+ end
12
+
13
+ def expression_of(value)
14
+ Expression.of(value, expression_cache: @expression_cache)
15
+ end
16
+
17
+ def expression_or_value(value)
18
+ Expression.expression_or_value(value, expression_cache: @expression_cache)
19
+ end
20
+
21
+ def declare(*symbols, **assigns)
22
+ symbols.concat(assigns.keys - symbols)
23
+ conflicts = symbols & methods
24
+
25
+ raise "Cannot declare these variables: #{conflicts.join(', ')}" if conflicts.length > 1
26
+ raise "Cannot declare variable \"#{conflicts[0]}\"" if conflicts.length == 1
27
+
28
+ symbols.each do |symbol|
29
+ define_singleton_method(symbol) do
30
+ vars[symbol]
31
+ end
32
+ end
33
+
34
+ unless assigns.empty?
35
+ if @assigns
36
+ @assigns.merge!(assigns)
37
+ else
38
+ @assigns = assigns
39
+ end
40
+ end
41
+
42
+ UNDEFINED
43
+ end
44
+
45
+ ##
46
+ # Turns any object into a recorder, or a block into an expression
47
+ # @example
48
+ # # turn object into recorder
49
+ # expr(Time).now
50
+ # expr(1) + vars[:a]
51
+ # # deeply nested structures are supported
52
+ # expr([{ foo: Set[vars[:a]] }])
53
+ # # turn block into expression (not inspectable)
54
+ # expr { |actual| actual ? 1 : 2 }
55
+ # @overload expr(obj)
56
+ # @param obj the object to wrap
57
+ # @return [Recorder]
58
+ # @overload expr(&block)
59
+ # @return [Recorder]
60
+ def expr(obj = UNDEFINED, &block)
61
+ raise 'obj and block given' if !Matcher.undefined?(obj) && block_given?
62
+
63
+ expression = block_given? ? ProcExpression.new(block) : expression_of(obj)
64
+ expression.to_recorder
65
+ end
66
+
67
+ ##
68
+ # Builds a range expression where +from+ and +to+ can be expressions
69
+ # @example
70
+ # range(0, vars[:limit])
71
+ # # evaluates to 0..10 when limit: 10
72
+ # @param from [Expression, Object]
73
+ # @param to [Expression, Object]
74
+ # @param exclude_end [Boolean]
75
+ # @return [RangeExpression]
76
+ def range(from, to, exclude_end = false)
77
+ from = expression_of(from)
78
+ to = expression_of(to)
79
+
80
+ RangeExpression.new(from, to, exclude_end)
81
+ end
82
+
83
+ def rescue_exception(expression)
84
+ expression = expression_of(expression)
85
+ rescue_last_error = RescueLastErrorExpression.new(expression)
86
+
87
+ expression_of(rescue_last_error).to_recorder
88
+ end
89
+
90
+ ##
91
+ # Returns a recorder for +Kernel+, useful for calling Kernel methods
92
+ # @example
93
+ # kernel.Integer(vars[:a]) # => Integer(a)
94
+ # @return [Recorder]
95
+ def kernel
96
+ expr(Kernel)
97
+ end
98
+
99
+ ##
100
+ # Concatenates expressions into a string expression
101
+ # @example
102
+ # concat('foo=', vars[:foo])
103
+ # # evaluates to "foo=23" when foo: 23
104
+ # @param parts [Array<Expression, Object>]
105
+ # @return [Recorder]
106
+ def concat(*parts)
107
+ parts = parts.map { expression_of(_1) }
108
+ string_expression = StringExpression.new(parts)
109
+
110
+ expression_of(string_expression).to_recorder
111
+ end
112
+
113
+ ##
114
+ # Returns a recorder for the actual value being matched
115
+ # @example
116
+ # _ > 10
117
+ # _.even?
118
+ # _.length == 3
119
+ # @return [Recorder]
120
+ def actual
121
+ vars[:actual]
122
+ end
123
+ alias _ actual
124
+
125
+ ##
126
+ # Returns a recorder for the current hash key
127
+ # @example
128
+ # each_pair(k.to_s == v)
129
+ # @return [Recorder]
130
+ def key
131
+ vars[:key]
132
+ end
133
+ alias k key
134
+
135
+ ##
136
+ # Returns a recorder for the current hash value
137
+ # @example
138
+ # each_pair(k.to_s == v)
139
+ # @return [Recorder]
140
+ def value
141
+ vars[:value]
142
+ end
143
+ alias v value
144
+
145
+ ##
146
+ # Returns a recorder for the current element index
147
+ # @example
148
+ # each(_ == i)
149
+ # @return [Recorder]
150
+ def index
151
+ vars[:index]
152
+ end
153
+ alias i index
154
+
155
+ ##
156
+ # Returns a recorder for the parent collection
157
+ # @example
158
+ # each(_ < parent.length)
159
+ # @return [Recorder]
160
+ def parent
161
+ vars[:parent]
162
+ end
163
+
164
+ ##
165
+ # Returns a recorder for the original value before mapping
166
+ # @return [Recorder]
167
+ # @see MatcherBuilding#map
168
+ def original
169
+ vars[:original]
170
+ end
171
+
172
+ ##
173
+ # Evaluates block with +&+ and +|+ acting as +&&+ and +||+
174
+ #
175
+ # Useful because +&&+ and +||+ cannot be captured by recorders.
176
+ # Note that +&+ and +|+ have different precedence than +&&+ and +||+.
177
+ # @example
178
+ # lo { vars[:foo] | vars[:bar] < 2 }
179
+ # # => (foo || bar) < 2
180
+ # @return [Recorder]
181
+ def logical_operators(&)
182
+ Matcher.with_settings(logical_operators: true, &)
183
+ end
184
+ alias lo logical_operators
185
+
186
+ ##
187
+ # Passes blocks through to the actual call instead of evaluating them
188
+ # as expression builders
189
+ # @example
190
+ # ptb do
191
+ # _.instance_exec { @foo }
192
+ # end
193
+ # @return [Recorder]
194
+ def pass_through_blocks(arg = UNDEFINED, &)
195
+ # Note that arg might be a recorder where #nil? won't work.
196
+
197
+ if Matcher.undefined?(arg)
198
+ Matcher.with_settings(pass_through_blocks: true, &)
199
+ else
200
+ Matcher.with_settings(pass_through_blocks: true) do
201
+ yield arg
202
+ end
203
+ end
204
+ end
205
+ alias ptb pass_through_blocks
206
+
207
+ ##
208
+ # Captures calls to assignment methods
209
+ # @example
210
+ # assign { _.foo = 1 } # => actual.foo = 1
211
+ # assign { _[:foo] = 1 } # => actual[:foo] = 1
212
+ # @return [Recorder]
213
+ def assign
214
+ value = expression_of(yield)
215
+ call = Call.last_assign
216
+
217
+ Call.reset_last_assign
218
+
219
+ status = if call&.assignment?
220
+ arg = call.args.last
221
+
222
+ if value.is_a?(Constant)
223
+ arg.is_a?(Constant) && value.value.equal?(arg.value)
224
+ else
225
+ value.equal?(arg)
226
+ end
227
+ end
228
+
229
+ raise 'Could not return last assignment' unless status
230
+
231
+ call.to_recorder
232
+ end
233
+
234
+ ##
235
+ # Returns the variable factory for accessing named variables
236
+ # @example
237
+ # vars[:my_value]
238
+ # _ == vars[:limit]
239
+ # @return [VariableFactory]
240
+ def vars
241
+ @vars ||= VariableFactory.new(@expression_cache)
242
+ end
243
+
244
+ class VariableFactory
245
+ include NoMatcher
246
+ include NoExpression
247
+ include NoKey
248
+
249
+ def initialize(expression_cache)
250
+ @expression_cache = expression_cache
251
+ end
252
+
253
+ def [](symbol)
254
+ Variable.cache(symbol, expression_cache: @expression_cache).to_recorder
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class ExpressionWalker
5
+ attr_accessor :constant_visitor, :variable_visitor, :call_visitor, :block_visitor, :proc_expression_visitor
6
+
7
+ def self.each_variable(expression, &block)
8
+ return to_enum(:each_variable, expression) unless block_given?
9
+
10
+ walker = new(expression)
11
+ walker.variable_visitor = block
12
+ walker.walk
13
+ end
14
+
15
+ def self.each_block(expression, &block)
16
+ return to_enum(:each_block, expression) unless block_given?
17
+
18
+ walker = new(expression)
19
+ walker.block_visitor = block
20
+ walker.walk
21
+ end
22
+
23
+ def initialize(expression)
24
+ @expression = expression
25
+ end
26
+
27
+ def walk
28
+ traverse(@expression)
29
+ end
30
+
31
+ private
32
+
33
+ def traverse(expression)
34
+ case expression
35
+ when Constant
36
+ @constant_visitor&.call(expression)
37
+ when Variable
38
+ @variable_visitor&.call(expression)
39
+ when Call
40
+ @call_visitor&.call(expression)
41
+
42
+ traverse(expression.receiver)
43
+ expression.args.each { traverse(_1) }
44
+ expression.kwargs.each { traverse(_2) }
45
+ traverse_block(expression.block) if expression.block
46
+ when ProcExpression
47
+ @proc_expression_visitor&.call(expression)
48
+ when ArrayExpression, SetExpression
49
+ expression.items.each { traverse(_1) }
50
+ when HashExpression
51
+ expression.pairs.each do |k, v|
52
+ traverse(k)
53
+ traverse(v)
54
+ end
55
+ when RangeExpression
56
+ traverse(expression.begin)
57
+ traverse(expression.end)
58
+ when RescueLastErrorExpression
59
+ traverse(expression.expression)
60
+ else
61
+ raise "unsupported expression type: #{expression.class}"
62
+ end
63
+ end
64
+
65
+ def traverse_block(block)
66
+ @block_visitor&.call(block)
67
+
68
+ return unless block.is_a?(Block)
69
+
70
+ traverse(block.expression)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class HashExpression < Expression
5
+ def initialize(pairs)
6
+ super()
7
+
8
+ @pairs = pairs
9
+ end
10
+
11
+ attr_reader :pairs
12
+
13
+ def ==(other)
14
+ return true if equal?(other)
15
+
16
+ other.instance_of?(self.class) &&
17
+ other.pairs.eql?(@pairs)
18
+ end
19
+ alias eql? ==
20
+
21
+ def hash
22
+ @hash ||= [self.class, @pairs].hash
23
+ end
24
+
25
+ def variables
26
+ @variables ||= @pairs.flat_map { |k, v| k.variables + v.variables }.uniq
27
+ end
28
+
29
+ def evaluate(values)
30
+ @pairs.to_h do |k, v|
31
+ [k.evaluate(values), v.evaluate(values)]
32
+ end
33
+ end
34
+
35
+ def substitute(replacements)
36
+ return self unless replacements.keys.intersect?(variables)
37
+
38
+ substituted_pairs = @pairs.map do |k, v|
39
+ [k.substitute(replacements), v.substitute(replacements)]
40
+ end
41
+
42
+ HashExpression.new(substituted_pairs)
43
+ end
44
+
45
+ def to_s
46
+ parts = @pairs.map do |k, v|
47
+ key_part = if k.is_a?(Constant) && k.value.is_a?(Symbol)
48
+ "#{k.value}:"
49
+ else
50
+ "#{k} =>"
51
+ end
52
+
53
+ "#{key_part} #{v}"
54
+ end
55
+
56
+ "{ #{parts.join(', ')} }"
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class ProcExpression < Expression
5
+ def initialize(block, substitution: nil)
6
+ super()
7
+
8
+ check_parameters(block.parameters)
9
+
10
+ @block = block
11
+ @substitution = substitution
12
+ end
13
+
14
+ attr_reader :block, :substitution
15
+
16
+ def check_parameters(parameters)
17
+ parameters.each_with_index do |(type, name), i|
18
+ case type
19
+ when :req, :opt
20
+ raise 'ProcExpression cannot have more than 1 arg' if i > 0
21
+ when :keyreq, :key
22
+ raise 'ProcExpression cannot have an kwarg called "actual"' if name == :actual
23
+ end
24
+ end
25
+ end
26
+
27
+ def variables
28
+ @variables ||= begin
29
+ variables = @block.parameters.filter_map.with_index do |(type, name), i|
30
+ case type
31
+ when :req, :opt
32
+ :actual if i == 0
33
+ when :keyreq, :key
34
+ name
35
+ end
36
+ end
37
+
38
+ @substitution ? variables.map { @substitution[_1] || _1 } : variables
39
+ end
40
+ end
41
+
42
+ def evaluate(values)
43
+ values = substitute_hash(values, @substitution) if @substitution
44
+
45
+ Utils.call_block(@block, values)
46
+ end
47
+
48
+ def ==(other)
49
+ return true if equal?(other)
50
+
51
+ other.instance_of?(ProcExpression) &&
52
+ other.block == @block &&
53
+ other.substitution == @substitution
54
+ end
55
+ alias eql? ==
56
+
57
+ def hash
58
+ [self.class, @block].hash
59
+ end
60
+
61
+ def substitute(replacements)
62
+ replacements = replacements.slice(*variables)
63
+
64
+ return self if replacements.empty?
65
+
66
+ replacements = substitute_hash(@substitution, replacements) if @substitution
67
+
68
+ ProcExpression.new(@block, substitution: replacements)
69
+ end
70
+
71
+ def to_s(substitutions: nil)
72
+ args_and_kwargs = Utils.inspect_block_params(@block)
73
+
74
+ if args_and_kwargs.empty?
75
+ 'expr { ... }'
76
+ else
77
+ "expr { |#{args_and_kwargs}| ... }"
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def substitute_hash(hash, substitution)
84
+ hash.to_h do |k, v|
85
+ k2 = substitution[k]
86
+ v2 = k2.nil? && !substitution.key?(k2) ? v : hash[k2]
87
+
88
+ [k, v2]
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class RangeExpression < Expression
5
+ def initialize(from, to, exclude_end = false)
6
+ super()
7
+
8
+ @begin = from
9
+ @end = to
10
+ @exclude_end = exclude_end
11
+ end
12
+
13
+ attr_reader :begin, :end
14
+
15
+ def exclude_end?
16
+ @exclude_end
17
+ end
18
+
19
+ def ==(other)
20
+ equal?(other) ||
21
+ other.instance_of?(RangeExpression) &&
22
+ other.begin.eql?(@begin) &&
23
+ other.end.eql?(@end) &&
24
+ other.exclude_end?.eql?(@exclude_end)
25
+ end
26
+ alias eql? ==
27
+
28
+ def hash
29
+ [self.class, @begin, @end, @exclude_end].hash
30
+ end
31
+
32
+ def variables
33
+ @variables ||= (@begin.variables + @end.variables).uniq
34
+ end
35
+
36
+ def evaluate(values)
37
+ from = @begin.evaluate(values)
38
+ to = @end.evaluate(values)
39
+
40
+ Range.new(from, to, @exclude_end)
41
+ end
42
+
43
+ def substitute(replacements)
44
+ return self unless replacements.keys.intersect?(variables)
45
+
46
+ from = @begin.substitute(replacements)
47
+ to = @end.substitute(replacements)
48
+
49
+ RangeExpression.new(from, to, @exclude_end)
50
+ end
51
+
52
+ def to_s
53
+ dots = @exclude_end ? '...' : '..'
54
+
55
+ "#{@begin}#{dots}#{@end}"
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class Recorder
5
+ def self.recorder?(object)
6
+ Object.instance_method(:kind_of?)
7
+ .bind_call(object, Recorder)
8
+ end
9
+
10
+ def self.to_expression(recorder)
11
+ Object.instance_method(:instance_variable_get)
12
+ .bind_call(recorder, :@expression)
13
+ end
14
+
15
+ # NOTE: In order for Recorder to work, it can't have any methods except for
16
+ # the ones below.
17
+
18
+ (instance_methods - %i[__id__ __send__ object_id])
19
+ .each { undef_method _1 }
20
+
21
+ def initialize(expression)
22
+ @expression = expression
23
+ end
24
+
25
+ # NOTE: That might be a really nasty thing to do. Let's see how it goes.
26
+ define_method(:object_id) do
27
+ method_missing(:object_id)
28
+ end
29
+
30
+ def method_missing(method, *args, **kwargs, &block)
31
+ # *.hash.to_int indicates that a Hash evaluates this recorder as a key.
32
+ if @hash_parent # @hash_parent is set if @expression is a *.hash call.
33
+ to_int = method == :to_int && args.empty? && kwargs.empty? && !block
34
+
35
+ # Confirm to parent that indeed a Hash called it.
36
+ Object.instance_method(:instance_variable_set)
37
+ .bind_call(@hash_parent, :@hash_confirmed, to_int)
38
+
39
+ return @expression.receiver.hash if to_int
40
+ end
41
+
42
+ # In "uncertain" state: Check if Hash called eql? on this recorder.
43
+ if @hash_caller # @hash_caller indicates the "uncertain" state.
44
+ if method == :eql? && @hash_confirmed && caller[0] == @hash_caller
45
+ # Return proper eql? result.
46
+ return Recorder.recorder?(args[0]) &&
47
+ @expression.eql?(Recorder.to_expression(args[0]))
48
+ else # Not a call from Hash.
49
+ # Leave the "uncertain" state and resume recorder behavior.
50
+ @hash_caller = nil
51
+ @hash_confirmed = false
52
+ end
53
+ end
54
+
55
+ expression_cache = ExpressionCache.current
56
+ args = args.map { Expression.of(_1, expression_cache:) }
57
+ kwargs = kwargs.transform_values { Expression.of(_1, expression_cache:) }
58
+ block = Block.build(expression_cache:, &block) if
59
+ block && !Matcher.settings[:pass_through_blocks]
60
+
61
+ expression = Call.new(@expression, method, args, kwargs, block)
62
+ expression = expression_cache[expression] if expression_cache
63
+ recorder = expression.to_recorder
64
+
65
+ # A Hash might evaluate this recorder as a key.
66
+ if method == :hash && args.empty? && kwargs.empty? && !block
67
+ # If the new recorder registers a *.to_int call then it was called by a
68
+ # Hash.
69
+
70
+ # Enter the "uncertain" state. Save the caller for later check against
71
+ # false positives.
72
+ @hash_caller = caller[0]
73
+
74
+ # Give reference to the new recorder for confirmation.
75
+ Object.instance_method(:instance_variable_set)
76
+ .bind_call(recorder, :@hash_parent, self)
77
+ end
78
+
79
+ recorder
80
+ end
81
+
82
+ def respond_to_missing?(...)
83
+ true
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class RescueLastErrorExpression < Expression
5
+ extend Forwardable
6
+
7
+ def initialize(expression)
8
+ super()
9
+
10
+ @expression = expression
11
+ end
12
+
13
+ attr_reader :expression
14
+
15
+ def_delegator :@expression, :variables
16
+
17
+ def ==(other)
18
+ equal?(other) ||
19
+ other.instance_of?(self.class) &&
20
+ @expression == other.expression
21
+ end
22
+ alias eql? ==
23
+
24
+ def hash
25
+ [self.class, @expression].hash
26
+ end
27
+
28
+ def evaluate(values)
29
+ @expression.evaluate(values)
30
+ rescue CallError => e
31
+ e.cause
32
+ end
33
+
34
+ def substitute(replacements)
35
+ substitution = @expression.substitute(replacements)
36
+
37
+ RescueLastErrorExpression.new(substitution)
38
+ end
39
+
40
+ def to_s(**)
41
+ "#{@expression.to_s(**)} rescue $!"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class SetExpression < Expression
5
+ def initialize(items)
6
+ super()
7
+
8
+ @items = items
9
+ end
10
+
11
+ attr_reader :items
12
+
13
+ def ==(other)
14
+ return true if equal?(other)
15
+
16
+ other.instance_of?(SetExpression) &&
17
+ other.items.eql?(@items)
18
+ end
19
+ alias eql? ==
20
+
21
+ def hash
22
+ @hash ||= [self.class, @items].hash
23
+ end
24
+
25
+ def variables
26
+ @variables ||= @items.flat_map(&:variables).uniq
27
+ end
28
+
29
+ def evaluate(values)
30
+ @items.to_set { _1.evaluate(values) }
31
+ end
32
+
33
+ def substitute(replacements)
34
+ return self unless replacements.keys.intersect?(variables)
35
+
36
+ substituted_items = @items.map { _1.substitute(replacements) }
37
+
38
+ SetExpression.new(substituted_items)
39
+ end
40
+
41
+ def to_s
42
+ "Set[#{@items.map(&:to_s).join(', ')}]"
43
+ end
44
+ end
45
+ end