matchers 0.1.0.pre.1
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.
- checksums.yaml +7 -0
- data/lib/matcher/assertions.rb +19 -0
- data/lib/matcher/autoload.rb +5 -0
- data/lib/matcher/base.rb +183 -0
- data/lib/matcher/compatibility.rb +34 -0
- data/lib/matcher/debug.rb +62 -0
- data/lib/matcher/dsl/builder.rb +99 -0
- data/lib/matcher/dsl/chain.rb +84 -0
- data/lib/matcher/dsl/expression_dsl.rb +306 -0
- data/lib/matcher/dsl/matcher_dsl.rb +5 -0
- data/lib/matcher/dsl/optional.rb +82 -0
- data/lib/matcher/dsl/optional_chain.rb +24 -0
- data/lib/matcher/dsl/others.rb +28 -0
- data/lib/matcher/errors/and_error.rb +88 -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 +100 -0
- data/lib/matcher/errors/nested_error.rb +98 -0
- data/lib/matcher/errors/or_error.rb +88 -0
- data/lib/matcher/expression_cache.rb +57 -0
- data/lib/matcher/expression_labeler.rb +96 -0
- data/lib/matcher/expressions/array_expression.rb +45 -0
- data/lib/matcher/expressions/block.rb +189 -0
- data/lib/matcher/expressions/call.rb +307 -0
- data/lib/matcher/expressions/call_error.rb +45 -0
- data/lib/matcher/expressions/constant.rb +53 -0
- data/lib/matcher/expressions/expression.rb +237 -0
- data/lib/matcher/expressions/expression_walker.rb +77 -0
- data/lib/matcher/expressions/hash_expression.rb +59 -0
- data/lib/matcher/expressions/proc_expression.rb +96 -0
- data/lib/matcher/expressions/range_expression.rb +65 -0
- data/lib/matcher/expressions/recorder.rb +136 -0
- data/lib/matcher/expressions/rescue_last_error_expression.rb +49 -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 +87 -0
- data/lib/matcher/hash_stack.rb +52 -0
- data/lib/matcher/list.rb +102 -0
- data/lib/matcher/markers.rb +7 -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 +34 -0
- data/lib/matcher/matchers/any_matcher.rb +70 -0
- data/lib/matcher/matchers/array_matcher.rb +72 -0
- data/lib/matcher/matchers/block_matcher.rb +61 -0
- data/lib/matcher/matchers/boolean_matcher.rb +37 -0
- data/lib/matcher/matchers/dig_matcher.rb +149 -0
- data/lib/matcher/matchers/each_matcher.rb +85 -0
- data/lib/matcher/matchers/each_pair_matcher.rb +119 -0
- data/lib/matcher/matchers/equal_matcher.rb +198 -0
- data/lib/matcher/matchers/equal_set_matcher.rb +112 -0
- data/lib/matcher/matchers/expression_matcher.rb +69 -0
- data/lib/matcher/matchers/filter_matcher.rb +115 -0
- data/lib/matcher/matchers/hash_matcher.rb +315 -0
- data/lib/matcher/matchers/imply_matcher.rb +83 -0
- data/lib/matcher/matchers/imply_some_matcher.rb +116 -0
- data/lib/matcher/matchers/index_by_matcher.rb +177 -0
- data/lib/matcher/matchers/inline_matcher.rb +101 -0
- data/lib/matcher/matchers/keys_matcher.rb +131 -0
- data/lib/matcher/matchers/kind_of_matcher.rb +35 -0
- data/lib/matcher/matchers/lazy_all_matcher.rb +69 -0
- data/lib/matcher/matchers/lazy_any_matcher.rb +69 -0
- data/lib/matcher/matchers/let_matcher.rb +73 -0
- data/lib/matcher/matchers/map_matcher.rb +148 -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 +25 -0
- data/lib/matcher/matchers/negated_project_matcher.rb +31 -0
- data/lib/matcher/matchers/never_matcher.rb +35 -0
- data/lib/matcher/matchers/one_matcher.rb +68 -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 +101 -0
- data/lib/matcher/matchers/parse_iso8601_helper.rb +41 -0
- data/lib/matcher/matchers/parse_iso8601_matcher.rb +52 -0
- data/lib/matcher/matchers/parse_json_helper.rb +43 -0
- data/lib/matcher/matchers/parse_json_matcher.rb +59 -0
- data/lib/matcher/matchers/project_matcher.rb +72 -0
- data/lib/matcher/matchers/raises_matcher.rb +131 -0
- data/lib/matcher/matchers/range_matcher.rb +50 -0
- data/lib/matcher/matchers/reference_matcher.rb +213 -0
- data/lib/matcher/matchers/reference_matcher_collection.rb +57 -0
- data/lib/matcher/matchers/regexp_matcher.rb +86 -0
- data/lib/matcher/messages/expected_phrasing.rb +355 -0
- data/lib/matcher/messages/message.rb +104 -0
- data/lib/matcher/messages/message_builder.rb +35 -0
- data/lib/matcher/messages/message_rules.rb +240 -0
- data/lib/matcher/messages/namespaced_message_builder.rb +19 -0
- data/lib/matcher/messages/phrasing.rb +59 -0
- data/lib/matcher/messages/standard_message_builder.rb +105 -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 +62 -0
- data/lib/matcher/patterns/pattern.rb +104 -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 +103 -0
- data/lib/matcher/rules/message_factory.rb +26 -0
- data/lib/matcher/rules/message_rule.rb +18 -0
- data/lib/matcher/rules/message_rule_context.rb +26 -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 +514 -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 +107 -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 +346 -0
- metadata +174 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class Call < Expression
|
|
5
|
+
attr_reader :receiver, :method, :args, :kwargs, :block
|
|
6
|
+
|
|
7
|
+
UNARY_OPERATORS =
|
|
8
|
+
%i[! ~ +@ -@].freeze
|
|
9
|
+
BINARY_OPERATORS =
|
|
10
|
+
%i[+ - * ** / % < > <= >= <=> == === != =~ !~ & | ^ << >> && ||].freeze
|
|
11
|
+
|
|
12
|
+
def self.last_assign
|
|
13
|
+
Matcher.build_session&.dig(Call, :last_assign)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.reset_last_assign
|
|
17
|
+
Matcher.build_session&.[](Call)&.delete(:last_assign)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(receiver, method, args = [], kwargs = {}, block = nil)
|
|
21
|
+
super()
|
|
22
|
+
|
|
23
|
+
@receiver = receiver
|
|
24
|
+
@method = method
|
|
25
|
+
@args = args
|
|
26
|
+
@kwargs = kwargs
|
|
27
|
+
@block = block
|
|
28
|
+
|
|
29
|
+
if binary? && Matcher.settings[:logical_operators]
|
|
30
|
+
case method
|
|
31
|
+
when :&
|
|
32
|
+
@method = :"&&"
|
|
33
|
+
when :|
|
|
34
|
+
@method = :"||"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
set_last_assign if method.end_with?("=")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def unary?
|
|
42
|
+
knary?(0)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def binary?
|
|
46
|
+
knary?(1)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def knary?(arity)
|
|
50
|
+
@args.length == arity && @kwargs.empty? && !@block
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def assignment?
|
|
54
|
+
@method.end_with?("=") && !%i[<= >= == === !=].include?(@method)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def precedence
|
|
58
|
+
@precedence ||= begin
|
|
59
|
+
has_precedence = (unary? && UNARY_OPERATORS.include?(@method)) ||
|
|
60
|
+
(binary? && BINARY_OPERATORS.include?(@method))
|
|
61
|
+
|
|
62
|
+
# if method is not an operator then precedence is highest (-1)
|
|
63
|
+
has_precedence ? OPERATOR_PRECEDENCE[@method] : -1
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def evaluate(values)
|
|
68
|
+
receiver = @receiver.evaluate(values)
|
|
69
|
+
|
|
70
|
+
return receiver if lazy?(receiver)
|
|
71
|
+
|
|
72
|
+
args = @args.map { _1.evaluate(values) }
|
|
73
|
+
kwargs = @kwargs.transform_values { _1.evaluate(values) }
|
|
74
|
+
|
|
75
|
+
return args[0] if logical_operator?
|
|
76
|
+
|
|
77
|
+
invoke(values, receiver, args, kwargs)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def evaluate_tree(values)
|
|
81
|
+
receiver_t = @receiver.evaluate_tree(values)
|
|
82
|
+
receiver = receiver_t.last
|
|
83
|
+
|
|
84
|
+
return [receiver_t, nil, nil, receiver] if lazy?(receiver)
|
|
85
|
+
|
|
86
|
+
args, args_t = evaluate_args_tree(values)
|
|
87
|
+
kwargs, kwargs_t = evaluate_kwargs_tree(values)
|
|
88
|
+
|
|
89
|
+
return [receiver_t, args_t, kwargs_t, args[0]] if logical_operator?
|
|
90
|
+
|
|
91
|
+
value = invoke(values, receiver, args, kwargs)
|
|
92
|
+
|
|
93
|
+
[receiver_t, args_t, kwargs_t, value]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def variables
|
|
97
|
+
@variables ||= begin
|
|
98
|
+
variables = @receiver.variables +
|
|
99
|
+
@args.flat_map(&:variables) +
|
|
100
|
+
@kwargs.each_value.flat_map(&:variables)
|
|
101
|
+
|
|
102
|
+
variables.concat(@block.variables) if @block
|
|
103
|
+
|
|
104
|
+
variables.uniq
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def ==(other)
|
|
109
|
+
return true if equal?(other)
|
|
110
|
+
|
|
111
|
+
other.instance_of?(Call) &&
|
|
112
|
+
other.receiver == @receiver &&
|
|
113
|
+
other.method == @method &&
|
|
114
|
+
other.args.eql?(@args) &&
|
|
115
|
+
other.kwargs.eql?(@kwargs) &&
|
|
116
|
+
other.block == @block
|
|
117
|
+
end
|
|
118
|
+
alias eql? ==
|
|
119
|
+
|
|
120
|
+
def hash
|
|
121
|
+
@hash ||= [self.class, @receiver, @args, @method, @kwargs, @block].hash
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def visit(&)
|
|
125
|
+
return to_enum(:visit) unless block_given?
|
|
126
|
+
|
|
127
|
+
@receiver.visit(&)
|
|
128
|
+
@args.each { _1.visit(&) }
|
|
129
|
+
@kwargs.each_value { _1.visit(&) }
|
|
130
|
+
|
|
131
|
+
yield self
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def substitute(replacements)
|
|
135
|
+
replacement_names = replacements.keys
|
|
136
|
+
|
|
137
|
+
return self unless replacement_names.intersect?(variables)
|
|
138
|
+
|
|
139
|
+
receiver = @receiver.substitute(replacements)
|
|
140
|
+
|
|
141
|
+
no_change = nil
|
|
142
|
+
substitute = lambda do |expression|
|
|
143
|
+
result = expression.substitute(replacements)
|
|
144
|
+
no_change = false unless result.equal?(expression)
|
|
145
|
+
|
|
146
|
+
result
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
no_change = true
|
|
150
|
+
args = @args.map(&substitute)
|
|
151
|
+
args = @args if no_change
|
|
152
|
+
|
|
153
|
+
no_change = true
|
|
154
|
+
kwargs = @kwargs.transform_values(&substitute)
|
|
155
|
+
kwargs = @kwargs if no_change
|
|
156
|
+
|
|
157
|
+
block = @block.is_a?(Block) ? @block.substitute(replacements) : @block
|
|
158
|
+
|
|
159
|
+
Call.new(receiver, @method, args, kwargs, block)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def to_s
|
|
163
|
+
receiver = parenthesized_receiver
|
|
164
|
+
|
|
165
|
+
case @method
|
|
166
|
+
when :!, :~, :+@, :-@
|
|
167
|
+
# !foo
|
|
168
|
+
return "#{@method[0]}#{receiver}" if unary?
|
|
169
|
+
when :+, :-, :*, :/, :%, :**, :<, :>, :<=, :>=, :<=>, :==, :===, :!=, :=~,
|
|
170
|
+
:!~, :&, :|, :^, :<<, :>>, :"&&", :"||"
|
|
171
|
+
|
|
172
|
+
if binary?
|
|
173
|
+
# foo**2
|
|
174
|
+
return "#{receiver}**#{parenthesized_operand}" if @method == :**
|
|
175
|
+
|
|
176
|
+
# foo + bar
|
|
177
|
+
return "#{receiver} #{@method} #{parenthesized_operand}"
|
|
178
|
+
end
|
|
179
|
+
when :[]
|
|
180
|
+
# foo[a, b, ...]
|
|
181
|
+
return "#{receiver}[#{args_and_kwargs_string}]#{block_string}"
|
|
182
|
+
when :[]=
|
|
183
|
+
# foo[a, b, ...] = 1
|
|
184
|
+
if @args.length >= 2 && @kwargs.empty? && !@block
|
|
185
|
+
first_args = @args[0..-2].join(", ")
|
|
186
|
+
last_arg = @args[-1].to_s
|
|
187
|
+
|
|
188
|
+
return "#{receiver}[#{first_args}] = #{last_arg}"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
if @method.end_with?("=") && @method != :[]= && binary?
|
|
193
|
+
# foo.bar = 42
|
|
194
|
+
|
|
195
|
+
"#{receiver}.#{@method[0..-2]} = #{@args[0]}"
|
|
196
|
+
else
|
|
197
|
+
# foo.bar OR foo.bar(arg1, arg2, ...)
|
|
198
|
+
|
|
199
|
+
is_kernel = @receiver.is_a?(Constant) && @receiver.value == Kernel
|
|
200
|
+
args_and_kwargs = args_and_kwargs_string
|
|
201
|
+
string = is_kernel ? @method.to_s : "#{receiver}.#{@method}"
|
|
202
|
+
string += "(#{args_and_kwargs})" unless args_and_kwargs.empty?
|
|
203
|
+
string += block_string
|
|
204
|
+
|
|
205
|
+
string
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
private
|
|
210
|
+
|
|
211
|
+
def lazy?(receiver)
|
|
212
|
+
@method == :"&&" && !receiver || @method == :"||" && receiver
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def logical_operator?
|
|
216
|
+
%i[&& ||].include?(@method)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def invoke(values, receiver, args, kwargs)
|
|
220
|
+
block = @block.is_a?(Block) ? @block&.to_proc(values:) : @block
|
|
221
|
+
|
|
222
|
+
begin
|
|
223
|
+
result = receiver.send(@method, *args, **kwargs, &block)
|
|
224
|
+
assignment? ? args.last : result
|
|
225
|
+
rescue StandardError => e
|
|
226
|
+
message = "#{self} raised #{e.class}: #{e.message}"
|
|
227
|
+
given = given_for(values)
|
|
228
|
+
|
|
229
|
+
raise CallError.new(message, self, given)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def set_last_assign
|
|
234
|
+
build_session = Matcher.build_session
|
|
235
|
+
|
|
236
|
+
return unless build_session
|
|
237
|
+
|
|
238
|
+
call_session = (build_session[Call] ||= {})
|
|
239
|
+
call_session[:last_assign] = self
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def evaluate_args_tree(values)
|
|
243
|
+
n = @args.length
|
|
244
|
+
args = Array.new(n)
|
|
245
|
+
args_t = Array.new(n)
|
|
246
|
+
|
|
247
|
+
@args.each_with_index do |arg, i|
|
|
248
|
+
arg_t = arg.evaluate_tree(values)
|
|
249
|
+
args[i] = arg_t.last
|
|
250
|
+
args_t[i] = arg_t
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
[args, args_t]
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def evaluate_kwargs_tree(values)
|
|
257
|
+
kwargs = {}
|
|
258
|
+
kwargs_t = {}
|
|
259
|
+
|
|
260
|
+
@kwargs.each do |key, kwarg|
|
|
261
|
+
kwarg_t = kwarg.evaluate_tree(values)
|
|
262
|
+
kwargs[key] = kwarg_t.last
|
|
263
|
+
kwargs_t[key] = kwarg_t
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
[kwargs, kwargs_t]
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def args_and_kwargs_string
|
|
270
|
+
args = @args.map(&:to_s)
|
|
271
|
+
kwargs = @kwargs.map do |k, v|
|
|
272
|
+
v_to_s = v.to_s
|
|
273
|
+
|
|
274
|
+
if k.is_a?(Symbol)
|
|
275
|
+
"#{k}: #{v_to_s}"
|
|
276
|
+
else
|
|
277
|
+
"#{k.inspect} => #{v_to_s}"
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
list = args + kwargs
|
|
282
|
+
list << "&#{@block.symbol.inspect}" if @block.is_a?(SymbolProc)
|
|
283
|
+
|
|
284
|
+
list.join(", ")
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def block_string
|
|
288
|
+
if @block.is_a?(Block)
|
|
289
|
+
" #{@block.to_s(as_block: true)}"
|
|
290
|
+
elsif @block && !@block.is_a?(SymbolProc)
|
|
291
|
+
" { ... }"
|
|
292
|
+
else
|
|
293
|
+
""
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def parenthesized_receiver
|
|
298
|
+
non_associative = %i[<=> == === != =~ !~].include?(@method)
|
|
299
|
+
|
|
300
|
+
@receiver.parenthesize(precedence, non_associative)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def parenthesized_operand
|
|
304
|
+
@args[0].parenthesize(precedence, true)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class CallError < StandardError
|
|
5
|
+
attr_reader :call, :given
|
|
6
|
+
|
|
7
|
+
def initialize(message, call, given)
|
|
8
|
+
super(message)
|
|
9
|
+
|
|
10
|
+
@call = call
|
|
11
|
+
@given = given
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def message_for_errors(actual)
|
|
15
|
+
case cause
|
|
16
|
+
when NoMethodError
|
|
17
|
+
not_responding_message(actual)
|
|
18
|
+
else
|
|
19
|
+
raising_message(actual)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def raising_message(actual)
|
|
26
|
+
Message.new(%i[expression raising], false, actual, @call, cause, @given)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def not_responding_message(actual)
|
|
30
|
+
if @call.receiver == Variable.actual
|
|
31
|
+
Message.new(:responding_to, true, cause.receiver, @call.method)
|
|
32
|
+
else
|
|
33
|
+
Message.new(
|
|
34
|
+
%i[expression responding_to],
|
|
35
|
+
true,
|
|
36
|
+
actual,
|
|
37
|
+
@call.receiver,
|
|
38
|
+
cause.receiver,
|
|
39
|
+
@call.method,
|
|
40
|
+
@given,
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class Constant < Expression
|
|
5
|
+
def self.cache(value, expression_cache = ExpressionCache.current)
|
|
6
|
+
if expression_cache
|
|
7
|
+
expression_cache.constant_for(value)
|
|
8
|
+
else
|
|
9
|
+
Constant.new(value)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :value
|
|
14
|
+
|
|
15
|
+
def initialize(value)
|
|
16
|
+
super()
|
|
17
|
+
|
|
18
|
+
@value = value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def negated
|
|
22
|
+
Constant.new(!@value)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def variables
|
|
26
|
+
[]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def evaluate(_values)
|
|
30
|
+
@value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def ==(other)
|
|
34
|
+
return true if equal?(other)
|
|
35
|
+
|
|
36
|
+
other.instance_of?(Constant) &&
|
|
37
|
+
@value.eql?(other.value)
|
|
38
|
+
end
|
|
39
|
+
alias eql? ==
|
|
40
|
+
|
|
41
|
+
def hash
|
|
42
|
+
@hash ||= [self.class, @value].hash
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def substitute(_replacements)
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_s
|
|
50
|
+
@value.inspect
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
##
|
|
5
|
+
# Expressions are a central feature of this library. They are used for:
|
|
6
|
+
#
|
|
7
|
+
# - building ad-hoc matchers (e.g. <tt>_ > 10</tt> , +_.even?+ )
|
|
8
|
+
# - tracking where match errors happen
|
|
9
|
+
# (e.g. <tt>root[:name]: expected ...</tt>)
|
|
10
|
+
# - as parameters for other matchers like +map+ where they take the role of
|
|
11
|
+
# anonymous functions (e.g. <tt>map(_.to_s, "some_string")</tt> )
|
|
12
|
+
#
|
|
13
|
+
# Helpers (like +map+) use expressions instead of procs because the AST of an
|
|
14
|
+
# Expression can be inspected and transformed. This is useful when building
|
|
15
|
+
# the path and message of errors.
|
|
16
|
+
#
|
|
17
|
+
# my_expression = Matcher::Expression.build { _ * 21 }
|
|
18
|
+
# my_expression.evaluate(actual: 2) # => 42
|
|
19
|
+
# # proc equivalent:
|
|
20
|
+
# ->(x) { x * 21 }
|
|
21
|
+
#
|
|
22
|
+
# Have a look at {Recorder} where we explain how reorders are used to build
|
|
23
|
+
# expressions.
|
|
24
|
+
#
|
|
25
|
+
# @see Recorder
|
|
26
|
+
class Expression
|
|
27
|
+
class ExpressionBuilder
|
|
28
|
+
include ExpressionDsl
|
|
29
|
+
|
|
30
|
+
def initialize(build_session: Matcher.build_session)
|
|
31
|
+
ExpressionDsl.init(self, build_session)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
# Builds an expression conveniently using {Recorder} and helpers from
|
|
37
|
+
# {ExpressionDsl}.
|
|
38
|
+
#
|
|
39
|
+
# @example
|
|
40
|
+
# Matcher::Expression.build do
|
|
41
|
+
# _.sum(&:to_i)
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# Matcher::Expression.build do
|
|
45
|
+
# range(vars[:from], vars[:to]).include?(_)
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# @see ExpressionDsl
|
|
49
|
+
def self.build(&)
|
|
50
|
+
Matcher.with_build_session do |build_session|
|
|
51
|
+
builder = ExpressionBuilder.new(build_session:)
|
|
52
|
+
result = builder.instance_exec(&)
|
|
53
|
+
builder.expression_of(result)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.expression_or_value(obj, expression_cache: nil)
|
|
58
|
+
case obj
|
|
59
|
+
when -> { Recorder.recorder?(_1) }
|
|
60
|
+
return Recorder.to_expression(obj)
|
|
61
|
+
when Base
|
|
62
|
+
raise ArgumentError, "Cannot use matcher as expression"
|
|
63
|
+
when NoExpression
|
|
64
|
+
raise ArgumentError, "Cannot use #{obj.class} as expression"
|
|
65
|
+
when Proc
|
|
66
|
+
raise ArgumentError, "Cannot use Proc as expression. " \
|
|
67
|
+
"Use `expr { ... }' instead"
|
|
68
|
+
when Array
|
|
69
|
+
items = obj.map { expression_or_value(_1, expression_cache:) }
|
|
70
|
+
|
|
71
|
+
if items.any?(Expression)
|
|
72
|
+
items.each_with_index do |item, i|
|
|
73
|
+
unless item.is_a?(Expression)
|
|
74
|
+
items[i] = Constant.cache(item, expression_cache)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
return ArrayExpression.new(items)
|
|
79
|
+
end
|
|
80
|
+
when Hash
|
|
81
|
+
pairs = obj.map do |key, value|
|
|
82
|
+
key = expression_or_value(key, expression_cache:)
|
|
83
|
+
value = expression_or_value(value, expression_cache:)
|
|
84
|
+
|
|
85
|
+
[key, value]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if pairs.any? { |k, v| k.is_a?(Expression) || v.is_a?(Expression) }
|
|
89
|
+
pairs.each do |pair|
|
|
90
|
+
k, v = pair
|
|
91
|
+
|
|
92
|
+
unless k.is_a?(Expression)
|
|
93
|
+
pair[0] = Constant.cache(k, expression_cache)
|
|
94
|
+
end
|
|
95
|
+
unless v.is_a?(Expression)
|
|
96
|
+
pair[1] = Constant.cache(v, expression_cache)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
return HashExpression.new(pairs)
|
|
101
|
+
end
|
|
102
|
+
when Range
|
|
103
|
+
from = expression_or_value(obj.begin, expression_cache:)
|
|
104
|
+
to = expression_or_value(obj.end, expression_cache:)
|
|
105
|
+
|
|
106
|
+
if from.is_a?(Expression) || to.is_a?(Expression)
|
|
107
|
+
unless from.is_a?(Expression)
|
|
108
|
+
from = Constant.cache(from, expression_cache)
|
|
109
|
+
end
|
|
110
|
+
to = Constant.cache(to, expression_cache) unless to.is_a?(Expression)
|
|
111
|
+
|
|
112
|
+
return RangeExpression.new(from, to, exclude_end: obj.exclude_end?)
|
|
113
|
+
end
|
|
114
|
+
when Set
|
|
115
|
+
items = obj.map { expression_or_value(_1, expression_cache:) }
|
|
116
|
+
|
|
117
|
+
if items.any?(Expression)
|
|
118
|
+
items.each_with_index do |item, i|
|
|
119
|
+
unless item.is_a?(Expression)
|
|
120
|
+
items[i] = Constant.cache(item, expression_cache)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
return SetExpression.new(items)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
obj
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.of(obj, expression_cache: nil)
|
|
132
|
+
obj = expression_or_value(obj, expression_cache:)
|
|
133
|
+
|
|
134
|
+
if obj.is_a?(Expression)
|
|
135
|
+
obj
|
|
136
|
+
else
|
|
137
|
+
Constant.cache(obj, expression_cache)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.try_recorder(obj)
|
|
142
|
+
return obj unless Recorder.recorder?(obj)
|
|
143
|
+
|
|
144
|
+
Recorder.to_expression(obj)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def initialize
|
|
148
|
+
raise "abstract class" if instance_of?(Expression)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def evaluate_tree(values)
|
|
152
|
+
[evaluate(values)]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def given_for(values)
|
|
156
|
+
variables.to_h { [_1, values[_1]] }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def visit
|
|
160
|
+
return to_enum(:visit) unless block_given?
|
|
161
|
+
|
|
162
|
+
yield self
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def free_symbol(symbol)
|
|
166
|
+
parameters = ExpressionWalker.each_block(self).flat_map do |block|
|
|
167
|
+
block.parameters.map { |_type, name| name }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
identifiers = (variables + parameters).to_set
|
|
171
|
+
|
|
172
|
+
return symbol unless identifiers.include?(symbol)
|
|
173
|
+
|
|
174
|
+
i = 2
|
|
175
|
+
loop do
|
|
176
|
+
symbol_i = :"#{symbol}#{i}"
|
|
177
|
+
|
|
178
|
+
return symbol_i unless identifiers.include?(symbol_i)
|
|
179
|
+
|
|
180
|
+
i += 1
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
OPERATOR_PRECEDENCE = begin
|
|
185
|
+
precedence = {}
|
|
186
|
+
|
|
187
|
+
# see https://docs.ruby-lang.org/en/master/syntax/precedence_rdoc.html
|
|
188
|
+
[
|
|
189
|
+
%i[! ~ +@],
|
|
190
|
+
%i[**],
|
|
191
|
+
%i[-@],
|
|
192
|
+
%i[* / %],
|
|
193
|
+
%i[+ -],
|
|
194
|
+
%i[<< >>],
|
|
195
|
+
%i[&],
|
|
196
|
+
%i[| ^],
|
|
197
|
+
%i[> >= < <=],
|
|
198
|
+
%i[<=> == === != =~ !~],
|
|
199
|
+
%i[&&],
|
|
200
|
+
%i[||],
|
|
201
|
+
%i[..],
|
|
202
|
+
%i[modifier_rescue],
|
|
203
|
+
].each_with_index do |operators, index|
|
|
204
|
+
operators.each { precedence[_1] = index }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
precedence.freeze
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def precedence
|
|
211
|
+
# highest precedence, won't need parentheses
|
|
212
|
+
-1
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def parenthesize(precedence, when_equal)
|
|
216
|
+
# Parenthesize if own precedence is lower than the other. In some
|
|
217
|
+
# situations (as right hand side or for non-associative operators) we also
|
|
218
|
+
# parenthesize when precedence is equal.
|
|
219
|
+
|
|
220
|
+
need_parentheses = if when_equal
|
|
221
|
+
self.precedence >= precedence
|
|
222
|
+
else
|
|
223
|
+
self.precedence > precedence
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
need_parentheses ? "(#{self})" : to_s
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def inspect
|
|
230
|
+
to_s
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def to_recorder
|
|
234
|
+
Recorder.new(self)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ExpressionWalker
|
|
5
|
+
attr_accessor :constant_visitor,
|
|
6
|
+
:variable_visitor,
|
|
7
|
+
:call_visitor,
|
|
8
|
+
:block_visitor,
|
|
9
|
+
:proc_expression_visitor
|
|
10
|
+
|
|
11
|
+
def self.each_variable(expression, &block)
|
|
12
|
+
return to_enum(:each_variable, expression) unless block_given?
|
|
13
|
+
|
|
14
|
+
walker = new(expression)
|
|
15
|
+
walker.variable_visitor = block
|
|
16
|
+
walker.walk
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.each_block(expression, &block)
|
|
20
|
+
return to_enum(:each_block, expression) unless block_given?
|
|
21
|
+
|
|
22
|
+
walker = new(expression)
|
|
23
|
+
walker.block_visitor = block
|
|
24
|
+
walker.walk
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(expression)
|
|
28
|
+
@expression = expression
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def walk
|
|
32
|
+
traverse(@expression)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def traverse(expression)
|
|
38
|
+
case expression
|
|
39
|
+
when Constant
|
|
40
|
+
@constant_visitor&.call(expression)
|
|
41
|
+
when Variable
|
|
42
|
+
@variable_visitor&.call(expression)
|
|
43
|
+
when Call
|
|
44
|
+
@call_visitor&.call(expression)
|
|
45
|
+
|
|
46
|
+
traverse(expression.receiver)
|
|
47
|
+
expression.args.each { traverse(_1) }
|
|
48
|
+
expression.kwargs.each { traverse(_2) }
|
|
49
|
+
traverse_block(expression.block) if expression.block
|
|
50
|
+
when ProcExpression
|
|
51
|
+
@proc_expression_visitor&.call(expression)
|
|
52
|
+
when ArrayExpression, SetExpression
|
|
53
|
+
expression.items.each { traverse(_1) }
|
|
54
|
+
when HashExpression
|
|
55
|
+
expression.pairs.each do |k, v|
|
|
56
|
+
traverse(k)
|
|
57
|
+
traverse(v)
|
|
58
|
+
end
|
|
59
|
+
when RangeExpression
|
|
60
|
+
traverse(expression.begin)
|
|
61
|
+
traverse(expression.end)
|
|
62
|
+
when RescueLastErrorExpression
|
|
63
|
+
traverse(expression.expression)
|
|
64
|
+
else
|
|
65
|
+
raise "unsupported expression type: #{expression.class}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def traverse_block(block)
|
|
70
|
+
@block_visitor&.call(block)
|
|
71
|
+
|
|
72
|
+
return unless block.is_a?(Block)
|
|
73
|
+
|
|
74
|
+
traverse(block.expression)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|