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.
Files changed (127) hide show
  1. checksums.yaml +7 -0
  2. data/lib/matcher/assertions.rb +19 -0
  3. data/lib/matcher/autoload.rb +5 -0
  4. data/lib/matcher/base.rb +183 -0
  5. data/lib/matcher/compatibility.rb +34 -0
  6. data/lib/matcher/debug.rb +62 -0
  7. data/lib/matcher/dsl/builder.rb +99 -0
  8. data/lib/matcher/dsl/chain.rb +84 -0
  9. data/lib/matcher/dsl/expression_dsl.rb +306 -0
  10. data/lib/matcher/dsl/matcher_dsl.rb +5 -0
  11. data/lib/matcher/dsl/optional.rb +82 -0
  12. data/lib/matcher/dsl/optional_chain.rb +24 -0
  13. data/lib/matcher/dsl/others.rb +28 -0
  14. data/lib/matcher/errors/and_error.rb +88 -0
  15. data/lib/matcher/errors/boolean_collector.rb +51 -0
  16. data/lib/matcher/errors/element_error.rb +24 -0
  17. data/lib/matcher/errors/empty_error.rb +23 -0
  18. data/lib/matcher/errors/error.rb +39 -0
  19. data/lib/matcher/errors/error_collector.rb +100 -0
  20. data/lib/matcher/errors/nested_error.rb +98 -0
  21. data/lib/matcher/errors/or_error.rb +88 -0
  22. data/lib/matcher/expression_cache.rb +57 -0
  23. data/lib/matcher/expression_labeler.rb +96 -0
  24. data/lib/matcher/expressions/array_expression.rb +45 -0
  25. data/lib/matcher/expressions/block.rb +189 -0
  26. data/lib/matcher/expressions/call.rb +307 -0
  27. data/lib/matcher/expressions/call_error.rb +45 -0
  28. data/lib/matcher/expressions/constant.rb +53 -0
  29. data/lib/matcher/expressions/expression.rb +237 -0
  30. data/lib/matcher/expressions/expression_walker.rb +77 -0
  31. data/lib/matcher/expressions/hash_expression.rb +59 -0
  32. data/lib/matcher/expressions/proc_expression.rb +96 -0
  33. data/lib/matcher/expressions/range_expression.rb +65 -0
  34. data/lib/matcher/expressions/recorder.rb +136 -0
  35. data/lib/matcher/expressions/rescue_last_error_expression.rb +49 -0
  36. data/lib/matcher/expressions/set_expression.rb +45 -0
  37. data/lib/matcher/expressions/string_expression.rb +53 -0
  38. data/lib/matcher/expressions/symbol_proc.rb +53 -0
  39. data/lib/matcher/expressions/variable.rb +87 -0
  40. data/lib/matcher/hash_stack.rb +52 -0
  41. data/lib/matcher/list.rb +102 -0
  42. data/lib/matcher/markers.rb +7 -0
  43. data/lib/matcher/matcher_cache.rb +18 -0
  44. data/lib/matcher/matchers/all_matcher.rb +60 -0
  45. data/lib/matcher/matchers/always_matcher.rb +34 -0
  46. data/lib/matcher/matchers/any_matcher.rb +70 -0
  47. data/lib/matcher/matchers/array_matcher.rb +72 -0
  48. data/lib/matcher/matchers/block_matcher.rb +61 -0
  49. data/lib/matcher/matchers/boolean_matcher.rb +37 -0
  50. data/lib/matcher/matchers/dig_matcher.rb +149 -0
  51. data/lib/matcher/matchers/each_matcher.rb +85 -0
  52. data/lib/matcher/matchers/each_pair_matcher.rb +119 -0
  53. data/lib/matcher/matchers/equal_matcher.rb +198 -0
  54. data/lib/matcher/matchers/equal_set_matcher.rb +112 -0
  55. data/lib/matcher/matchers/expression_matcher.rb +69 -0
  56. data/lib/matcher/matchers/filter_matcher.rb +115 -0
  57. data/lib/matcher/matchers/hash_matcher.rb +315 -0
  58. data/lib/matcher/matchers/imply_matcher.rb +83 -0
  59. data/lib/matcher/matchers/imply_some_matcher.rb +116 -0
  60. data/lib/matcher/matchers/index_by_matcher.rb +177 -0
  61. data/lib/matcher/matchers/inline_matcher.rb +101 -0
  62. data/lib/matcher/matchers/keys_matcher.rb +131 -0
  63. data/lib/matcher/matchers/kind_of_matcher.rb +35 -0
  64. data/lib/matcher/matchers/lazy_all_matcher.rb +69 -0
  65. data/lib/matcher/matchers/lazy_any_matcher.rb +69 -0
  66. data/lib/matcher/matchers/let_matcher.rb +73 -0
  67. data/lib/matcher/matchers/map_matcher.rb +148 -0
  68. data/lib/matcher/matchers/negated_array_matcher.rb +38 -0
  69. data/lib/matcher/matchers/negated_each_matcher.rb +36 -0
  70. data/lib/matcher/matchers/negated_each_pair_matcher.rb +38 -0
  71. data/lib/matcher/matchers/negated_imply_some_matcher.rb +46 -0
  72. data/lib/matcher/matchers/negated_matcher.rb +25 -0
  73. data/lib/matcher/matchers/negated_project_matcher.rb +31 -0
  74. data/lib/matcher/matchers/never_matcher.rb +35 -0
  75. data/lib/matcher/matchers/one_matcher.rb +68 -0
  76. data/lib/matcher/matchers/optional_matcher.rb +38 -0
  77. data/lib/matcher/matchers/parse_float_matcher.rb +86 -0
  78. data/lib/matcher/matchers/parse_integer_matcher.rb +101 -0
  79. data/lib/matcher/matchers/parse_iso8601_helper.rb +41 -0
  80. data/lib/matcher/matchers/parse_iso8601_matcher.rb +52 -0
  81. data/lib/matcher/matchers/parse_json_helper.rb +43 -0
  82. data/lib/matcher/matchers/parse_json_matcher.rb +59 -0
  83. data/lib/matcher/matchers/project_matcher.rb +72 -0
  84. data/lib/matcher/matchers/raises_matcher.rb +131 -0
  85. data/lib/matcher/matchers/range_matcher.rb +50 -0
  86. data/lib/matcher/matchers/reference_matcher.rb +213 -0
  87. data/lib/matcher/matchers/reference_matcher_collection.rb +57 -0
  88. data/lib/matcher/matchers/regexp_matcher.rb +86 -0
  89. data/lib/matcher/messages/expected_phrasing.rb +355 -0
  90. data/lib/matcher/messages/message.rb +104 -0
  91. data/lib/matcher/messages/message_builder.rb +35 -0
  92. data/lib/matcher/messages/message_rules.rb +240 -0
  93. data/lib/matcher/messages/namespaced_message_builder.rb +19 -0
  94. data/lib/matcher/messages/phrasing.rb +59 -0
  95. data/lib/matcher/messages/standard_message_builder.rb +105 -0
  96. data/lib/matcher/patterns/ast_mapping.rb +42 -0
  97. data/lib/matcher/patterns/capture_hole.rb +33 -0
  98. data/lib/matcher/patterns/constant_hole.rb +14 -0
  99. data/lib/matcher/patterns/hole.rb +30 -0
  100. data/lib/matcher/patterns/method_hole.rb +62 -0
  101. data/lib/matcher/patterns/pattern.rb +104 -0
  102. data/lib/matcher/patterns/pattern_building.rb +39 -0
  103. data/lib/matcher/patterns/pattern_capture.rb +11 -0
  104. data/lib/matcher/patterns/pattern_match.rb +29 -0
  105. data/lib/matcher/patterns/variable_hole.rb +14 -0
  106. data/lib/matcher/reporter.rb +103 -0
  107. data/lib/matcher/rules/message_factory.rb +26 -0
  108. data/lib/matcher/rules/message_rule.rb +18 -0
  109. data/lib/matcher/rules/message_rule_context.rb +26 -0
  110. data/lib/matcher/rules/rule_builder.rb +29 -0
  111. data/lib/matcher/rules/rule_set.rb +57 -0
  112. data/lib/matcher/rules/transform_builder.rb +24 -0
  113. data/lib/matcher/rules/transform_mapping.rb +5 -0
  114. data/lib/matcher/rules/transform_rule.rb +21 -0
  115. data/lib/matcher/state.rb +40 -0
  116. data/lib/matcher/testing/error_builder.rb +62 -0
  117. data/lib/matcher/testing/error_checker.rb +514 -0
  118. data/lib/matcher/testing/error_testing.rb +37 -0
  119. data/lib/matcher/testing/pattern_testing.rb +11 -0
  120. data/lib/matcher/testing/pattern_testing_scope.rb +34 -0
  121. data/lib/matcher/testing.rb +107 -0
  122. data/lib/matcher/undefined.rb +10 -0
  123. data/lib/matcher/utils/mapping_utils.rb +61 -0
  124. data/lib/matcher/utils.rb +72 -0
  125. data/lib/matcher/version.rb +5 -0
  126. data/lib/matcher.rb +346 -0
  127. 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