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,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ErrorCollector
|
|
5
|
+
def self.error_from(obj)
|
|
6
|
+
case obj
|
|
7
|
+
when String, Message
|
|
8
|
+
ElementError.new(obj)
|
|
9
|
+
when Error
|
|
10
|
+
obj
|
|
11
|
+
else
|
|
12
|
+
"Unexpected error object: #{obj.inspect}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :error
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@error = EmptyError.instance
|
|
20
|
+
@mode = :and
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def empty?
|
|
24
|
+
@error.valid?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def or!
|
|
28
|
+
@mode = :or
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def or?
|
|
33
|
+
@mode == :or
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def and?
|
|
37
|
+
@mode == :and
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class Brackets
|
|
41
|
+
def initialize(parent, key)
|
|
42
|
+
@parent = parent
|
|
43
|
+
@key = key
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def error
|
|
47
|
+
@parent.error
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def <<(error)
|
|
51
|
+
error = ErrorCollector.error_from(error)
|
|
52
|
+
|
|
53
|
+
return error if error.is_a?(EmptyError) || @key == Variable.actual
|
|
54
|
+
|
|
55
|
+
key = if @key.is_a?(Expression)
|
|
56
|
+
@key
|
|
57
|
+
else
|
|
58
|
+
Call.new(Variable.actual, :[], [Constant.new(@key)])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@parent << NestedError.new(key, error)
|
|
62
|
+
|
|
63
|
+
@parent.error
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def [](key)
|
|
67
|
+
Brackets.new(self, key)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def <<(error)
|
|
72
|
+
return @error if error.is_a?(EmptyError)
|
|
73
|
+
|
|
74
|
+
error = ErrorCollector.error_from(error)
|
|
75
|
+
|
|
76
|
+
case @error
|
|
77
|
+
when EmptyError
|
|
78
|
+
@error = error
|
|
79
|
+
when and? ? AndError : OrError
|
|
80
|
+
@error << error
|
|
81
|
+
else
|
|
82
|
+
if and?
|
|
83
|
+
@error &= error
|
|
84
|
+
else
|
|
85
|
+
@error |= error
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
@error
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def [](key)
|
|
93
|
+
Brackets.new(self, key)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def clear
|
|
97
|
+
@error = EmptyError.instance
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class NestedError < Error
|
|
5
|
+
def self.from(key, child)
|
|
6
|
+
return child if child.is_a?(EmptyError) || key == Variable.actual
|
|
7
|
+
|
|
8
|
+
unless key.is_a?(Expression)
|
|
9
|
+
key = Call.new(Variable.actual, :[], [Constant.new(key)])
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
NestedError.new(key, child)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.key_to_s(key, path)
|
|
16
|
+
remaining = 20 + path.length
|
|
17
|
+
key.visit do |expr|
|
|
18
|
+
next if !expr.is_a?(Variable) || expr.symbol != :actual
|
|
19
|
+
|
|
20
|
+
remaining -= path.length
|
|
21
|
+
break if remaining < 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if remaining >= 0 && key.variables.include?(:actual)
|
|
25
|
+
Variable.with_substitutions(actual: path) do
|
|
26
|
+
key.to_s
|
|
27
|
+
end
|
|
28
|
+
else
|
|
29
|
+
"#{path} -> #{key}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
attr_reader :key, :child
|
|
34
|
+
|
|
35
|
+
def initialize(key, child)
|
|
36
|
+
super()
|
|
37
|
+
|
|
38
|
+
@key = key
|
|
39
|
+
@child = child
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def ==(other)
|
|
43
|
+
return true if equal?(other)
|
|
44
|
+
|
|
45
|
+
other.instance_of?(NestedError) &&
|
|
46
|
+
@key.eql?(other.key) &&
|
|
47
|
+
@child == other.child
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def &(other)
|
|
51
|
+
return self if other.is_a?(EmptyError)
|
|
52
|
+
|
|
53
|
+
if other.is_a?(NestedError) && @key == other.key
|
|
54
|
+
NestedError.new(@key, @child & other.child)
|
|
55
|
+
elsif other.is_a?(AndError)
|
|
56
|
+
errors = other.children
|
|
57
|
+
index = errors.find_index { _1.is_a?(NestedError) && _1.key == @key }
|
|
58
|
+
|
|
59
|
+
if index
|
|
60
|
+
new_errors = errors.dup
|
|
61
|
+
new_errors[index] = self & errors[index]
|
|
62
|
+
else
|
|
63
|
+
new_errors = [self] + errors
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
AndError.new(new_errors)
|
|
67
|
+
else
|
|
68
|
+
AndError.new([self, other])
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def |(other)
|
|
73
|
+
return self if other.is_a?(EmptyError)
|
|
74
|
+
|
|
75
|
+
if other.is_a?(NestedError) && other.key == @key
|
|
76
|
+
NestedError.new(@key, @child | other.child)
|
|
77
|
+
elsif other.is_a?(OrError)
|
|
78
|
+
errors = other.children
|
|
79
|
+
index = errors.find_index { _1.is_a?(NestedError) && _1.key == @key }
|
|
80
|
+
|
|
81
|
+
if index
|
|
82
|
+
new_errors = errors.dup
|
|
83
|
+
new_errors[index] = self | errors[index]
|
|
84
|
+
else
|
|
85
|
+
new_errors = [self] + errors
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
OrError.new(new_errors)
|
|
89
|
+
else
|
|
90
|
+
OrError.new([self, other])
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def to_s
|
|
95
|
+
"#{@key} -> #{@child}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class OrError < Error
|
|
5
|
+
attr_reader :children
|
|
6
|
+
|
|
7
|
+
def self.from(children)
|
|
8
|
+
length = children.length
|
|
9
|
+
|
|
10
|
+
return EmptyError.instance if length == 0
|
|
11
|
+
|
|
12
|
+
children.reduce do |left, right|
|
|
13
|
+
if left.is_a?(OrError)
|
|
14
|
+
left << right
|
|
15
|
+
else
|
|
16
|
+
left | right
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(children)
|
|
22
|
+
raise "children fewer than 2" if children.length < 2
|
|
23
|
+
|
|
24
|
+
super()
|
|
25
|
+
|
|
26
|
+
@children = children
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ==(other)
|
|
30
|
+
return true if equal?(other)
|
|
31
|
+
|
|
32
|
+
other.instance_of?(OrError) &&
|
|
33
|
+
@children == other.children
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def |(other)
|
|
37
|
+
return self if other.is_a?(EmptyError)
|
|
38
|
+
|
|
39
|
+
clone << other
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def add(other)
|
|
43
|
+
case other
|
|
44
|
+
when OrError
|
|
45
|
+
right = other.children.dup
|
|
46
|
+
|
|
47
|
+
@children.each_with_index do |l, i|
|
|
48
|
+
next unless l.is_a?(NestedError)
|
|
49
|
+
|
|
50
|
+
index = right.find_index { _1.is_a?(NestedError) && _1.key == l.key }
|
|
51
|
+
|
|
52
|
+
@children[i] = l | right.delete_at(index) if index
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@children.concat(right)
|
|
56
|
+
when NestedError
|
|
57
|
+
index = @children.find_index do |child|
|
|
58
|
+
child.is_a?(NestedError) && child.key == other.key
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if index
|
|
62
|
+
@children[index] |= other
|
|
63
|
+
else
|
|
64
|
+
@children << other
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
@children << other
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
alias << add
|
|
73
|
+
|
|
74
|
+
def clone
|
|
75
|
+
klone = super
|
|
76
|
+
klone.instance_exec do
|
|
77
|
+
@children = @children.dup
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
klone
|
|
81
|
+
end
|
|
82
|
+
alias dup clone
|
|
83
|
+
|
|
84
|
+
def to_s
|
|
85
|
+
"(#{@children.join(' | ')})"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ExpressionCache < ExpressionLabeler
|
|
5
|
+
def self.current(build_session = Matcher.build_session)
|
|
6
|
+
build_session[:_expression_cache] ||= new if build_session
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
super
|
|
11
|
+
|
|
12
|
+
@cache = {}.compare_by_identity
|
|
13
|
+
@index = [Variable.actual]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def [](expression)
|
|
17
|
+
@index[label(expression)]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def label(expression, actual_label = ROOT)
|
|
21
|
+
cached_label = @cache[expression]
|
|
22
|
+
|
|
23
|
+
return cached_label if cached_label
|
|
24
|
+
|
|
25
|
+
count = @label_count
|
|
26
|
+
label = super
|
|
27
|
+
|
|
28
|
+
if label > count
|
|
29
|
+
if expression.is_a?(Variable)
|
|
30
|
+
symbol = expression.symbol
|
|
31
|
+
expression = Variable.send(symbol) if Variable.well_known?(symbol)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@cache[expression] = label
|
|
35
|
+
@index[label] = expression
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
label
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def constant_for(value)
|
|
42
|
+
label = label_for(@constant_labels, value)
|
|
43
|
+
@index[label] ||= Constant.new(value)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def variable_for(symbol)
|
|
47
|
+
return Variable.send(symbol) if Variable.well_known?(symbol)
|
|
48
|
+
|
|
49
|
+
less_known_variable_for(symbol)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def less_known_variable_for(symbol)
|
|
53
|
+
label = label_for(@variable_labels, symbol)
|
|
54
|
+
@index[label] ||= Variable.new(symbol)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ExpressionLabeler
|
|
5
|
+
ROOT = 0
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@label_count = 0
|
|
9
|
+
|
|
10
|
+
@constant_labels = {}
|
|
11
|
+
@variable_labels = {}
|
|
12
|
+
@call_labels = {}
|
|
13
|
+
@block_labels = {}
|
|
14
|
+
@proc_labels = {}
|
|
15
|
+
@array_labels = {}
|
|
16
|
+
@set_labels = {}
|
|
17
|
+
@hash_labels = {}
|
|
18
|
+
@range_labels = {}
|
|
19
|
+
@string_labels = {}
|
|
20
|
+
@rescue_labels = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def label(expression, actual_label = ROOT)
|
|
24
|
+
case expression
|
|
25
|
+
when Constant
|
|
26
|
+
label_for(@constant_labels, expression.value)
|
|
27
|
+
when Variable
|
|
28
|
+
if expression.symbol == :actual
|
|
29
|
+
actual_label
|
|
30
|
+
else
|
|
31
|
+
label_for(@variable_labels, expression.symbol)
|
|
32
|
+
end
|
|
33
|
+
when Call
|
|
34
|
+
receiver_l = label(expression.receiver, actual_label)
|
|
35
|
+
args_l = expression.args
|
|
36
|
+
.map { label(_1, actual_label) }
|
|
37
|
+
kwargs_l = expression.kwargs
|
|
38
|
+
.transform_values { label(_1, actual_label) }
|
|
39
|
+
block_l = label_for_block(expression.block)
|
|
40
|
+
|
|
41
|
+
label_for(
|
|
42
|
+
@call_labels,
|
|
43
|
+
[receiver_l, expression.method, args_l, kwargs_l, block_l],
|
|
44
|
+
)
|
|
45
|
+
when ProcExpression
|
|
46
|
+
label_for(@proc_labels, expression.block)
|
|
47
|
+
when ArrayExpression
|
|
48
|
+
items_l = expression.items.map { label(_1, actual_label) }
|
|
49
|
+
|
|
50
|
+
label_for(@array_labels, items_l)
|
|
51
|
+
when SetExpression
|
|
52
|
+
items_l = expression.items.map { label(_1, actual_label) }
|
|
53
|
+
|
|
54
|
+
label_for(@set_labels, items_l)
|
|
55
|
+
when HashExpression
|
|
56
|
+
pairs_l = expression.pairs.flat_map do |k, v|
|
|
57
|
+
[label(k, actual_label), label(v, actual_label)]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
label_for(@hash_labels, pairs_l)
|
|
61
|
+
when RangeExpression
|
|
62
|
+
begin_l = label(expression.begin, actual_label)
|
|
63
|
+
end_l = label(expression.end, actual_label)
|
|
64
|
+
|
|
65
|
+
label_for(@range_labels, [begin_l, end_l, expression.exclude_end?])
|
|
66
|
+
when StringExpression
|
|
67
|
+
parts_l = expression.parts.map { label(_1, actual_label) }
|
|
68
|
+
|
|
69
|
+
label_for(@string_labels, parts_l)
|
|
70
|
+
when RescueLastErrorExpression
|
|
71
|
+
expression_l = label(expression.expression, actual_label)
|
|
72
|
+
|
|
73
|
+
label_for(@rescue_labels, expression_l)
|
|
74
|
+
else
|
|
75
|
+
raise "unexpected expression: #{expression.inspect}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def label_for(index, key)
|
|
82
|
+
index[key] ||= (@label_count += 1)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def label_for_block(block)
|
|
86
|
+
case block
|
|
87
|
+
when Block
|
|
88
|
+
label_for(@block_labels, [block.parameters, label(block.expression)])
|
|
89
|
+
when SymbolProc
|
|
90
|
+
label_for(@block_labels, block.symbol)
|
|
91
|
+
else # nil, Proc
|
|
92
|
+
label_for(@block_labels, block)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ArrayExpression < 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?(ArrayExpression) &&
|
|
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.map { _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
|
+
ArrayExpression.new(substituted_items)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_s
|
|
42
|
+
"[#{@items.join(', ')}]"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
##
|
|
5
|
+
# It's possible to build calls with a block:
|
|
6
|
+
#
|
|
7
|
+
# exp = Matcher::Expression.build do
|
|
8
|
+
# _.map { |x| x * 2 }
|
|
9
|
+
# end
|
|
10
|
+
#
|
|
11
|
+
# exp.evaluate(actual: [1, 2]) # => [2, 4]
|
|
12
|
+
#
|
|
13
|
+
# During build time the block acts like an expression builder
|
|
14
|
+
# (e.g. like `Expression.build`), where its arguments are recorders. So the
|
|
15
|
+
# inside of a block cannot be arbitrary but must follow the same rules as for
|
|
16
|
+
# building other expressions.
|
|
17
|
+
#
|
|
18
|
+
# # WRONG
|
|
19
|
+
# Matcher::Expression.build do
|
|
20
|
+
# _.map { |x| 2 * x } # cannot multiply 2 with a recorder
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# == Support for symbol procs
|
|
24
|
+
#
|
|
25
|
+
# exp = Matcher::Expression.build do
|
|
26
|
+
# _.map(&:to_i)
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# exp.evaluate(actual: ['1', '2']) # => [1, 2]
|
|
30
|
+
#
|
|
31
|
+
# @see ExpressionDsl#pass_through_blocks
|
|
32
|
+
class Block
|
|
33
|
+
extend Compatibility
|
|
34
|
+
|
|
35
|
+
class Context
|
|
36
|
+
attr_reader :expression, :values
|
|
37
|
+
|
|
38
|
+
def initialize(expression, values)
|
|
39
|
+
@expression = expression
|
|
40
|
+
@values = values
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def evaluate(values)
|
|
44
|
+
values.merge!(@values) { |_k, _l, r| r }
|
|
45
|
+
|
|
46
|
+
@expression.evaluate(values)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.build(expression_cache: nil, &block)
|
|
51
|
+
return SymbolProc.new(block) if
|
|
52
|
+
block.parameters == [[:req], [:rest]] &&
|
|
53
|
+
/\(&:(\w+[!?]?|".*")\)/.match?(block.to_s)
|
|
54
|
+
|
|
55
|
+
parameters = block.parameters
|
|
56
|
+
args = []
|
|
57
|
+
kwargs = {}
|
|
58
|
+
parameter_names = Set.new
|
|
59
|
+
variable_object_ids = Set.new
|
|
60
|
+
|
|
61
|
+
parameters.each do |type, name|
|
|
62
|
+
parameter_names << name
|
|
63
|
+
variable = Variable.cache(name, expression_cache:)
|
|
64
|
+
variable_object_ids << variable.object_id
|
|
65
|
+
|
|
66
|
+
case type
|
|
67
|
+
when :req, :opt
|
|
68
|
+
args << variable.to_recorder
|
|
69
|
+
when :key, :keyreq
|
|
70
|
+
kwargs[name] = variable.to_recorder
|
|
71
|
+
when :rest
|
|
72
|
+
raise "*#{name unless name == :*} not allowed"
|
|
73
|
+
when :keyrest
|
|
74
|
+
raise "**#{name unless name == :**} not allowed"
|
|
75
|
+
when :block
|
|
76
|
+
raise "&#{name unless name == :&} not allowed"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
result = block.call(*args, **kwargs)
|
|
81
|
+
expression = Expression.of(result, expression_cache:)
|
|
82
|
+
|
|
83
|
+
return SymbolProc.new(expression.method) if
|
|
84
|
+
args.length == 1 &&
|
|
85
|
+
expression.is_a?(Call) &&
|
|
86
|
+
expression.unary? &&
|
|
87
|
+
expression.receiver == Recorder.to_expression(args[0])
|
|
88
|
+
|
|
89
|
+
ExpressionWalker.each_variable(expression) do |variable|
|
|
90
|
+
shadowed = !parameter_names.include?(variable.symbol) ||
|
|
91
|
+
variable_object_ids.include?(variable.object_id)
|
|
92
|
+
|
|
93
|
+
next if shadowed
|
|
94
|
+
|
|
95
|
+
quoted_method = quote_method(variable.symbol)
|
|
96
|
+
|
|
97
|
+
raise "parameter #{quoted_method} shadows an outer variable"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
new(parameters, expression)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
attr_reader :parameters, :expression
|
|
104
|
+
|
|
105
|
+
def initialize(parameters, expression)
|
|
106
|
+
@parameters = parameters
|
|
107
|
+
@expression = expression
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def ==(other)
|
|
111
|
+
return true if equal?(other)
|
|
112
|
+
|
|
113
|
+
other.instance_of?(Block) &&
|
|
114
|
+
@parameters.eql?(other.parameters) &&
|
|
115
|
+
@expression == other.expression
|
|
116
|
+
end
|
|
117
|
+
alias eql? ==
|
|
118
|
+
|
|
119
|
+
def hash
|
|
120
|
+
[self.class, @parameters, @expression].hash
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def variables
|
|
124
|
+
@variables ||= @expression.variables - @parameters.map { _2 }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def substitute(replacements)
|
|
128
|
+
replacements = replacements.slice(*variables)
|
|
129
|
+
|
|
130
|
+
return self if replacements.empty?
|
|
131
|
+
|
|
132
|
+
expression = @expression.substitute(replacements)
|
|
133
|
+
|
|
134
|
+
Block.new(@parameters, expression)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def to_proc(values: nil)
|
|
138
|
+
@proc ||= begin
|
|
139
|
+
kwlist = @parameters.map { |_type, name| "#{name}:" }.join(", ")
|
|
140
|
+
|
|
141
|
+
instance_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
|
142
|
+
->(#{arg_list}) { evaluate({ #{kwlist} }) } # ->(arg, kwarg:) { evaluate({ arg:, kwarg: }) }
|
|
143
|
+
RUBY
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
if values && !variables.empty?
|
|
147
|
+
values = values.slice(*variables)
|
|
148
|
+
|
|
149
|
+
return @proc if values.empty?
|
|
150
|
+
|
|
151
|
+
context = Context.new(@expression, values)
|
|
152
|
+
|
|
153
|
+
lambda do |*args, **kwargs|
|
|
154
|
+
context.instance_exec(*args, **kwargs, &@proc)
|
|
155
|
+
end
|
|
156
|
+
else
|
|
157
|
+
@proc
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def to_s(as_block: false)
|
|
162
|
+
if @parameters.empty?
|
|
163
|
+
as_block ? "{ #{@expression} }" : "-> { #{@expression} }"
|
|
164
|
+
elsif as_block
|
|
165
|
+
"{ |#{arg_list}| #{@expression} }"
|
|
166
|
+
else
|
|
167
|
+
"->(#{arg_list}) { #{@expression} }"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
alias inspect to_s
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
def arg_list
|
|
175
|
+
@parameters.map do |type, name|
|
|
176
|
+
case type
|
|
177
|
+
when :req, :opt
|
|
178
|
+
name
|
|
179
|
+
when :key, :keyreq
|
|
180
|
+
"#{name}:"
|
|
181
|
+
end
|
|
182
|
+
end.join(", ")
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def evaluate(values)
|
|
186
|
+
@expression.evaluate(**values)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|