ruby-rego 0.1.0

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 (124) hide show
  1. checksums.yaml +7 -0
  2. data/.reek.yml +80 -0
  3. data/.vscode/extensions.json +19 -0
  4. data/.vscode/launch.json +35 -0
  5. data/.vscode/settings.json +25 -0
  6. data/.vscode/tasks.json +117 -0
  7. data/.yardopts +12 -0
  8. data/ARCHITECTURE.md +39 -0
  9. data/CHANGELOG.md +25 -0
  10. data/CODE_OF_CONDUCT.md +10 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +183 -0
  13. data/RELEASING.md +37 -0
  14. data/Rakefile +38 -0
  15. data/SECURITY.md +26 -0
  16. data/Steepfile +10 -0
  17. data/TODO.md +35 -0
  18. data/benchmark/builtin_calls.rb +29 -0
  19. data/benchmark/complex_policy.rb +19 -0
  20. data/benchmark/comprehensions.rb +19 -0
  21. data/benchmark/simple_rules.rb +20 -0
  22. data/examples/README.md +27 -0
  23. data/examples/sample_config.yaml +2 -0
  24. data/examples/simple_policy.rego +7 -0
  25. data/examples/validation_policy.rego +11 -0
  26. data/exe/rego-validate +6 -0
  27. data/lib/ruby/rego/ast/base.rb +95 -0
  28. data/lib/ruby/rego/ast/binary_op.rb +64 -0
  29. data/lib/ruby/rego/ast/call.rb +27 -0
  30. data/lib/ruby/rego/ast/composite.rb +48 -0
  31. data/lib/ruby/rego/ast/comprehension.rb +63 -0
  32. data/lib/ruby/rego/ast/every.rb +37 -0
  33. data/lib/ruby/rego/ast/import.rb +32 -0
  34. data/lib/ruby/rego/ast/literal.rb +70 -0
  35. data/lib/ruby/rego/ast/module.rb +32 -0
  36. data/lib/ruby/rego/ast/package.rb +22 -0
  37. data/lib/ruby/rego/ast/query.rb +63 -0
  38. data/lib/ruby/rego/ast/reference.rb +58 -0
  39. data/lib/ruby/rego/ast/rule.rb +114 -0
  40. data/lib/ruby/rego/ast/unary_op.rb +42 -0
  41. data/lib/ruby/rego/ast/variable.rb +22 -0
  42. data/lib/ruby/rego/ast.rb +17 -0
  43. data/lib/ruby/rego/builtins/aggregates.rb +124 -0
  44. data/lib/ruby/rego/builtins/base.rb +95 -0
  45. data/lib/ruby/rego/builtins/collections/array_ops.rb +103 -0
  46. data/lib/ruby/rego/builtins/collections/object_ops.rb +120 -0
  47. data/lib/ruby/rego/builtins/collections/set_ops.rb +51 -0
  48. data/lib/ruby/rego/builtins/collections.rb +137 -0
  49. data/lib/ruby/rego/builtins/comparisons/casts.rb +139 -0
  50. data/lib/ruby/rego/builtins/comparisons.rb +84 -0
  51. data/lib/ruby/rego/builtins/numeric_helpers.rb +56 -0
  52. data/lib/ruby/rego/builtins/registry.rb +199 -0
  53. data/lib/ruby/rego/builtins/registry_helpers.rb +27 -0
  54. data/lib/ruby/rego/builtins/strings/case_ops.rb +22 -0
  55. data/lib/ruby/rego/builtins/strings/concat.rb +19 -0
  56. data/lib/ruby/rego/builtins/strings/formatting.rb +35 -0
  57. data/lib/ruby/rego/builtins/strings/helpers.rb +62 -0
  58. data/lib/ruby/rego/builtins/strings/number_helpers.rb +48 -0
  59. data/lib/ruby/rego/builtins/strings/search.rb +63 -0
  60. data/lib/ruby/rego/builtins/strings/split.rb +19 -0
  61. data/lib/ruby/rego/builtins/strings/substring.rb +22 -0
  62. data/lib/ruby/rego/builtins/strings/trim.rb +42 -0
  63. data/lib/ruby/rego/builtins/strings/trim_helpers.rb +62 -0
  64. data/lib/ruby/rego/builtins/strings.rb +58 -0
  65. data/lib/ruby/rego/builtins/types.rb +89 -0
  66. data/lib/ruby/rego/call_name.rb +55 -0
  67. data/lib/ruby/rego/cli.rb +1122 -0
  68. data/lib/ruby/rego/compiled_module.rb +114 -0
  69. data/lib/ruby/rego/compiler.rb +1097 -0
  70. data/lib/ruby/rego/environment/overrides.rb +33 -0
  71. data/lib/ruby/rego/environment/reference_resolution.rb +86 -0
  72. data/lib/ruby/rego/environment.rb +230 -0
  73. data/lib/ruby/rego/environment_pool.rb +71 -0
  74. data/lib/ruby/rego/error_handling.rb +58 -0
  75. data/lib/ruby/rego/error_payload.rb +34 -0
  76. data/lib/ruby/rego/errors.rb +196 -0
  77. data/lib/ruby/rego/evaluator/assignment_support.rb +126 -0
  78. data/lib/ruby/rego/evaluator/binding_helpers.rb +60 -0
  79. data/lib/ruby/rego/evaluator/comprehension_evaluator.rb +182 -0
  80. data/lib/ruby/rego/evaluator/expression_dispatch.rb +45 -0
  81. data/lib/ruby/rego/evaluator/expression_evaluator.rb +492 -0
  82. data/lib/ruby/rego/evaluator/object_literal_evaluator.rb +52 -0
  83. data/lib/ruby/rego/evaluator/operator_evaluator.rb +163 -0
  84. data/lib/ruby/rego/evaluator/query_node_builder.rb +38 -0
  85. data/lib/ruby/rego/evaluator/reference_key_resolver.rb +50 -0
  86. data/lib/ruby/rego/evaluator/reference_resolver.rb +352 -0
  87. data/lib/ruby/rego/evaluator/rule_evaluator/bindings.rb +70 -0
  88. data/lib/ruby/rego/evaluator/rule_evaluator.rb +550 -0
  89. data/lib/ruby/rego/evaluator/rule_value_provider.rb +56 -0
  90. data/lib/ruby/rego/evaluator/variable_collector.rb +221 -0
  91. data/lib/ruby/rego/evaluator.rb +174 -0
  92. data/lib/ruby/rego/lexer/number_reader.rb +68 -0
  93. data/lib/ruby/rego/lexer/stream.rb +137 -0
  94. data/lib/ruby/rego/lexer/string_reader.rb +90 -0
  95. data/lib/ruby/rego/lexer/template_string_reader.rb +62 -0
  96. data/lib/ruby/rego/lexer.rb +206 -0
  97. data/lib/ruby/rego/location.rb +73 -0
  98. data/lib/ruby/rego/memoization.rb +67 -0
  99. data/lib/ruby/rego/parser/collections.rb +173 -0
  100. data/lib/ruby/rego/parser/expressions.rb +216 -0
  101. data/lib/ruby/rego/parser/precedence.rb +42 -0
  102. data/lib/ruby/rego/parser/query.rb +139 -0
  103. data/lib/ruby/rego/parser/references.rb +115 -0
  104. data/lib/ruby/rego/parser/rules.rb +310 -0
  105. data/lib/ruby/rego/parser.rb +210 -0
  106. data/lib/ruby/rego/policy.rb +50 -0
  107. data/lib/ruby/rego/result.rb +91 -0
  108. data/lib/ruby/rego/token.rb +206 -0
  109. data/lib/ruby/rego/unifier.rb +451 -0
  110. data/lib/ruby/rego/value.rb +379 -0
  111. data/lib/ruby/rego/version.rb +7 -0
  112. data/lib/ruby/rego/with_modifiers/with_modifier.rb +37 -0
  113. data/lib/ruby/rego/with_modifiers/with_modifier_applier.rb +48 -0
  114. data/lib/ruby/rego/with_modifiers/with_modifier_builtin_override.rb +128 -0
  115. data/lib/ruby/rego/with_modifiers/with_modifier_context.rb +120 -0
  116. data/lib/ruby/rego/with_modifiers/with_modifier_path_key_resolver.rb +42 -0
  117. data/lib/ruby/rego/with_modifiers/with_modifier_path_override.rb +99 -0
  118. data/lib/ruby/rego/with_modifiers/with_modifier_root_scope.rb +58 -0
  119. data/lib/ruby/rego.rb +72 -0
  120. data/sig/objspace.rbs +4 -0
  121. data/sig/psych.rbs +7 -0
  122. data/sig/rego_validate.rbs +382 -0
  123. data/sig/ruby/rego.rbs +2150 -0
  124. metadata +172 -0
@@ -0,0 +1,379 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Lint/RedundantRequireStatement
4
+ require "set"
5
+ # rubocop:enable Lint/RedundantRequireStatement
6
+ require_relative "errors"
7
+
8
+ module Ruby
9
+ module Rego
10
+ # Base class for Rego values.
11
+ class Value
12
+ TYPE_NAME = "value"
13
+
14
+ # Create a value wrapper.
15
+ #
16
+ # @param value [Object] underlying value
17
+ def initialize(value = nil)
18
+ @value = value
19
+ end
20
+
21
+ # The wrapped Ruby value.
22
+ #
23
+ # @return [Object]
24
+ attr_reader :value
25
+
26
+ # Determine truthiness for Rego evaluation.
27
+ #
28
+ # @return [Boolean]
29
+ def truthy?
30
+ true
31
+ end
32
+
33
+ # Convert the value back to Ruby.
34
+ #
35
+ # @return [Object]
36
+ def to_ruby
37
+ value
38
+ end
39
+
40
+ # Return the Rego type name.
41
+ #
42
+ # @return [String]
43
+ def type_name
44
+ self.class::TYPE_NAME
45
+ end
46
+
47
+ # Resolve a reference on the value.
48
+ #
49
+ # @param _key [Object] reference key
50
+ # @return [Value] resolved value or undefined
51
+ def fetch_reference(_key)
52
+ return self if undefined?
53
+
54
+ UndefinedValue.new
55
+ end
56
+
57
+ # Check if the value is undefined.
58
+ #
59
+ # @return [Boolean]
60
+ def undefined?
61
+ is_a?(UndefinedValue)
62
+ end
63
+
64
+ # Return a normalized object key representation.
65
+ #
66
+ # @return [Object]
67
+ def object_key
68
+ to_ruby
69
+ end
70
+
71
+ # Compare values by class and underlying Ruby value.
72
+ #
73
+ # @param other [Object]
74
+ # @return [Boolean]
75
+ def ==(other)
76
+ other.is_a?(self.class) && other.to_ruby == to_ruby
77
+ end
78
+
79
+ alias eql? ==
80
+
81
+ # Hash for use in Ruby collections.
82
+ #
83
+ # @return [Integer]
84
+ def hash
85
+ [self.class.name, to_ruby].hash
86
+ end
87
+
88
+ # Coerce Ruby values into Rego values.
89
+ #
90
+ # @param value [Object] value to convert
91
+ # @return [Value] converted value
92
+ def self.from_ruby(value)
93
+ return value if value.is_a?(Value)
94
+ return UndefinedValue.new if value.equal?(UndefinedValue::UNDEFINED)
95
+
96
+ built_value = build_value(value)
97
+ return built_value if built_value
98
+
99
+ raise ArgumentError, "Unsupported value type: #{value.class}"
100
+ end
101
+
102
+ def self.build_value(value)
103
+ build_simple_value(value) || build_composite_value(value)
104
+ end
105
+ private_class_method :build_value
106
+
107
+ def self.build_simple_value(value)
108
+ case value
109
+ when NilClass
110
+ NullValue.new
111
+ when TrueClass, FalseClass
112
+ BooleanValue.new(value)
113
+ when String
114
+ StringValue.new(value)
115
+ when Numeric
116
+ NumberValue.new(value)
117
+ end
118
+ end
119
+ private_class_method :build_simple_value
120
+
121
+ def self.build_composite_value(value)
122
+ case value
123
+ when Array
124
+ ArrayValue.new(value)
125
+ when Hash
126
+ ObjectValue.new(value)
127
+ when Set
128
+ SetValue.new(value)
129
+ end
130
+ end
131
+ private_class_method :build_composite_value
132
+ end
133
+
134
+ # Represents a string value.
135
+ class StringValue < Value
136
+ TYPE_NAME = "string"
137
+
138
+ # Create a string value.
139
+ #
140
+ # @param value [String] string value
141
+ def initialize(value)
142
+ super(String(value))
143
+ end
144
+ end
145
+
146
+ # Represents a numeric value.
147
+ class NumberValue < Value
148
+ TYPE_NAME = "number"
149
+
150
+ # Create a numeric value.
151
+ #
152
+ # @param value [Numeric] numeric value
153
+ def initialize(value)
154
+ raise ArgumentError, "Expected Numeric, got #{value.class}" unless value.is_a?(Numeric)
155
+
156
+ super
157
+ end
158
+ end
159
+
160
+ # Represents a boolean value.
161
+ class BooleanValue < Value
162
+ TYPE_NAME = "boolean"
163
+
164
+ # Create a boolean value.
165
+ #
166
+ # @param value [Boolean] boolean value
167
+ def initialize(value)
168
+ klass = value.class
169
+ raise ArgumentError, "Expected Boolean, got #{klass}" unless [TrueClass, FalseClass].include?(klass)
170
+
171
+ super
172
+ end
173
+
174
+ # Determine truthiness.
175
+ #
176
+ # @return [Boolean]
177
+ def truthy?
178
+ value
179
+ end
180
+ end
181
+
182
+ # Represents a null value.
183
+ class NullValue < Value
184
+ TYPE_NAME = "null"
185
+
186
+ # Create a null value.
187
+ def initialize
188
+ super(nil)
189
+ end
190
+
191
+ # Null is falsy.
192
+ #
193
+ # @return [Boolean]
194
+ def truthy?
195
+ false
196
+ end
197
+ end
198
+
199
+ # Represents an undefined value.
200
+ class UndefinedValue < Value
201
+ TYPE_NAME = "undefined"
202
+ UNDEFINED = Object.new.freeze
203
+
204
+ # Create an undefined value marker.
205
+ def initialize
206
+ super(UNDEFINED)
207
+ end
208
+
209
+ # Undefined is falsy.
210
+ #
211
+ # @return [Boolean]
212
+ def truthy?
213
+ false
214
+ end
215
+
216
+ # Return the singleton undefined marker.
217
+ #
218
+ # @return [Object]
219
+ def to_ruby
220
+ UNDEFINED
221
+ end
222
+
223
+ # Use the instance itself as an object key.
224
+ #
225
+ # @return [UndefinedValue]
226
+ def object_key
227
+ self
228
+ end
229
+ end
230
+
231
+ # Represents an array value.
232
+ class ArrayValue < Value
233
+ TYPE_NAME = "array"
234
+
235
+ # Create an array value.
236
+ #
237
+ # @param elements [Array<Object>] elements to wrap
238
+ def initialize(elements)
239
+ @elements = elements.map { |element| Value.from_ruby(element) }
240
+ super(@elements)
241
+ end
242
+
243
+ # Fetch an element by index.
244
+ #
245
+ # @param index [Integer] array index
246
+ # @return [Value] element or undefined
247
+ def fetch_index(index)
248
+ return UndefinedValue.new unless index.is_a?(Integer)
249
+
250
+ @elements[index] || UndefinedValue.new
251
+ end
252
+
253
+ # Resolve a reference for an array.
254
+ #
255
+ # @param key [Object] index value
256
+ # @return [Value] element or undefined
257
+ def fetch_reference(key)
258
+ fetch_index(key)
259
+ end
260
+
261
+ # Convert the array back to Ruby.
262
+ #
263
+ # @return [Array<Object>]
264
+ def to_ruby
265
+ @elements.map(&:to_ruby)
266
+ end
267
+
268
+ private
269
+
270
+ attr_reader :elements
271
+ end
272
+
273
+ # Represents an object value.
274
+ class ObjectValue < Value
275
+ TYPE_NAME = "object"
276
+
277
+ # Create an object value.
278
+ #
279
+ # @param pairs [Hash<Object, Object>] object pairs
280
+ def initialize(pairs)
281
+ @values = normalize_pairs(pairs)
282
+ super(@values)
283
+ end
284
+
285
+ # :reek:TooManyStatements
286
+ # :reek:FeatureEnvy
287
+ def normalize_pairs(pairs)
288
+ values = {} # @type var values: Hash[Object, Value]
289
+ key_sources = {} # @type var key_sources: Hash[Object, Object]
290
+ pairs.each_with_object(values) do |(key, val), acc|
291
+ normalized_key = key.is_a?(Symbol) ? key.to_s : key
292
+ ensure_unique_key(normalized_key, key_sources, key)
293
+ acc[normalized_key] = Value.from_ruby(val)
294
+ end
295
+ values
296
+ end
297
+
298
+ # :reek:FeatureEnvy
299
+ def ensure_unique_key(normalized_key, key_sources, key)
300
+ return unless key_sources.key?(normalized_key)
301
+
302
+ existing_key = key_sources[normalized_key]
303
+ raise ObjectKeyConflictError, "Conflicting object keys: #{existing_key.inspect} and #{key.inspect}"
304
+ ensure
305
+ key_sources[normalized_key] = key
306
+ end
307
+
308
+ # Fetch a value by key.
309
+ #
310
+ # @param key [Object] object key
311
+ # @return [Value] value or undefined
312
+ def fetch(key)
313
+ return @values[key] if @values.key?(key)
314
+
315
+ return fetch_by_symbol_key(key) if key.is_a?(Symbol)
316
+
317
+ UndefinedValue.new
318
+ end
319
+
320
+ # Resolve a reference for an object.
321
+ #
322
+ # @param key [Object] object key
323
+ # @return [Value] value or undefined
324
+ def fetch_reference(key)
325
+ fetch(key)
326
+ end
327
+
328
+ def fetch_by_symbol_key(key)
329
+ string_key = key.to_s
330
+ @values[string_key] || UndefinedValue.new
331
+ end
332
+
333
+ # Convert the object back to Ruby.
334
+ #
335
+ # @return [Hash<Object, Object>]
336
+ def to_ruby
337
+ @values.transform_values(&:to_ruby)
338
+ end
339
+
340
+ private
341
+
342
+ attr_reader :values
343
+ private :fetch_by_symbol_key, :normalize_pairs, :ensure_unique_key
344
+ end
345
+
346
+ # Represents a set value.
347
+ class SetValue < Value
348
+ TYPE_NAME = "set"
349
+
350
+ # Create a set value.
351
+ #
352
+ # @param elements [Set<Object>, Array<Object>] set elements
353
+ def initialize(elements)
354
+ collection = elements.is_a?(Set) ? elements.to_a : Array(elements)
355
+ @elements = Set.new(collection.map { |element| Value.from_ruby(element) })
356
+ super(@elements)
357
+ end
358
+
359
+ # Check whether the set includes a value.
360
+ #
361
+ # @param value [Object] value to check
362
+ # @return [Boolean]
363
+ def include?(value)
364
+ @elements.include?(Value.from_ruby(value))
365
+ end
366
+
367
+ # Convert the set back to Ruby.
368
+ #
369
+ # @return [Set<Object>]
370
+ def to_ruby
371
+ Set.new(@elements.map(&:to_ruby))
372
+ end
373
+
374
+ private
375
+
376
+ attr_reader :elements
377
+ end
378
+ end
379
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "with_modifier_context"
4
+
5
+ module Ruby
6
+ module Rego
7
+ module WithModifiers
8
+ # Applies `with` modifiers to a temporary environment.
9
+ class WithModifier
10
+ # @param target [Object]
11
+ # @param value [Object]
12
+ def initialize(target:, value:)
13
+ @target = target
14
+ @value = value
15
+ end
16
+
17
+ # @return [Object]
18
+ attr_reader :target
19
+
20
+ # @return [Object]
21
+ attr_reader :value
22
+
23
+ # @param environment [Environment]
24
+ # @param expression_evaluator [Evaluator::ExpressionEvaluator]
25
+ # @yieldparam environment [Environment]
26
+ # @return [Object]
27
+ def with_environment(environment, expression_evaluator, &)
28
+ WithModifierContext.new(
29
+ modifier: self,
30
+ environment: environment,
31
+ expression_evaluator: expression_evaluator
32
+ ).apply(&)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "with_modifier"
4
+
5
+ module Ruby
6
+ module Rego
7
+ module WithModifiers
8
+ # Applies a sequence of with modifiers around a block.
9
+ class WithModifierApplier
10
+ # @param modifiers [Array<AST::WithModifier>]
11
+ # @param environment [Environment]
12
+ # @param expression_evaluator [Evaluator::ExpressionEvaluator]
13
+ # @yieldparam environment [Environment]
14
+ # @return [Object]
15
+ def self.apply(modifiers, environment, expression_evaluator, &block)
16
+ block ||= ->(_env) {}
17
+ return block.call(environment) if modifiers.empty?
18
+
19
+ build_chain(modifiers, expression_evaluator, block).call(environment)
20
+ end
21
+
22
+ def self.build_chain(modifiers, expression_evaluator, block)
23
+ index = modifiers.length - 1
24
+ chain = block
25
+
26
+ while index >= 0
27
+ chain = wrap_modifier(modifiers[index], expression_evaluator, chain)
28
+ index -= 1
29
+ end
30
+
31
+ chain
32
+ end
33
+
34
+ def self.wrap_modifier(modifier, expression_evaluator, block)
35
+ lambda do |env|
36
+ WithModifier.new(target: modifier.target, value: modifier.value).with_environment(
37
+ env,
38
+ expression_evaluator,
39
+ &block
40
+ )
41
+ end
42
+ end
43
+
44
+ private_class_method :build_chain, :wrap_modifier
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../ast"
4
+ require_relative "../builtins/registry"
5
+ require_relative "../errors"
6
+ require_relative "../value"
7
+
8
+ module Ruby
9
+ module Rego
10
+ module WithModifiers
11
+ # Temporarily replaces a builtin with another builtin implementation.
12
+ class WithModifierBuiltinOverride
13
+ # @param name [String]
14
+ # @param value [Object]
15
+ # @param expression_evaluator [Evaluator::ExpressionEvaluator]
16
+ # @param location [Location, nil]
17
+ def initialize(name:, value:, expression_evaluator:, location: nil)
18
+ @name = name.to_s
19
+ @value = value
20
+ @expression_evaluator = expression_evaluator
21
+ @location = location
22
+ end
23
+
24
+ # @yieldparam environment [Environment]
25
+ # @return [Object]
26
+ def apply(environment, &block)
27
+ block ||= ->(_env) {}
28
+ registry = environment.builtin_registry
29
+ registry.entry_for(name)
30
+ override_registry = registry.with_override(name, override_entry(registry, environment))
31
+ environment.with_builtin_registry(override_registry, &block)
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :name, :value, :expression_evaluator, :location
37
+
38
+ def override_entry(registry, environment)
39
+ original = registry.entry_for(name)
40
+ replacement = replacement_entry(registry, environment)
41
+ ensure_matching_arity(original.arity, replacement.arity)
42
+ Builtins::BuiltinRegistry::Entry.new(name: name, arity: replacement.arity, handler: replacement.handler)
43
+ end
44
+
45
+ def replacement_entry(registry, environment)
46
+ replacement = replacement_name
47
+ return registry.entry_for(replacement) if registry.registered?(replacement)
48
+
49
+ function_entry(environment, replacement)
50
+ end
51
+
52
+ # rubocop:disable Metrics/MethodLength
53
+ def function_entry(environment, function_name)
54
+ # @type var empty_rules: Array[AST::Rule]
55
+ empty_rules = []
56
+ rules = environment.rules.fetch(function_name.to_s) { empty_rules }
57
+ function_rule = rules.find(&:function?)
58
+ unless function_rule
59
+ raise EvaluationError.new(
60
+ "With modifier expects a builtin function name",
61
+ rule: nil,
62
+ location: location
63
+ )
64
+ end
65
+
66
+ arity = Array(function_rule.head[:args]).length
67
+ handler = lambda do |*args|
68
+ expression_evaluator.evaluate_user_function(function_name, args)
69
+ end
70
+ Builtins::BuiltinRegistry::Entry.new(name: function_name, arity: arity, handler: handler)
71
+ end
72
+ # rubocop:enable Metrics/MethodLength
73
+
74
+ def ensure_matching_arity(expected, actual)
75
+ expected_list = normalize_arity_list(expected)
76
+ actual_list = normalize_arity_list(actual)
77
+ return if expected_list.sort == actual_list.sort
78
+
79
+ raise EvaluationError.new(
80
+ "With modifier function arity mismatch",
81
+ rule: nil,
82
+ location: location
83
+ )
84
+ end
85
+
86
+ # :reek:UtilityFunction
87
+ def normalize_arity_list(arity)
88
+ arity.is_a?(Array) ? arity : [arity]
89
+ end
90
+
91
+ def replacement_name
92
+ direct_name = direct_name_from_value
93
+ return direct_name if direct_name
94
+
95
+ resolved_name = resolved_name_from_value
96
+ return resolved_name if resolved_name
97
+
98
+ raise EvaluationError.new(
99
+ "With modifier expects a builtin function name",
100
+ rule: nil,
101
+ location: location
102
+ )
103
+ end
104
+
105
+ def direct_name_from_value
106
+ return value.name if value.is_a?(AST::Variable)
107
+ return reference_name_from_value if value.is_a?(AST::Reference)
108
+ return value.value if value.is_a?(AST::StringLiteral)
109
+
110
+ nil
111
+ end
112
+
113
+ def reference_name_from_value
114
+ base = value.base
115
+ return base.name if base.is_a?(AST::Variable) && value.path.empty?
116
+
117
+ nil
118
+ end
119
+
120
+ def resolved_name_from_value
121
+ resolved = expression_evaluator.evaluate(value)
122
+ resolved_value = resolved.is_a?(Value) ? resolved.to_ruby : resolved
123
+ resolved_value if resolved_value.is_a?(String)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end