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.
- checksums.yaml +7 -0
- data/.reek.yml +80 -0
- data/.vscode/extensions.json +19 -0
- data/.vscode/launch.json +35 -0
- data/.vscode/settings.json +25 -0
- data/.vscode/tasks.json +117 -0
- data/.yardopts +12 -0
- data/ARCHITECTURE.md +39 -0
- data/CHANGELOG.md +25 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/RELEASING.md +37 -0
- data/Rakefile +38 -0
- data/SECURITY.md +26 -0
- data/Steepfile +10 -0
- data/TODO.md +35 -0
- data/benchmark/builtin_calls.rb +29 -0
- data/benchmark/complex_policy.rb +19 -0
- data/benchmark/comprehensions.rb +19 -0
- data/benchmark/simple_rules.rb +20 -0
- data/examples/README.md +27 -0
- data/examples/sample_config.yaml +2 -0
- data/examples/simple_policy.rego +7 -0
- data/examples/validation_policy.rego +11 -0
- data/exe/rego-validate +6 -0
- data/lib/ruby/rego/ast/base.rb +95 -0
- data/lib/ruby/rego/ast/binary_op.rb +64 -0
- data/lib/ruby/rego/ast/call.rb +27 -0
- data/lib/ruby/rego/ast/composite.rb +48 -0
- data/lib/ruby/rego/ast/comprehension.rb +63 -0
- data/lib/ruby/rego/ast/every.rb +37 -0
- data/lib/ruby/rego/ast/import.rb +32 -0
- data/lib/ruby/rego/ast/literal.rb +70 -0
- data/lib/ruby/rego/ast/module.rb +32 -0
- data/lib/ruby/rego/ast/package.rb +22 -0
- data/lib/ruby/rego/ast/query.rb +63 -0
- data/lib/ruby/rego/ast/reference.rb +58 -0
- data/lib/ruby/rego/ast/rule.rb +114 -0
- data/lib/ruby/rego/ast/unary_op.rb +42 -0
- data/lib/ruby/rego/ast/variable.rb +22 -0
- data/lib/ruby/rego/ast.rb +17 -0
- data/lib/ruby/rego/builtins/aggregates.rb +124 -0
- data/lib/ruby/rego/builtins/base.rb +95 -0
- data/lib/ruby/rego/builtins/collections/array_ops.rb +103 -0
- data/lib/ruby/rego/builtins/collections/object_ops.rb +120 -0
- data/lib/ruby/rego/builtins/collections/set_ops.rb +51 -0
- data/lib/ruby/rego/builtins/collections.rb +137 -0
- data/lib/ruby/rego/builtins/comparisons/casts.rb +139 -0
- data/lib/ruby/rego/builtins/comparisons.rb +84 -0
- data/lib/ruby/rego/builtins/numeric_helpers.rb +56 -0
- data/lib/ruby/rego/builtins/registry.rb +199 -0
- data/lib/ruby/rego/builtins/registry_helpers.rb +27 -0
- data/lib/ruby/rego/builtins/strings/case_ops.rb +22 -0
- data/lib/ruby/rego/builtins/strings/concat.rb +19 -0
- data/lib/ruby/rego/builtins/strings/formatting.rb +35 -0
- data/lib/ruby/rego/builtins/strings/helpers.rb +62 -0
- data/lib/ruby/rego/builtins/strings/number_helpers.rb +48 -0
- data/lib/ruby/rego/builtins/strings/search.rb +63 -0
- data/lib/ruby/rego/builtins/strings/split.rb +19 -0
- data/lib/ruby/rego/builtins/strings/substring.rb +22 -0
- data/lib/ruby/rego/builtins/strings/trim.rb +42 -0
- data/lib/ruby/rego/builtins/strings/trim_helpers.rb +62 -0
- data/lib/ruby/rego/builtins/strings.rb +58 -0
- data/lib/ruby/rego/builtins/types.rb +89 -0
- data/lib/ruby/rego/call_name.rb +55 -0
- data/lib/ruby/rego/cli.rb +1122 -0
- data/lib/ruby/rego/compiled_module.rb +114 -0
- data/lib/ruby/rego/compiler.rb +1097 -0
- data/lib/ruby/rego/environment/overrides.rb +33 -0
- data/lib/ruby/rego/environment/reference_resolution.rb +86 -0
- data/lib/ruby/rego/environment.rb +230 -0
- data/lib/ruby/rego/environment_pool.rb +71 -0
- data/lib/ruby/rego/error_handling.rb +58 -0
- data/lib/ruby/rego/error_payload.rb +34 -0
- data/lib/ruby/rego/errors.rb +196 -0
- data/lib/ruby/rego/evaluator/assignment_support.rb +126 -0
- data/lib/ruby/rego/evaluator/binding_helpers.rb +60 -0
- data/lib/ruby/rego/evaluator/comprehension_evaluator.rb +182 -0
- data/lib/ruby/rego/evaluator/expression_dispatch.rb +45 -0
- data/lib/ruby/rego/evaluator/expression_evaluator.rb +492 -0
- data/lib/ruby/rego/evaluator/object_literal_evaluator.rb +52 -0
- data/lib/ruby/rego/evaluator/operator_evaluator.rb +163 -0
- data/lib/ruby/rego/evaluator/query_node_builder.rb +38 -0
- data/lib/ruby/rego/evaluator/reference_key_resolver.rb +50 -0
- data/lib/ruby/rego/evaluator/reference_resolver.rb +352 -0
- data/lib/ruby/rego/evaluator/rule_evaluator/bindings.rb +70 -0
- data/lib/ruby/rego/evaluator/rule_evaluator.rb +550 -0
- data/lib/ruby/rego/evaluator/rule_value_provider.rb +56 -0
- data/lib/ruby/rego/evaluator/variable_collector.rb +221 -0
- data/lib/ruby/rego/evaluator.rb +174 -0
- data/lib/ruby/rego/lexer/number_reader.rb +68 -0
- data/lib/ruby/rego/lexer/stream.rb +137 -0
- data/lib/ruby/rego/lexer/string_reader.rb +90 -0
- data/lib/ruby/rego/lexer/template_string_reader.rb +62 -0
- data/lib/ruby/rego/lexer.rb +206 -0
- data/lib/ruby/rego/location.rb +73 -0
- data/lib/ruby/rego/memoization.rb +67 -0
- data/lib/ruby/rego/parser/collections.rb +173 -0
- data/lib/ruby/rego/parser/expressions.rb +216 -0
- data/lib/ruby/rego/parser/precedence.rb +42 -0
- data/lib/ruby/rego/parser/query.rb +139 -0
- data/lib/ruby/rego/parser/references.rb +115 -0
- data/lib/ruby/rego/parser/rules.rb +310 -0
- data/lib/ruby/rego/parser.rb +210 -0
- data/lib/ruby/rego/policy.rb +50 -0
- data/lib/ruby/rego/result.rb +91 -0
- data/lib/ruby/rego/token.rb +206 -0
- data/lib/ruby/rego/unifier.rb +451 -0
- data/lib/ruby/rego/value.rb +379 -0
- data/lib/ruby/rego/version.rb +7 -0
- data/lib/ruby/rego/with_modifiers/with_modifier.rb +37 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_applier.rb +48 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_builtin_override.rb +128 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_context.rb +120 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_path_key_resolver.rb +42 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_path_override.rb +99 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_root_scope.rb +58 -0
- data/lib/ruby/rego.rb +72 -0
- data/sig/objspace.rbs +4 -0
- data/sig/psych.rbs +7 -0
- data/sig/rego_validate.rbs +382 -0
- data/sig/ruby/rego.rbs +2150 -0
- 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
|