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,550 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ class Evaluator
6
+ # Evaluates rule bodies and heads.
7
+ # rubocop:disable Metrics/ClassLength
8
+ # :reek:TooManyMethods
9
+ # :reek:DataClump
10
+ class RuleEvaluator
11
+ # Bundles query evaluation state to minimize parameter passing.
12
+ QueryContext = Struct.new(:literals, :env, keyword_init: true)
13
+ # Bundles value evaluation parameters for else/default handling.
14
+ ValueEvaluationContext = Struct.new(:body, :rule, :value_node, :initial_bindings, keyword_init: true)
15
+ # Bundles modifier evaluation state.
16
+ class ModifierContext
17
+ # @param expression [Object]
18
+ # @param env [Environment]
19
+ # @param bound_vars [Array<String>]
20
+ def initialize(expression:, env:, bound_vars:)
21
+ @expression = expression
22
+ @env = env
23
+ @bound_vars = bound_vars
24
+ end
25
+
26
+ # @return [Object]
27
+ attr_reader :expression
28
+
29
+ # @return [Environment]
30
+ attr_reader :env
31
+
32
+ # @return [Array<String>]
33
+ attr_reader :bound_vars
34
+
35
+ # @param new_env [Environment]
36
+ # @return [ModifierContext]
37
+ def with_env(new_env)
38
+ self.class.new(expression: expression, env: new_env, bound_vars: bound_vars)
39
+ end
40
+ end
41
+
42
+ # @param environment [Environment]
43
+ # @param expression_evaluator [ExpressionEvaluator]
44
+ def initialize(environment:, expression_evaluator:)
45
+ @environment = environment
46
+ @expression_evaluator = expression_evaluator
47
+ @unifier = Unifier.new
48
+ end
49
+
50
+ # @param rules [Array<AST::Rule>]
51
+ # @return [Value]
52
+ def evaluate_group(rules)
53
+ return UndefinedValue.new if rules.empty?
54
+
55
+ first_rule = rules.first
56
+ return UndefinedValue.new if first_rule.function?
57
+ return evaluate_partial_set_rules(rules) if first_rule.partial_set?
58
+ return evaluate_partial_object_rules(rules) if first_rule.partial_object?
59
+
60
+ evaluate_complete_rules(rules)
61
+ end
62
+
63
+ # @param name [String]
64
+ # @param args [Array<Value>]
65
+ # @return [Value]
66
+ def evaluate_function_call(name, args)
67
+ cache = memoization&.context&.function_values
68
+ if cache
69
+ key = [name.to_s, args]
70
+ return cache[key] if cache.key?(key)
71
+
72
+ cache[key] = evaluate_function_call_uncached(name, args)
73
+ return cache[key]
74
+ end
75
+
76
+ evaluate_function_call_uncached(name, args)
77
+ end
78
+
79
+ def evaluate_function_call_uncached(name, args)
80
+ rules = environment.rules.fetch(name.to_s) { [] }
81
+ function_rules = rules.select(&:function?)
82
+ return UndefinedValue.new if function_rules.empty?
83
+
84
+ value = evaluate_function_rules(function_rules, args)
85
+ return value unless value.is_a?(Array)
86
+
87
+ resolved = resolve_conflicts(value, name)
88
+ resolved || UndefinedValue.new
89
+ end
90
+
91
+ # @param rule [AST::Rule]
92
+ # @return [Value, Array]
93
+ # :reek:FeatureEnvy
94
+ def evaluate_rule(rule)
95
+ values = rule_body_values(rule)
96
+ resolved = resolve_conflicts(values, rule.name)
97
+ resolved || UndefinedValue.new
98
+ end
99
+
100
+ # @param literals [Array<Object>]
101
+ # @param env [Environment]
102
+ # @return [Enumerator]
103
+ # @api private
104
+ def query_solutions(literals, env = environment)
105
+ eval_query(literals, env)
106
+ end
107
+
108
+ private
109
+
110
+ attr_reader :environment, :expression_evaluator, :unifier
111
+
112
+ def memoization
113
+ environment.memoization
114
+ end
115
+
116
+ def evaluate_partial_set_rules(rules)
117
+ values = rules.flat_map { |rule| rule_body_values(rule) }
118
+ return UndefinedValue.new if values.empty?
119
+
120
+ SetValue.new(values)
121
+ end
122
+
123
+ def evaluate_partial_object_rules(rules)
124
+ hash = partial_object_pairs(rules)
125
+ hash.empty? ? UndefinedValue.new : ObjectValue.new(hash)
126
+ end
127
+
128
+ def partial_object_pairs(rules)
129
+ pairs = rules.flat_map { |rule| rule_body_pairs(rule) }
130
+ # @type var values: Hash[Object, Value]
131
+ values = {}
132
+ # @type var nested_flags: Hash[Object, bool]
133
+ nested_flags = {}
134
+ pairs.each do |key, value, nested|
135
+ existing = values[key]
136
+ existing_nested = nested_flags[key] || false
137
+ values[key] = merge_partial_object_value(existing, value, key, existing_nested, nested)
138
+ nested_flags[key] = existing_nested || nested
139
+ end
140
+ values
141
+ end
142
+
143
+ # :reek:LongParameterList
144
+ def merge_partial_object_value(existing, value, key, existing_nested, current_nested)
145
+ return value unless existing
146
+ return existing if existing == value
147
+
148
+ if existing.is_a?(ObjectValue) && value.is_a?(ObjectValue) && existing_nested && current_nested
149
+ merged = merge_object_value_hash(existing.value, value.value, key)
150
+ return ObjectValue.new(merged)
151
+ end
152
+
153
+ raise EvaluationError.new("Conflicting object key #{key.inspect}", rule: nil, location: nil)
154
+ end
155
+
156
+ def merge_object_value_hash(left, right, key)
157
+ merged = left.dup
158
+ right.each do |child_key, child_value|
159
+ merged[child_key] = merge_object_value_value(merged[child_key], child_value, key)
160
+ end
161
+ merged
162
+ end
163
+
164
+ def merge_object_value_value(existing, value, key)
165
+ return value unless existing
166
+ return existing if existing == value
167
+
168
+ if existing.is_a?(ObjectValue) && value.is_a?(ObjectValue)
169
+ merged = merge_object_value_hash(existing.value, value.value, key)
170
+ return ObjectValue.new(merged)
171
+ end
172
+
173
+ raise EvaluationError.new("Conflicting object key #{key.inspect}", rule: nil, location: nil)
174
+ end
175
+
176
+ def evaluate_complete_rules(rules)
177
+ values = rules.reject(&:default_value).map do |rule|
178
+ complete_rule_value_with_else(rule)
179
+ end.reject(&:undefined?)
180
+
181
+ resolved = resolve_conflicts(values, rules.first.name)
182
+ return resolved if resolved
183
+
184
+ default_rule = rules.find(&:default_value)
185
+ return UndefinedValue.new unless default_rule
186
+
187
+ expression_evaluator.evaluate(default_rule.default_value)
188
+ end
189
+
190
+ def evaluate_partial_object_value(head)
191
+ key = expression_evaluator.evaluate(head[:key])
192
+ value = expression_evaluator.evaluate(head[:value])
193
+ return UndefinedValue.new if key.is_a?(UndefinedValue) || value.is_a?(UndefinedValue)
194
+
195
+ [key.to_ruby, value]
196
+ end
197
+
198
+ def evaluate_complete_rule_value(head, value_node = nil)
199
+ node = value_node || head[:value]
200
+ return expression_evaluator.evaluate(node) if node
201
+
202
+ BooleanValue.new(true)
203
+ end
204
+
205
+ def body_succeeds?(body)
206
+ literals = Array(body)
207
+ return true if literals.empty?
208
+
209
+ eval_query(literals, environment).any?
210
+ end
211
+
212
+ def query_literal_truthy?(literal)
213
+ eval_query([literal], environment).any?
214
+ end
215
+
216
+ def evaluate_rule_value(head)
217
+ case head[:type]
218
+ when :complete, :function
219
+ evaluate_complete_rule_value(head)
220
+ when :partial_set
221
+ expression_evaluator.evaluate(head[:term])
222
+ else
223
+ UndefinedValue.new
224
+ end
225
+ end
226
+
227
+ def some_decl_truthy?(literal)
228
+ each_some_solution(literal).any?
229
+ end
230
+
231
+ # :reek:TooManyStatements
232
+ # rubocop:disable Metrics/MethodLength
233
+ def rule_body_values(rule, initial_bindings = {})
234
+ environment.push_scope
235
+ values = environment.with_bindings(initial_bindings) do
236
+ eval_rule_body(rule.body, environment).filter_map do |bindings|
237
+ environment.with_bindings(bindings) do
238
+ value = evaluate_rule_value(rule.head)
239
+ value unless value.is_a?(UndefinedValue)
240
+ end
241
+ end
242
+ end
243
+ values
244
+ ensure
245
+ environment.pop_scope
246
+ end
247
+ # rubocop:enable Metrics/MethodLength
248
+
249
+ # :reek:TooManyStatements
250
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
251
+ def rule_body_pairs(rule)
252
+ environment.push_scope
253
+ values = eval_rule_body(rule.body, environment).filter_map do |bindings|
254
+ environment.with_bindings(bindings) do
255
+ pair = evaluate_partial_object_value(rule.head)
256
+ next unless pair.is_a?(Array)
257
+
258
+ nested_flag = rule.head.is_a?(Hash) && rule.head[:nested] ? true : false
259
+ [pair[0], pair[1], nested_flag]
260
+ end
261
+ end
262
+ values
263
+ ensure
264
+ environment.pop_scope
265
+ end
266
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
267
+
268
+ def complete_rule_value_with_else(rule)
269
+ values = rule_body_values(rule)
270
+ resolved = resolve_conflicts(values, rule.name)
271
+ return resolved if resolved
272
+
273
+ else_clause_value(rule, rule.else_clause)
274
+ end
275
+
276
+ def else_clause_value(rule, clause)
277
+ return UndefinedValue.new unless clause
278
+
279
+ values = evaluate_clause_value(rule, clause, empty_bindings)
280
+ resolved = resolve_conflicts(values, rule.name)
281
+ return resolved if resolved
282
+
283
+ else_clause_value(rule, clause[:else_clause])
284
+ end
285
+
286
+ def evaluate_value_with_body(context)
287
+ environment.push_scope
288
+ values = values_for_body_context(context)
289
+ values
290
+ ensure
291
+ environment.pop_scope
292
+ end
293
+
294
+ def values_for_body_context(context)
295
+ environment.with_bindings(context.initial_bindings) do
296
+ eval_rule_body(context.body, environment).filter_map do |bindings|
297
+ environment.with_bindings(bindings) { evaluate_value_node(context.rule, context.value_node) }
298
+ end
299
+ end
300
+ end
301
+
302
+ def evaluate_value_node(rule, value_node)
303
+ value = evaluate_complete_rule_value(rule.head, value_node)
304
+ value unless value.is_a?(UndefinedValue)
305
+ end
306
+
307
+ def resolve_conflicts(values, name)
308
+ return nil if values.empty?
309
+
310
+ unique = values.uniq
311
+ return unique.first if unique.length == 1
312
+
313
+ raise EvaluationError.new("Conflicting values for #{name}", rule: name)
314
+ end
315
+
316
+ def evaluate_function_rules(rules, args)
317
+ # @type var values: Array[Value]
318
+ values = []
319
+ rules.reject(&:default_value).each do |rule|
320
+ values.concat(function_rule_values(rule, args))
321
+ end
322
+
323
+ return values unless values.empty?
324
+
325
+ default_rule = rules.find(&:default_value)
326
+ return [] unless default_rule
327
+
328
+ function_rule_values(default_rule, args)
329
+ end
330
+
331
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
332
+ def function_rule_values(rule, args)
333
+ head_args = Array(rule.head[:args])
334
+ return [] unless head_args.length == args.length
335
+
336
+ # @type var binding_sets: Array[Hash[String, Value]]
337
+ binding_sets = [{}]
338
+ head_args.each_with_index do |pattern, index|
339
+ # @type var next_sets: Array[Hash[String, Value]]
340
+ next_sets = []
341
+ binding_sets.each do |bindings|
342
+ environment.with_bindings(bindings) do
343
+ unifier.unify(pattern, args[index], environment).each do |new_bindings|
344
+ merged = merge_bindings(bindings, new_bindings)
345
+ next_sets << merged if merged
346
+ end
347
+ end
348
+ end
349
+ binding_sets = next_sets
350
+ end
351
+
352
+ binding_sets.flat_map do |bindings|
353
+ function_values_with_else(rule, bindings)
354
+ end
355
+ end
356
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
357
+
358
+ def function_values_with_else(rule, bindings)
359
+ values = rule_body_values(rule, bindings)
360
+ return values unless values.empty?
361
+
362
+ function_else_values(rule, rule.else_clause, bindings)
363
+ end
364
+
365
+ def function_else_values(rule, clause, bindings)
366
+ return [] unless clause
367
+
368
+ values = evaluate_clause_value(rule, clause, bindings)
369
+ return values unless values.empty?
370
+
371
+ function_else_values(rule, clause[:else_clause], bindings)
372
+ end
373
+
374
+ def empty_bindings
375
+ {} # @type var empty_bindings: Hash[String, Value]
376
+ end
377
+
378
+ def evaluate_clause_value(rule, clause, bindings)
379
+ context = ValueEvaluationContext.new(
380
+ body: clause[:body],
381
+ rule: rule,
382
+ value_node: clause[:value],
383
+ initial_bindings: bindings
384
+ )
385
+ evaluate_value_with_body(context)
386
+ end
387
+
388
+ def eval_rule_body(body, env)
389
+ eval_query(Array(body), env)
390
+ end
391
+
392
+ # :reek:TooManyStatements
393
+ # rubocop:disable Metrics/MethodLength
394
+ def eval_query(literals, env)
395
+ literals = Array(literals)
396
+ if literals.empty?
397
+ # @type var empty_bindings: Hash[String, Value]
398
+ empty_bindings = {}
399
+ return Enumerator.new { |yielder| yielder << empty_bindings }
400
+ end
401
+
402
+ Enumerator.new do |yielder|
403
+ with_query_scope(env, literals) do
404
+ bound_vars = Environment::RESERVED_NAMES.dup
405
+ context = QueryContext.new(literals: literals, env: env)
406
+ # @type var bindings: Hash[String, Value]
407
+ bindings = {}
408
+ yield_query_solutions(yielder, context, 0, bindings, bound_vars)
409
+ end
410
+ end
411
+ end
412
+ # rubocop:enable Metrics/MethodLength
413
+
414
+ def with_query_scope(env, literals)
415
+ env.push_scope
416
+ shadow_query_locals(env, literals)
417
+ yield
418
+ ensure
419
+ env.pop_scope
420
+ end
421
+
422
+ def shadow_query_locals(env, literals)
423
+ details = BoundVariableCollector.new.collect_details(literals)
424
+ explicit = details[:explicit]
425
+ shadow_explicit_locals(env, explicit)
426
+ shadow_unification_locals(env, details[:unification], explicit)
427
+ end
428
+
429
+ def shadow_explicit_locals(env, names)
430
+ names.each { |name| bind_undefined(env, name) }
431
+ end
432
+
433
+ def shadow_unification_locals(env, names, explicit_names)
434
+ names.each do |name|
435
+ next if explicit_names.include?(name)
436
+ next unless env.lookup(name).is_a?(UndefinedValue)
437
+
438
+ bind_undefined(env, name)
439
+ end
440
+ end
441
+
442
+ # :reek:UtilityFunction
443
+ def bind_undefined(env, name)
444
+ return if Environment::RESERVED_NAMES.include?(name) || name == "_"
445
+
446
+ env.bind(name, UndefinedValue.new)
447
+ end
448
+
449
+ # rubocop:disable Metrics/MethodLength
450
+ # :reek:TooManyStatements
451
+ # :reek:LongParameterList
452
+ def yield_query_solutions(yielder, context, index, bindings, bound_vars)
453
+ literals = context.literals
454
+ env = context.env
455
+ if index >= literals.length
456
+ yielder << bindings
457
+ return
458
+ end
459
+
460
+ literal = literals[index]
461
+ eval_literal(literal, env, bound_vars).each do |literal_bindings|
462
+ merged = merge_bindings(bindings, literal_bindings)
463
+ next unless merged
464
+
465
+ env.with_bindings(literal_bindings) do
466
+ next_bound_vars = bound_vars | literal_bindings.keys
467
+ yield_query_solutions(yielder, context, index + 1, merged, next_bound_vars)
468
+ end
469
+ end
470
+ end
471
+ # rubocop:enable Metrics/MethodLength
472
+
473
+ def eval_literal(literal, env, bound_vars)
474
+ return eval_query_literal(literal, env, bound_vars) if literal.is_a?(AST::QueryLiteral)
475
+ return eval_some_decl(literal, env) if literal.is_a?(AST::SomeDecl)
476
+
477
+ raise EvaluationError.new("Unsupported query literal: #{literal.class}", rule: nil, location: nil)
478
+ end
479
+
480
+ def eval_query_literal(literal, env, bound_vars)
481
+ expression = literal.expression
482
+ modifiers = literal.with_modifiers
483
+ return eval_query_expression(expression, env, bound_vars) if modifiers.empty?
484
+
485
+ context = ModifierContext.new(expression: expression, env: env, bound_vars: bound_vars)
486
+ with_modifiers_enum(modifiers, context)
487
+ end
488
+
489
+ # :reek:NestedIterators
490
+ def with_modifiers_enum(modifiers, context)
491
+ Enumerator.new do |yielder|
492
+ WithModifiers::WithModifierApplier.apply(modifiers, context.env, expression_evaluator) do |modified_env|
493
+ yield_query_expression(yielder, context.with_env(modified_env))
494
+ end
495
+ end
496
+ end
497
+
498
+ # :reek:FeatureEnvy
499
+ def yield_query_expression(yielder, context)
500
+ eval_query_expression(context.expression, context.env, context.bound_vars).each do |bindings|
501
+ yielder << bindings
502
+ end
503
+ end
504
+
505
+ def eval_query_expression(expression, env, bound_vars)
506
+ case expression
507
+ in AST::UnaryOp[operator: :not, operand:]
508
+ eval_not(operand, env, bound_vars)
509
+ else
510
+ expression_evaluator.eval_with_unification(expression, env)
511
+ end
512
+ end
513
+
514
+ def eval_not(expr, env, bound_vars)
515
+ check_safety(expr, env, bound_vars)
516
+ Enumerator.new do |yielder|
517
+ solutions = expression_evaluator.eval_with_unification(expr, env)
518
+ yielder << {} unless solutions.any?
519
+ end
520
+ end
521
+
522
+ def check_safety(expr, env, bound_vars)
523
+ unbound = unbound_variables(VariableCollector.new.collect(expr), env, bound_vars)
524
+ return if unbound.empty?
525
+
526
+ raise_unsafe_negation(expr, unbound)
527
+ end
528
+
529
+ def raise_unsafe_negation(expr, unbound)
530
+ message = "Unsafe negation: unbound variables #{unbound.sort.join(", ")}"
531
+ raise EvaluationError.new(message, rule: nil, location: expr.location)
532
+ end
533
+
534
+ # :reek:UtilityFunction
535
+ def unbound_variables(names, env, bound_vars)
536
+ safe_names = bound_vars | Environment::RESERVED_NAMES | ["_"]
537
+ names.reject { |name| safe_names.include?(name) || env_bound?(env, name) }.uniq
538
+ end
539
+
540
+ # :reek:UtilityFunction
541
+ def env_bound?(env, name)
542
+ !env.lookup(name).is_a?(UndefinedValue)
543
+ end
544
+ end
545
+ # rubocop:enable Metrics/ClassLength
546
+ end
547
+ end
548
+ end
549
+
550
+ require_relative "rule_evaluator/bindings"
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ class Evaluator
6
+ # Provides evaluated rule values for data references.
7
+ class RuleValueProvider
8
+ # @param rules_by_name [Hash{String => Array<AST::Rule>}]
9
+ # @param memoization [Memoization::Store, nil]
10
+ def initialize(rules_by_name:, memoization: nil)
11
+ @rules_by_name = rules_by_name
12
+ @memoization = memoization
13
+ @rule_evaluator = nil
14
+ end
15
+
16
+ # @param rule_evaluator [RuleEvaluator]
17
+ # @return [void]
18
+ def attach(rule_evaluator)
19
+ @rule_evaluator = rule_evaluator
20
+ end
21
+
22
+ # @param name [String]
23
+ # @return [Value]
24
+ def value_for(name)
25
+ memoization ? memoized_value_for(name) : evaluate_value_for(name)
26
+ end
27
+
28
+ # @param name [String]
29
+ # @return [Boolean]
30
+ def rule_defined?(name)
31
+ rules_by_name.key?(name)
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :memoization, :rule_evaluator, :rules_by_name
37
+
38
+ def memoized_value_for(name)
39
+ memo = memoization
40
+ return evaluate_value_for(name) unless memo
41
+
42
+ cache = memo.context.rule_values
43
+ cache.fetch(name.to_s) { |key| cache[key] = evaluate_value_for(key) }
44
+ end
45
+
46
+ def evaluate_value_for(name)
47
+ rules = rules_by_name.fetch(name) { [] } # @type var rules: Array[AST::Rule]
48
+ return UndefinedValue.new if rules.empty?
49
+ return UndefinedValue.new unless rule_evaluator
50
+
51
+ rule_evaluator.evaluate_group(rules)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end