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,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ ExpressionMatcher.message_rules.configure do
5
+ # binary standard expression
6
+ standard_ops = %i[== != < > <= >= <=> =~ !~] +
7
+ %i[equal? is_a? kind_of? instance_of? respond_to? key? include? in?]
8
+
9
+ message method_hole(:call, _, standard_ops, const(:operand)) do |v, e|
10
+ case e[:call].method
11
+ when :<
12
+ standard_message.less_than(v[:operand])
13
+ when :>
14
+ standard_message.greater_than(v[:operand])
15
+ when :<=
16
+ standard_message.less_than_or_equal(v[:operand])
17
+ when :>=
18
+ standard_message.greater_than_or_equal(v[:operand])
19
+ when :<=>
20
+ # <=> returns nil if operands are not comparable
21
+ standard_message.comparable_to(v[:operand])
22
+ when :==
23
+ standard_message.equal(v[:operand])
24
+ when :!=
25
+ standard_message.not.equal(v[:operand])
26
+ when :=~
27
+ standard_message.matching(v[:operand])
28
+ when :!~
29
+ standard_message.not.matching(v[:operand])
30
+ when :equal?
31
+ standard_message.same(v[:operand])
32
+ when :is_a?, :kind_of?
33
+ standard_message.kind_of(v[:operand])
34
+ when :instance_of?
35
+ standard_message.instance_of(v[:operand])
36
+ when :respond_to?
37
+ standard_message.responding_to(v[:operand])
38
+ when :key?
39
+ standard_message.having_key(v[:operand])
40
+ when :include?
41
+ standard_message.including(v[:operand])
42
+ when :in?
43
+ standard_message.in(v[:operand])
44
+ else
45
+ raise "Unexpected method: #{e[:call].method}"
46
+ end
47
+ end
48
+
49
+ # predicate
50
+ message method_hole(:predicate, _, -> { _1.end_with?("?") }) do |_v, e|
51
+ standard_message.predicate(e[:predicate].method)
52
+ end
53
+
54
+ # flip
55
+ transform(
56
+ method_hole(
57
+ :call,
58
+ hole(:operand) { !_1.variables.include?(:actual) },
59
+ %i[== != < > <= >= equal? include? in?],
60
+ hole(:actual) { _1.variables.include?(:actual) },
61
+ ),
62
+ ) do |m|
63
+ method = m[:call].expression.method
64
+
65
+ flipped_method =
66
+ case method
67
+ when :< then :>
68
+ when :> then :<
69
+ when :<= then :>=
70
+ when :>= then :<=
71
+ when :include? then :in?
72
+ when :in? then :include?
73
+ else
74
+ method
75
+ end
76
+
77
+ call(m[:call], m[:actual], flipped_method, m[:operand])
78
+ end
79
+
80
+ # length
81
+ message capture(:act, _.length) == const(:exp) do |v|
82
+ standard_message.length_of(v[:exp], v[:act])
83
+ end
84
+
85
+ # between
86
+ message _.between?(const(:min), const(:max)) do |v|
87
+ standard_message.between(v[:min], v[:max])
88
+ end
89
+
90
+ # truthy
91
+ message _ do
92
+ standard_message.truthy
93
+ end
94
+
95
+ # transform !
96
+ transform !hole(:expression), negate: true do |m|
97
+ m[:expression]
98
+ end
99
+
100
+ # transform instance_of?
101
+ transform(
102
+ # rubocop:disable Style/ClassEqualityComparison
103
+ hole(:obj).class == hole(:class),
104
+ hole(:class) == hole(:obj).class,
105
+ # rubocop:enable Style/ClassEqualityComparison
106
+ ) do |m|
107
+ call(m[:root], m[:obj], :instance_of?, m[:class])
108
+ end
109
+
110
+ # transform between?
111
+ transform(
112
+ lo { (hole(:value) >= hole(:min)) & (hole(:value) <= hole(:max)) },
113
+ ) do |m|
114
+ call(m[:root], m[:value], :between?, m[:min], m[:max])
115
+ end
116
+
117
+ # transform comparison
118
+ transform(
119
+ method_hole(:compare, hole(:lhs) <=> hole(:rhs), %i[== != < > <= >=], 0),
120
+ ) do |m|
121
+ call(m[:compare], m[:lhs], m[:compare].expression.method, m[:rhs])
122
+ end
123
+
124
+ # length expression
125
+ message capture(:act, hole(:object).length) == hole(:exp) do |v, e|
126
+ expression_message.length_of(
127
+ e[:object], v[:object], v[:exp], v[:act], given
128
+ )
129
+ end
130
+
131
+ # match regexp at
132
+ message(
133
+ method_hole(
134
+ :comparison,
135
+ capture(:pos, hole(:lhs) =~ hole(:rhs)),
136
+ %i[== != < > <= >=],
137
+ hole(:operand),
138
+ ),
139
+ ) do |v, e|
140
+ expression, value, pattern =
141
+ MessageRules.decompose_pattern_matching(
142
+ e[:lhs], e[:rhs], v[:lhs], v[:rhs]
143
+ )
144
+
145
+ if expression
146
+ expression_message.match_at(
147
+ expression,
148
+ value,
149
+ pattern,
150
+ v[:pos],
151
+ e[:comparison].method,
152
+ v[:operand],
153
+ given,
154
+ )
155
+ else
156
+ expression_message.truthy(e[:comparison], v[:comparison], given)
157
+ end
158
+ end
159
+
160
+ # comparison expression
161
+ message method_hole(
162
+ :comparison, hole(:lhs), %i[== != < > <= >=], hole(:rhs)
163
+ ) do |v, e|
164
+ expression_message.comparison(e[:comparison], v[:lhs], v[:rhs], given)
165
+ end
166
+
167
+ # general binary expression
168
+ general_ops =
169
+ %i[<=> equal? is_a? kind_of? instance_of? respond_to? key? include? in?]
170
+
171
+ message method_hole(:call, hole(:lhs), general_ops, hole(:rhs)) do |v, e|
172
+ case e[:call].method
173
+ when :<=>
174
+ # <=> returns nil if operands are not comparable
175
+ expression_message.comparable_to(e[:lhs], v[:lhs], v[:rhs], given)
176
+ when :equal?
177
+ expression_message.same(e[:lhs], e[:rhs], v[:lhs], v[:rhs], given)
178
+ when :is_a?, :kind_of?
179
+ expression_message.kind_of(e[:lhs], v[:lhs], v[:rhs], given)
180
+ when :instance_of?
181
+ expression_message.instance_of(e[:lhs], v[:lhs], v[:rhs], given)
182
+ when :respond_to?
183
+ expression_message.responding_to(e[:lhs], v[:lhs], v[:rhs], given)
184
+ when :key?
185
+ expression_message.having_key(e[:lhs], v[:lhs], v[:rhs], given)
186
+ when :include?
187
+ expression_message.including(e[:lhs], v[:lhs], v[:rhs], given)
188
+ when :in?
189
+ expression_message.in(e[:lhs], v[:lhs], v[:rhs], given)
190
+ else
191
+ raise "Unexpected method: #{e[:call].method}"
192
+ end
193
+ end
194
+
195
+ # predicate expression
196
+ message method_hole(
197
+ :predicate, hole(:receiver), -> { _1.end_with?("?") }
198
+ ) do |v, e|
199
+ expression_message.predicate(
200
+ e[:receiver], v[:receiver], e[:predicate].method, given
201
+ )
202
+ end
203
+
204
+ # match regexp
205
+ message method_hole(:operator, hole(:lhs), %i[=~ !~], hole(:rhs)) do |v, e|
206
+ expression, value, pattern = MessageRules.decompose_pattern_matching(
207
+ e[:lhs], e[:rhs], v[:lhs], v[:rhs]
208
+ )
209
+
210
+ if expression
211
+ expression_message.not_if(e[:operator].method == :!~)
212
+ .matching(expression, value, pattern, given)
213
+ else
214
+ expression_message.truthy(e[:operator], v[:operator], given)
215
+ end
216
+ end
217
+
218
+ # between expression
219
+ message hole(:value).between?(hole(:min), hole(:max)) do |v, e|
220
+ expression_message.between(e[:value], v[:value], v[:min], v[:max], given)
221
+ end
222
+
223
+ # any expression
224
+ message hole(:expression) do |v, e|
225
+ expression_message.truthy(e[:expression], v[:expression], given)
226
+ end
227
+ end
228
+
229
+ module MessageRules
230
+ def self.decompose_pattern_matching(e_lhs, e_rhs, v_lhs, v_rhs)
231
+ if v_lhs.is_a?(String) && v_rhs.is_a?(Regexp)
232
+ [e_lhs, v_lhs, v_rhs]
233
+ elsif v_lhs.is_a?(Regexp) && v_rhs.is_a?(String)
234
+ [e_rhs, v_rhs, v_lhs]
235
+ else
236
+ nil
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class NamespacedMessageBuilder < MessageBuilder
5
+ def initialize(negated, actual, namespace)
6
+ super(negated, actual)
7
+
8
+ @namespace = namespace
9
+ end
10
+
11
+ protected
12
+
13
+ def message(key, *, **)
14
+ key = [@namespace, key]
15
+
16
+ super
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class Phrasing
5
+ extend Forwardable
6
+
7
+ attr_reader :path, :message
8
+
9
+ def_delegator :@message, :actual
10
+ def_delegator :@message, :negated
11
+
12
+ def self.namespace(value)
13
+ @namespace = value
14
+ yield
15
+ ensure
16
+ @namespace = nil
17
+ end
18
+
19
+ def self.define(key, &block)
20
+ key = [@namespace, key] if @namespace
21
+
22
+ dict[key] = block
23
+ end
24
+
25
+ def self.dict
26
+ @dict ||= {}
27
+ end
28
+
29
+ def self.phrasing
30
+ ->(path, message) { new(path, message).apply }
31
+ end
32
+
33
+ def initialize(path, message)
34
+ @path = path
35
+ @message = message
36
+ end
37
+
38
+ def apply
39
+ phrase(@message.key, *@message.args, **@message.kwargs)
40
+ end
41
+
42
+ def phrase(key, *, **)
43
+ block = self.class.dict[key]
44
+
45
+ if block
46
+ instance_exec(*, **, &block)
47
+ else
48
+ "got #{actual.inspect} but found no message for " \
49
+ "#{'*NOT* ' if negated}#{key.inspect}"
50
+ end
51
+ end
52
+
53
+ def phrase_negated(key, *, **)
54
+ message = Message.new(key, !negated, actual, *, **)
55
+
56
+ self.class.new(@path, message).apply
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class StandardMessageBuilder < MessageBuilder
5
+ def namespace(namespace)
6
+ NamespacedMessageBuilder.new(@negated, @actual, namespace)
7
+ end
8
+
9
+ def truthy
10
+ message(:truthy)
11
+ end
12
+
13
+ def same(object)
14
+ message(:same, object)
15
+ end
16
+
17
+ def equal(value)
18
+ message(:equal, value)
19
+ end
20
+
21
+ def less_than(operand)
22
+ message(:less_than, operand)
23
+ end
24
+
25
+ def greater_than(operand)
26
+ message(:greater_than, operand)
27
+ end
28
+
29
+ def less_than_or_equal(operand)
30
+ message(:less_than_or_equal, operand)
31
+ end
32
+
33
+ def greater_than_or_equal(operand)
34
+ message(:greater_than_or_equal, operand)
35
+ end
36
+
37
+ def comparable_to(operand)
38
+ message(:comparable_to, operand)
39
+ end
40
+
41
+ def between(min, max, exclude_end: false)
42
+ message(:between, min, max, exclude_end:)
43
+ end
44
+
45
+ def length_of(exp, act)
46
+ message(:length_of, exp, act)
47
+ end
48
+
49
+ def having_key(key)
50
+ message(:having_key, key)
51
+ end
52
+
53
+ def having_index(index)
54
+ message(:having_index, index)
55
+ end
56
+
57
+ def exist
58
+ message(:exist)
59
+ end
60
+
61
+ def in(collection)
62
+ message(:in, collection)
63
+ end
64
+
65
+ def including(item)
66
+ message(:including, item)
67
+ end
68
+
69
+ def duplicate(original_index)
70
+ message(:duplicate, original_index)
71
+ end
72
+
73
+ def duplicate_by(expression, value, original_index)
74
+ message(:duplicate_by, expression, value, original_index)
75
+ end
76
+
77
+ def matching(pattern)
78
+ message(:matching, pattern)
79
+ end
80
+
81
+ def valid_format(format)
82
+ message(:valid_format, format)
83
+ end
84
+
85
+ def instance_of(klass)
86
+ message(:instance_of, klass)
87
+ end
88
+
89
+ def kind_of(klass)
90
+ message(:kind_of, klass)
91
+ end
92
+
93
+ def responding_to(method)
94
+ message(:responding_to, method)
95
+ end
96
+
97
+ def predicate(name)
98
+ message(:predicate, name)
99
+ end
100
+
101
+ def described_by(description)
102
+ message(:described_by, description)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class AstMapping
5
+ RECEIVER = 0
6
+ ARGS = 1
7
+ KWARGS = 2
8
+
9
+ def initialize(path = List.empty)
10
+ @path = path
11
+ end
12
+
13
+ attr_reader :path
14
+
15
+ def receiver
16
+ @receiver ||= AstMapping.new(@path << RECEIVER)
17
+ end
18
+
19
+ def args
20
+ @args ||= Args.new(@path << ARGS, [])
21
+ end
22
+
23
+ def kwargs
24
+ @kwargs ||= Args.new(@path << KWARGS, {})
25
+ end
26
+
27
+ class Args
28
+ extend Forwardable
29
+
30
+ def initialize(path, data)
31
+ @path = path
32
+ @data = data
33
+ end
34
+
35
+ attr_reader :path, :data
36
+
37
+ def [](index)
38
+ @data[index] ||= AstMapping.new(@path << index)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class CaptureHole < Hole
5
+ def initialize(key, pattern)
6
+ super(key)
7
+
8
+ @pattern = pattern
9
+ end
10
+
11
+ attr_reader :pattern
12
+
13
+ def match?(_expression)
14
+ yield @pattern
15
+
16
+ true
17
+ end
18
+
19
+ def ==(other)
20
+ super && @pattern == other.pattern
21
+ end
22
+ alias eql? ==
23
+
24
+ def hash
25
+ @hash ||= [self.class, @key, @pattern].hash
26
+ end
27
+
28
+ def to_s
29
+ "capture(#{@key.inspect}, #{@pattern})"
30
+ end
31
+ alias inspect to_s
32
+ end
33
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class ConstantHole < Hole
5
+ def match?(expression)
6
+ expression.is_a?(Constant)
7
+ end
8
+
9
+ def to_s
10
+ "const(#{@key.inspect})"
11
+ end
12
+ alias inspect to_s
13
+ end
14
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class Hole
5
+ def initialize(key, filter = nil)
6
+ @key = key
7
+ @filter = filter
8
+ end
9
+
10
+ attr_reader :key
11
+
12
+ def match?(expression)
13
+ !@filter || @filter.call(expression)
14
+ end
15
+
16
+ def ==(other)
17
+ other.instance_of?(self.class) && key == other.key
18
+ end
19
+ alias eql? ==
20
+
21
+ def hash
22
+ [self.class, @key].hash
23
+ end
24
+
25
+ def to_s
26
+ "hole(#{@key.inspect})"
27
+ end
28
+ alias inspect to_s
29
+ end
30
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class MethodHole < Hole
5
+ def initialize(key, receiver, method, args, kwargs)
6
+ super(key)
7
+
8
+ @receiver = receiver
9
+ @method = method
10
+ @args = args
11
+ @kwargs = kwargs
12
+ end
13
+
14
+ attr_reader :receiver, :method, :args, :kwargs
15
+
16
+ def match?(expression)
17
+ if !expression.is_a?(Call) || !match_method?(expression.method)
18
+ return false
19
+ end
20
+
21
+ yield Call.new(@receiver, expression.method, @args, @kwargs)
22
+
23
+ true
24
+ end
25
+
26
+ def ==(other)
27
+ return true if other.equal?(self)
28
+
29
+ super &&
30
+ @receiver == other.receiver &&
31
+ @method == other.method &&
32
+ @args.eql?(other.receiver) &&
33
+ @kwargs.eql?(other.receiver)
34
+ end
35
+ alias eql? ==
36
+
37
+ def hash
38
+ @hash ||= [self.class, @key, @receiver, @method, @args, @kwargs].hash
39
+ end
40
+
41
+ def to_s
42
+ args = @args.map(&:inspect)
43
+ kwargs = @kwargs.map { "#{_1}: #{_2.inspect}" }
44
+ list = [@key.inspect, @receiver, @method.inspect].concat(args, kwargs)
45
+
46
+ "method_hole(#{list.join(', ')})"
47
+ end
48
+ alias inspect to_s
49
+
50
+ private
51
+
52
+ def match_method?(method)
53
+ if @method.is_a?(Array)
54
+ @method.include?(method)
55
+ else
56
+ # rubocop:disable Style/CaseEquality
57
+ @method === method
58
+ # rubocop:enable Style/CaseEquality
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class Pattern
5
+ class PatternBuilder
6
+ include PatternBuilding
7
+
8
+ def initialize(build_session: Matcher.build_session)
9
+ ExpressionDsl.init(self, build_session)
10
+ end
11
+ end
12
+
13
+ def self.build(&)
14
+ Matcher.with_build_session do |build_session|
15
+ builder = PatternBuilder.new(build_session:)
16
+ result = builder.instance_exec(&)
17
+ builder.pattern_of(result)
18
+ end
19
+ end
20
+
21
+ extend Forwardable
22
+
23
+ def initialize(expression)
24
+ @expression = expression
25
+ end
26
+
27
+ attr_reader :expression
28
+
29
+ def_delegator :@expression, :to_s
30
+ def_delegator :@expression, :inspect
31
+
32
+ def match(expression, mapping = AstMapping.new)
33
+ result = PatternMatch.new
34
+
35
+ catch(:mismatch) do
36
+ match_helper(expression, @expression, mapping, result)
37
+
38
+ result.capture(:root, expression, mapping) unless result.include?(:root)
39
+
40
+ return result
41
+ end
42
+
43
+ nil
44
+ end
45
+
46
+ private
47
+
48
+ def match_helper(expression, pattern, mapping, result)
49
+ if (hole = get_hole(pattern))
50
+ key = hole.key
51
+ capture = result[key]
52
+
53
+ if capture
54
+ throw(:mismatch) if capture.expression != expression
55
+ elsif !match?(hole, expression, mapping, result)
56
+ throw(:mismatch)
57
+ else
58
+ result.capture(key, expression, mapping)
59
+ end
60
+ elsif expression.is_a?(Call)
61
+ throw(:mismatch) unless similar_call?(expression, pattern)
62
+
63
+ match_helper(
64
+ expression.receiver, pattern.receiver, mapping.receiver, result
65
+ )
66
+
67
+ expression.args.each_index do |i|
68
+ match_helper(
69
+ expression.args[i], pattern.args[i], mapping.args[i], result
70
+ )
71
+ end
72
+
73
+ expression.kwargs.each_key do |k|
74
+ match_helper(
75
+ expression.kwargs[k], pattern.kwargs[k], mapping.kwargs[k], result
76
+ )
77
+ end
78
+ elsif expression != pattern
79
+ throw(:mismatch)
80
+ end
81
+ end
82
+
83
+ def match?(pattern, expression, mapping, result)
84
+ pattern.match?(expression) do |p|
85
+ match_helper(expression, p, mapping, result)
86
+ end
87
+ end
88
+
89
+ def get_hole(expression)
90
+ return unless expression.is_a?(Constant)
91
+
92
+ constant = expression.value
93
+ constant if constant.is_a?(Hole)
94
+ end
95
+
96
+ def similar_call?(expression, pattern)
97
+ expression.instance_of?(pattern.class) &&
98
+ expression.method == pattern.method &&
99
+ expression.args.length == pattern.args.length &&
100
+ expression.kwargs.size == pattern.kwargs.size &&
101
+ expression.kwargs.keys.sort == pattern.kwargs.keys.sort
102
+ end
103
+ end
104
+ end