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.
- checksums.yaml +7 -0
- data/lib/matcher/assertions.rb +21 -0
- data/lib/matcher/base.rb +189 -0
- data/lib/matcher/builder.rb +74 -0
- data/lib/matcher/chain.rb +60 -0
- data/lib/matcher/debug.rb +48 -0
- data/lib/matcher/errors/and_error.rb +86 -0
- data/lib/matcher/errors/boolean_collector.rb +51 -0
- data/lib/matcher/errors/element_error.rb +24 -0
- data/lib/matcher/errors/empty_error.rb +23 -0
- data/lib/matcher/errors/error.rb +39 -0
- data/lib/matcher/errors/error_collector.rb +99 -0
- data/lib/matcher/errors/nested_error.rb +96 -0
- data/lib/matcher/errors/or_error.rb +86 -0
- data/lib/matcher/expression_cache.rb +57 -0
- data/lib/matcher/expression_labeler.rb +91 -0
- data/lib/matcher/expressions/array_expression.rb +45 -0
- data/lib/matcher/expressions/block.rb +153 -0
- data/lib/matcher/expressions/call.rb +338 -0
- data/lib/matcher/expressions/call_error.rb +45 -0
- data/lib/matcher/expressions/constant.rb +53 -0
- data/lib/matcher/expressions/expression.rb +147 -0
- data/lib/matcher/expressions/expression_building.rb +258 -0
- data/lib/matcher/expressions/expression_walker.rb +73 -0
- data/lib/matcher/expressions/hash_expression.rb +59 -0
- data/lib/matcher/expressions/proc_expression.rb +92 -0
- data/lib/matcher/expressions/range_expression.rb +58 -0
- data/lib/matcher/expressions/recorder.rb +86 -0
- data/lib/matcher/expressions/rescue_last_error_expression.rb +44 -0
- data/lib/matcher/expressions/set_expression.rb +45 -0
- data/lib/matcher/expressions/string_expression.rb +53 -0
- data/lib/matcher/expressions/symbol_proc.rb +53 -0
- data/lib/matcher/expressions/variable.rb +85 -0
- data/lib/matcher/hash_stack.rb +53 -0
- data/lib/matcher/list.rb +102 -0
- data/lib/matcher/markers/optional.rb +80 -0
- data/lib/matcher/markers/others.rb +28 -0
- data/lib/matcher/matcher_cache.rb +18 -0
- data/lib/matcher/matchers/all_matcher.rb +60 -0
- data/lib/matcher/matchers/always_matcher.rb +28 -0
- data/lib/matcher/matchers/any_matcher.rb +70 -0
- data/lib/matcher/matchers/array_matcher.rb +35 -0
- data/lib/matcher/matchers/block_matcher.rb +59 -0
- data/lib/matcher/matchers/boolean_matcher.rb +35 -0
- data/lib/matcher/matchers/dig_matcher.rb +146 -0
- data/lib/matcher/matchers/each_matcher.rb +52 -0
- data/lib/matcher/matchers/each_pair_matcher.rb +119 -0
- data/lib/matcher/matchers/equal_matcher.rb +197 -0
- data/lib/matcher/matchers/equal_set_matcher.rb +99 -0
- data/lib/matcher/matchers/expression_matcher.rb +73 -0
- data/lib/matcher/matchers/filter_matcher.rb +111 -0
- data/lib/matcher/matchers/hash_matcher.rb +223 -0
- data/lib/matcher/matchers/imply_matcher.rb +81 -0
- data/lib/matcher/matchers/imply_some_matcher.rb +112 -0
- data/lib/matcher/matchers/index_by_matcher.rb +175 -0
- data/lib/matcher/matchers/inline_matcher.rb +99 -0
- data/lib/matcher/matchers/keys_matcher.rb +121 -0
- data/lib/matcher/matchers/kind_of_matcher.rb +35 -0
- data/lib/matcher/matchers/lazy_all_matcher.rb +68 -0
- data/lib/matcher/matchers/lazy_any_matcher.rb +68 -0
- data/lib/matcher/matchers/let_matcher.rb +73 -0
- data/lib/matcher/matchers/map_matcher.rb +129 -0
- data/lib/matcher/matchers/matcher_building.rb +5 -0
- data/lib/matcher/matchers/negated_array_matcher.rb +38 -0
- data/lib/matcher/matchers/negated_each_matcher.rb +36 -0
- data/lib/matcher/matchers/negated_each_pair_matcher.rb +38 -0
- data/lib/matcher/matchers/negated_imply_some_matcher.rb +46 -0
- data/lib/matcher/matchers/negated_matcher.rb +23 -0
- data/lib/matcher/matchers/negated_project_matcher.rb +31 -0
- data/lib/matcher/matchers/never_matcher.rb +29 -0
- data/lib/matcher/matchers/one_matcher.rb +70 -0
- data/lib/matcher/matchers/optional_matcher.rb +38 -0
- data/lib/matcher/matchers/parse_float_matcher.rb +86 -0
- data/lib/matcher/matchers/parse_integer_matcher.rb +98 -0
- data/lib/matcher/matchers/parse_iso8601_matcher.rb +92 -0
- data/lib/matcher/matchers/parse_json_matcher.rb +95 -0
- data/lib/matcher/matchers/project_matcher.rb +68 -0
- data/lib/matcher/matchers/raises_matcher.rb +124 -0
- data/lib/matcher/matchers/range_matcher.rb +47 -0
- data/lib/matcher/matchers/reference_matcher.rb +111 -0
- data/lib/matcher/matchers/reference_matcher_collection.rb +57 -0
- data/lib/matcher/matchers/regexp_matcher.rb +84 -0
- data/lib/matcher/messages/expected_phrasing.rb +342 -0
- data/lib/matcher/messages/message.rb +102 -0
- data/lib/matcher/messages/message_builder.rb +35 -0
- data/lib/matcher/messages/message_rules.rb +223 -0
- data/lib/matcher/messages/namespaced_message_builder.rb +19 -0
- data/lib/matcher/messages/phrasing.rb +57 -0
- data/lib/matcher/messages/standard_message_builder.rb +105 -0
- data/lib/matcher/once_before.rb +18 -0
- data/lib/matcher/optional_chain.rb +24 -0
- data/lib/matcher/patterns/ast_mapping.rb +42 -0
- data/lib/matcher/patterns/capture_hole.rb +33 -0
- data/lib/matcher/patterns/constant_hole.rb +14 -0
- data/lib/matcher/patterns/hole.rb +30 -0
- data/lib/matcher/patterns/method_hole.rb +58 -0
- data/lib/matcher/patterns/pattern.rb +92 -0
- data/lib/matcher/patterns/pattern_building.rb +39 -0
- data/lib/matcher/patterns/pattern_capture.rb +11 -0
- data/lib/matcher/patterns/pattern_match.rb +29 -0
- data/lib/matcher/patterns/variable_hole.rb +14 -0
- data/lib/matcher/reporter.rb +98 -0
- data/lib/matcher/rules/message_factory.rb +25 -0
- data/lib/matcher/rules/message_rule.rb +18 -0
- data/lib/matcher/rules/message_rule_context.rb +24 -0
- data/lib/matcher/rules/rule_builder.rb +29 -0
- data/lib/matcher/rules/rule_set.rb +57 -0
- data/lib/matcher/rules/transform_builder.rb +24 -0
- data/lib/matcher/rules/transform_mapping.rb +5 -0
- data/lib/matcher/rules/transform_rule.rb +21 -0
- data/lib/matcher/state.rb +40 -0
- data/lib/matcher/testing/error_builder.rb +62 -0
- data/lib/matcher/testing/error_checker.rb +496 -0
- data/lib/matcher/testing/error_testing.rb +37 -0
- data/lib/matcher/testing/pattern_testing.rb +11 -0
- data/lib/matcher/testing/pattern_testing_scope.rb +34 -0
- data/lib/matcher/testing.rb +102 -0
- data/lib/matcher/undefined.rb +10 -0
- data/lib/matcher/utils/mapping_utils.rb +61 -0
- data/lib/matcher/utils.rb +72 -0
- data/lib/matcher/version.rb +5 -0
- data/lib/matcher.rb +337 -0
- 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
|