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,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