Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1782,6 +1782,7 @@ impl Interpreter {

let mut comps = self.eval_rule_ref(&rule_ref)?;
if let Some(ke) = &key_expr {
is_const_rule = is_const_rule && Self::is_simple_literal(ke)?;
comps.push(self.eval_expr(ke)?);
}
let output = if let Some(oe) = &output_expr {
Expand Down
8 changes: 8 additions & 0 deletions src/languages/rego/compiler/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ pub enum CompilerError {
#[error("Invalid function expression with package")]
InvalidFunctionExpressionWithPackage,

#[error("partial object rules with constant keys are not yet supported by the RVM compiler")]
PartialObjectConstantKeyUnsupported,

#[error(
"partial object rules with nested bracket keys are not yet supported by the RVM compiler"
)]
PartialObjectNestedKeyUnsupported,

#[error("Compilation error: {message}")]
General { message: String },
}
Expand Down
54 changes: 53 additions & 1 deletion src/languages/rego/compiler/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ impl<'a> Compiler<'a> {
crate::ast::Expr::RefBrack { .. } if assign.is_some() => {
RuleType::PartialObject
}
crate::ast::Expr::RefBrack { .. } => RuleType::PartialSet,
crate::ast::Expr::RefBrack { .. } => RuleType::PartialObject,
_ => RuleType::Complete,
},
_ => RuleType::Complete,
Expand Down Expand Up @@ -88,6 +88,54 @@ impl<'a> Compiler<'a> {
})
}

fn validate_partial_object_shape(&self, refr: &ExprRef) -> Result<()> {
let Expr::RefBrack {
refr: prefix,
index,
..
} = refr.as_ref()
else {
return Ok(());
};

if Self::has_unsupported_bracket_prefix(prefix) {
return Err(CompilerError::PartialObjectNestedKeyUnsupported.at(refr.span()));
}

if Self::is_simple_literal(index) {
return Err(CompilerError::PartialObjectConstantKeyUnsupported.at(index.span()));
}

Ok(())
}

Comment on lines +92 to +111
fn has_unsupported_bracket_prefix(expr: &ExprRef) -> bool {
match expr.as_ref() {
Expr::RefBrack { refr, index, .. } => {
!Self::is_string_literal(index) || Self::has_unsupported_bracket_prefix(refr)
}
Expr::RefDot { refr, .. } => Self::has_unsupported_bracket_prefix(refr),
_ => false,
}
}

fn is_string_literal(expr: &ExprRef) -> bool {
matches!(expr.as_ref(), Expr::String { .. } | Expr::RawString { .. })
}

fn is_simple_literal(expr: &ExprRef) -> bool {
match expr.as_ref() {
Expr::String { .. }
| Expr::RawString { .. }
| Expr::Number { .. }
| Expr::Bool { .. }
| Expr::Null { .. } => true,
// Unary expressions like `-1` are constant literals too.
Expr::UnaryExpr { expr, .. } => Self::is_simple_literal(expr),
_ => false,
}
}

pub(super) fn get_or_assign_rule_index(&mut self, rule_path: &str) -> Result<u16> {
if let Some(&index) = self.rule_index_map.get(rule_path) {
return Ok(index);
Expand Down Expand Up @@ -345,6 +393,10 @@ impl<'a> Compiler<'a> {

let (key_expr, value_expr) = match head {
RuleHead::Compr { refr, assign, .. } => {
if rule_type == RuleType::PartialObject {
self.validate_partial_object_shape(refr)?;
}

self.rule_definition_function_params[rule_index as usize].push(None);
self.rule_definition_destructuring_patterns[rule_index as usize]
.push(None);
Expand Down
150 changes: 150 additions & 0 deletions tests/interpreter/cases/rule/partial_object_v1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

cases:
- note: constant_key_partial_object_v1
data: {}
input:
enabled: true
modules:
- |
package test
import rego.v1

p["fixed"] if {
input.enabled
}
Comment on lines +4 to +16
query: data.test
want_result:
p:
fixed: true

- note: multilevel_partial_object_v1
data: {}
input:
nested:
app:
read: 1
write: 2
ops:
deploy: 3
modules:
- |
package test
import rego.v1

p[a][b] if {
some a, obj in input.nested
some b, _ in obj
}
query: data.test
want_result:
p:
app:
read: true
write: true
ops:
deploy: true

- note: constant_key_partial_object_explicit_value_v1
data: {}
input:
enabled: true
modules:
- |
package test
import rego.v1

p["fixed"] := 7 if {
input.enabled
}
query: data.test
want_result:
p:
fixed: 7

- note: multilevel_partial_object_explicit_value_v1
data: {}
input:
nested:
app:
read: 1
write: 2
ops:
deploy: 3
modules:
- |
package test
import rego.v1

p[a][b] := v if {
some a, obj in input.nested
some b, v in obj
}
query: data.test
want_result:
p:
app:
read: 1
write: 2
ops:
deploy: 3

- note: issue_712_reproducer_v0_partial_set
data: {}
input:
servers:
FOO: 1
BAR: 2
BAZ: 3
modules:
- |
package test
import future.keywords.in

violations[k] {
some k, _ in input.servers
}
query: data.test.violations
want_result:
set!: ["BAR", "BAZ", "FOO"]

- note: issue_712_reproducer_v1_partial_object
data: {}
input:
servers:
FOO: 1
BAR: 2
BAZ: 3
modules:
- |
package test
import rego.v1

violations[k] if {
some k, _ in input.servers
}
query: data.test.violations
want_result:
BAR: true
BAZ: true
FOO: true

- note: issue_712_reproducer_v1_contains_partial_set
data: {}
input:
servers:
FOO: 1
BAR: 2
BAZ: 3
modules:
- |
package test
import rego.v1

violations contains k if {
some k, _ in input.servers
}
query: data.test.violations
want_result:
set!: ["BAR", "BAZ", "FOO"]
16 changes: 16 additions & 0 deletions tests/opa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,14 @@ fn is_with_keyword_unsupported_error(err: &anyhow::Error) -> bool {
})
}

fn is_partial_object_unsupported_error(err: &anyhow::Error) -> bool {
err.chain().any(|cause| {
let msg = cause.to_string();
msg.contains("partial object rules with constant keys are not yet supported")
|| msg.contains("partial object rules with nested bracket keys are not yet supported")
})
}

fn maybe_verify_rvm_case(case: &TestCase, is_rego_v0_test: bool, actual: &Value) -> Result<()> {
if case.note == "defaultkeyword/function with var arg, ref head query" {
println!(
Expand Down Expand Up @@ -308,6 +316,14 @@ fn maybe_verify_rvm_case(case: &TestCase, is_rego_v0_test: bool, actual: &Value)
return Ok(());
}

if is_partial_object_unsupported_error(&err) {
println!(
" skipping RVM check for '{}' (partial object pattern unsupported)",
case.note
);
return Ok(());
}

return Err(err);
}
};
Expand Down
Loading
Loading