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
6 changes: 6 additions & 0 deletions policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,3 +59,9 @@ java_library(
name = "compiler_builder",
exports = ["//policy/src/main/java/dev/cel/policy:compiler_builder"],
)

java_library(
name = "rule_composer",
visibility = ["//:internal"],
exports = ["//policy/src/main/java/dev/cel/policy:rule_composer"],
)
3 changes: 2 additions & 1 deletion policy/src/main/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -244,7 +244,6 @@ java_library(
java_library(
name = "rule_composer",
srcs = ["RuleComposer.java"],
visibility = ["//visibility:private"],
deps = [
":compiled_rule",
"//bundle:cel",
Expand All@@ -257,6 +256,8 @@ java_library(
"//common/ast:mutable_expr",
"//common/formats:value_string",
"//common/navigation:mutable_navigation",
"//common/types:cel_types",
"//common/types:type_providers",
"//extensions:optional_library",
"//optimizer:ast_optimizer",
"//optimizer:mutable_ast",
Expand Down
88 changes: 58 additions & 30 deletions policy/src/main/java/dev/cel/policy/RuleComposer.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.stream.Collectors.toCollection;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import dev.cel.bundle.Cel;
Expand All@@ -32,11 +33,14 @@
import dev.cel.common.formats.ValueString;
import dev.cel.common.navigation.CelNavigableMutableAst;
import dev.cel.common.navigation.CelNavigableMutableExpr;
import dev.cel.common.types.CelType;
import dev.cel.common.types.CelTypes;
import dev.cel.extensions.CelOptionalLibrary.Function;
import dev.cel.optimizer.AstMutator;
import dev.cel.optimizer.CelAstOptimizer;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.OutputValue;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
import java.util.ArrayList;
import java.util.Arrays;
Expand DownExpand Up@@ -74,54 +78,70 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) {
}

long lastOutputId = 0;
// The expected output type of the rule, used to verify that all branches agree on the type.
CelType lastOutputType = null;
for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) {
CelAbstractSyntaxTree conditionAst = match.condition();
boolean isTriviallyTrue = match.isConditionTriviallyTrue();
CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst);

long currentSourceId = lastOutputId;

switch (match.result().kind()) {
case OUTPUT:
// If the match has an output, then it is considered a non-optional output since
// it is explicitly stated. If the rule itself is optional, then the base case value
// of output being optional.none() will convert the non-optional value to an optional
// one.
OutputValue matchOutput = match.result().output();
CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast());
Step step = Step.newNonOptionalStep(!isTriviallyTrue, condAst, outAst);
Step step =
Step.newNonOptionalStep(
!isTriviallyTrue, condAst, CelMutableAst.fromCelAst(matchOutput.ast()));
currentSourceId = matchOutput.sourceId();

output = combine(astMutator, step, output);

assertComposedAstIsValid(
cel,
output.expr,
"incompatible output types found.",
matchOutput.sourceId(),
lastOutputId);
lastOutputId = matchOutput.sourceId();
String outputFailureMessage =
String.format(
"incompatible output types: block has output type %s, but previous outputs have"
+ " type %s",
lastOutputType == null ? "" : CelTypes.format(lastOutputType),
CelTypes.format(matchOutput.ast().getResultType()));
lastOutputType =
assertComposedAstIsValid(
cel, output.expr, outputFailureMessage, currentSourceId, lastOutputId)
.getResultType();

break;
case RULE:
// If the match has a nested rule, then compute the rule and whether it has
// an optional return value.
CelCompiledRule matchNestedRule = match.result().rule();
Step nestedRule = optimizeRule(cel, matchNestedRule);
boolean nestedHasOptional = matchNestedRule.hasOptionalOutput();

Step ruleStep =
nestedHasOptional
? Step.newOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr)
: Step.newNonOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr);
new Step(
matchNestedRule.hasOptionalOutput(), !isTriviallyTrue, condAst, nestedRule.expr);
currentSourceId = getFirstOutputSourceId(matchNestedRule);

output = combine(astMutator, ruleStep, output);

assertComposedAstIsValid(
cel,
output.expr,
String.format(
"failed composing the subrule '%s' due to incompatible output types.",
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
lastOutputId);
lastOutputType =
assertComposedAstIsValid(
cel,
output.expr,
String.format(
"failed composing the subrule '%s' due to incompatible output types.",
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
currentSourceId,
lastOutputId)
.getResultType();
break;
}

lastOutputId = currentSourceId;
}

Preconditions.checkState(output != null, "Policy contains no outputs.");
CelMutableAst resultExpr = output.expr;
resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables());
resultExpr = astMutator.renumberIdsConsecutively(resultExpr);
Expand DownExpand Up@@ -266,21 +286,34 @@ private CelMutableAst inlineCompiledVariables(
return mutatedAst;
}

private void assertComposedAstIsValid(
private CelAbstractSyntaxTree assertComposedAstIsValid(
Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) {
assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
return assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
}

private void assertComposedAstIsValid(
private CelAbstractSyntaxTree assertComposedAstIsValid(
Cel cel, CelMutableAst composedAst, String failureMessage, List<Long> ids) {
try {
cel.check(composedAst.toParsedAst()).getAst();
return cel.check(composedAst.toParsedAst()).getAst();
} catch (CelValidationException e) {
ids = ids.stream().filter(id -> id > 0).collect(toCollection(ArrayList::new));
throw new RuleCompositionException(failureMessage, e, ids);
}
}

private static long getFirstOutputSourceId(CelCompiledRule rule) {
for (CelCompiledMatch match : rule.matches()) {
if (match.result().kind() == Result.Kind.OUTPUT) {
return match.result().output().sourceId();
} else if (match.result().kind() == Result.Kind.RULE) {
return getFirstOutputSourceId(match.result().rule());
}
}

// Fallback to the nested rule ID if the policy is invalid and contains no output
return rule.sourceId();
}

// Step represents an intermediate stage of rule and match expression composition.
//
// The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of
Expand DownExpand Up@@ -311,11 +344,6 @@ private Step(
this.expr = expr;
}

private static Step newOptionalStep(
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
return new Step(/* isOptional= */ true, isConditional, cond, expr);
}

private static Step newNonOptionalStep(
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
return new Step(/* isOptional= */ false, isConditional, cond, expr);
Expand Down
2 changes: 2 additions & 0 deletions policy/src/test/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,9 +27,11 @@ java_library(
"//parser:parser_factory",
"//parser:unparser",
"//policy",
"//policy:compiled_rule",
"//policy:compiler_factory",
"//policy:parser",
"//policy:parser_factory",
"//policy:rule_composer",
"//policy:source",
"//policy:validation_exception",
"//policy/testing:k8s_test_tag_handler",
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,7 @@
import dev.cel.bundle.CelEnvironmentYamlParser;
import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.CelOptions;
import dev.cel.common.formats.ValueString;
import dev.cel.common.types.OptionalType;
import dev.cel.common.types.SimpleType;
import dev.cel.expr.conformance.proto3.TestAllTypes;
Expand DownExpand Up@@ -356,6 +357,24 @@ public void evaluateYamlPolicy_withSimpleVariable() throws Exception {
assertThat(evalResult).isFalse();
}

@Test
public void compose_ruleWithNoOutputs_throws() throws Exception {
Cel cel = newCel();
CelCompiledRule emptyRule =
CelCompiledRule.create(
1L,
Optional.of(ValueString.of(2L, "empty_rule")),
ImmutableList.of(),
ImmutableList.of(),
cel);
RuleComposer composer = RuleComposer.newInstance(emptyRule, "variables.", 1000);
CelAbstractSyntaxTree ast = cel.compile("true").getAst();

IllegalStateException e =
assertThrows(IllegalStateException.class, () -> composer.optimize(ast, cel));
assertThat(e).hasMessageThat().isEqualTo("Policy contains no outputs.");
}

private static final class EvaluablePolicyTestData {
private final TestYamlPolicy yamlPolicy;
private final PolicyTestCase testCase;
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types found.
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool
| output: "false"
| .............^
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types found.
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool
| - output: "{'banned': true}"
| .............^
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
ERROR: compose_errors_conflicting_subrule/policy.yaml:34:18: failed composing the subrule 'banned regions' due to incompatible output types.
| output: "true"
| .................^
ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to incompatible output types.
| output: "{'banned': false}"
| .............^
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
6 changes: 6 additions & 0 deletions policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,3 +59,9 @@ java_library(
name = "compiler_builder",
exports = ["//policy/src/main/java/dev/cel/policy:compiler_builder"],
)

java_library(
name = "rule_composer",
visibility = ["//:internal"],
exports = ["//policy/src/main/java/dev/cel/policy:rule_composer"],
)
3 changes: 2 additions & 1 deletion policy/src/main/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -244,7 +244,6 @@ java_library(
java_library(
name = "rule_composer",
srcs = ["RuleComposer.java"],
visibility = ["//visibility:private"],
deps = [
":compiled_rule",
"//bundle:cel",
Expand All@@ -257,6 +256,8 @@ java_library(
"//common/ast:mutable_expr",
"//common/formats:value_string",
"//common/navigation:mutable_navigation",
"//common/types:cel_types",
"//common/types:type_providers",
"//extensions:optional_library",
"//optimizer:ast_optimizer",
"//optimizer:mutable_ast",
Expand Down
88 changes: 58 additions & 30 deletions policy/src/main/java/dev/cel/policy/RuleComposer.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.stream.Collectors.toCollection;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import dev.cel.bundle.Cel;
Expand All@@ -32,11 +33,14 @@
import dev.cel.common.formats.ValueString;
import dev.cel.common.navigation.CelNavigableMutableAst;
import dev.cel.common.navigation.CelNavigableMutableExpr;
import dev.cel.common.types.CelType;
import dev.cel.common.types.CelTypes;
import dev.cel.extensions.CelOptionalLibrary.Function;
import dev.cel.optimizer.AstMutator;
import dev.cel.optimizer.CelAstOptimizer;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.OutputValue;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
import java.util.ArrayList;
import java.util.Arrays;
Expand DownExpand Up@@ -74,54 +78,70 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) {
}

long lastOutputId = 0;
// The expected output type of the rule, used to verify that all branches agree on the type.
CelType lastOutputType = null;
for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) {
CelAbstractSyntaxTree conditionAst = match.condition();
boolean isTriviallyTrue = match.isConditionTriviallyTrue();
CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst);

long currentSourceId = lastOutputId;

switch (match.result().kind()) {
case OUTPUT:
// If the match has an output, then it is considered a non-optional output since
// it is explicitly stated. If the rule itself is optional, then the base case value
// of output being optional.none() will convert the non-optional value to an optional
// one.
OutputValue matchOutput = match.result().output();
CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast());
Step step = Step.newNonOptionalStep(!isTriviallyTrue, condAst, outAst);
Step step =
Step.newNonOptionalStep(
!isTriviallyTrue, condAst, CelMutableAst.fromCelAst(matchOutput.ast()));
currentSourceId = matchOutput.sourceId();

output = combine(astMutator, step, output);

assertComposedAstIsValid(
cel,
output.expr,
"incompatible output types found.",
matchOutput.sourceId(),
lastOutputId);
lastOutputId = matchOutput.sourceId();
String outputFailureMessage =
String.format(
"incompatible output types: block has output type %s, but previous outputs have"
+ " type %s",
lastOutputType == null ? "" : CelTypes.format(lastOutputType),
CelTypes.format(matchOutput.ast().getResultType()));
lastOutputType =
assertComposedAstIsValid(
cel, output.expr, outputFailureMessage, currentSourceId, lastOutputId)
.getResultType();

break;
case RULE:
// If the match has a nested rule, then compute the rule and whether it has
// an optional return value.
CelCompiledRule matchNestedRule = match.result().rule();
Step nestedRule = optimizeRule(cel, matchNestedRule);
boolean nestedHasOptional = matchNestedRule.hasOptionalOutput();

Step ruleStep =
nestedHasOptional
? Step.newOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr)
: Step.newNonOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr);
new Step(
matchNestedRule.hasOptionalOutput(), !isTriviallyTrue, condAst, nestedRule.expr);
currentSourceId = getFirstOutputSourceId(matchNestedRule);

output = combine(astMutator, ruleStep, output);

assertComposedAstIsValid(
cel,
output.expr,
String.format(
"failed composing the subrule '%s' due to incompatible output types.",
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
lastOutputId);
lastOutputType =
assertComposedAstIsValid(
cel,
output.expr,
String.format(
"failed composing the subrule '%s' due to incompatible output types.",
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
currentSourceId,
lastOutputId)
.getResultType();
break;
}

lastOutputId = currentSourceId;
}

Preconditions.checkState(output != null, "Policy contains no outputs.");
CelMutableAst resultExpr = output.expr;
resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables());
resultExpr = astMutator.renumberIdsConsecutively(resultExpr);
Expand DownExpand Up@@ -266,21 +286,34 @@ private CelMutableAst inlineCompiledVariables(
return mutatedAst;
}

private void assertComposedAstIsValid(
private CelAbstractSyntaxTree assertComposedAstIsValid(
Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) {
assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
return assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
}

private void assertComposedAstIsValid(
private CelAbstractSyntaxTree assertComposedAstIsValid(
Cel cel, CelMutableAst composedAst, String failureMessage, List<Long> ids) {
try {
cel.check(composedAst.toParsedAst()).getAst();
return cel.check(composedAst.toParsedAst()).getAst();
} catch (CelValidationException e) {
ids = ids.stream().filter(id -> id > 0).collect(toCollection(ArrayList::new));
throw new RuleCompositionException(failureMessage, e, ids);
}
}

private static long getFirstOutputSourceId(CelCompiledRule rule) {
for (CelCompiledMatch match : rule.matches()) {
if (match.result().kind() == Result.Kind.OUTPUT) {
return match.result().output().sourceId();
} else if (match.result().kind() == Result.Kind.RULE) {
return getFirstOutputSourceId(match.result().rule());
}
}

// Fallback to the nested rule ID if the policy is invalid and contains no output
return rule.sourceId();
}

// Step represents an intermediate stage of rule and match expression composition.
//
// The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of
Expand DownExpand Up@@ -311,11 +344,6 @@ private Step(
this.expr = expr;
}

private static Step newOptionalStep(
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
return new Step(/* isOptional= */ true, isConditional, cond, expr);
}

private static Step newNonOptionalStep(
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
return new Step(/* isOptional= */ false, isConditional, cond, expr);
Expand Down
2 changes: 2 additions & 0 deletions policy/src/test/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,9 +27,11 @@ java_library(
"//parser:parser_factory",
"//parser:unparser",
"//policy",
"//policy:compiled_rule",
"//policy:compiler_factory",
"//policy:parser",
"//policy:parser_factory",
"//policy:rule_composer",
"//policy:source",
"//policy:validation_exception",
"//policy/testing:k8s_test_tag_handler",
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,7 @@
import dev.cel.bundle.CelEnvironmentYamlParser;
import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.CelOptions;
import dev.cel.common.formats.ValueString;
import dev.cel.common.types.OptionalType;
import dev.cel.common.types.SimpleType;
import dev.cel.expr.conformance.proto3.TestAllTypes;
Expand DownExpand Up@@ -356,6 +357,24 @@ public void evaluateYamlPolicy_withSimpleVariable() throws Exception {
assertThat(evalResult).isFalse();
}

@Test
public void compose_ruleWithNoOutputs_throws() throws Exception {
Cel cel = newCel();
CelCompiledRule emptyRule =
CelCompiledRule.create(
1L,
Optional.of(ValueString.of(2L, "empty_rule")),
ImmutableList.of(),
ImmutableList.of(),
cel);
RuleComposer composer = RuleComposer.newInstance(emptyRule, "variables.", 1000);
CelAbstractSyntaxTree ast = cel.compile("true").getAst();

IllegalStateException e =
assertThrows(IllegalStateException.class, () -> composer.optimize(ast, cel));
assertThat(e).hasMessageThat().isEqualTo("Policy contains no outputs.");
}

private static final class EvaluablePolicyTestData {
private final TestYamlPolicy yamlPolicy;
private final PolicyTestCase testCase;
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types found.
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool
| output: "false"
| .............^
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types found.
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool
| - output: "{'banned': true}"
| .............^
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
ERROR: compose_errors_conflicting_subrule/policy.yaml:34:18: failed composing the subrule 'banned regions' due to incompatible output types.
| output: "true"
| .................^
ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to incompatible output types.
| output: "{'banned': false}"
| .............^
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
6 changes: 6 additions & 0 deletions policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,3 +59,9 @@ java_library(
name = "compiler_builder",
exports = ["//policy/src/main/java/dev/cel/policy:compiler_builder"],
)

java_library(
name = "rule_composer",
visibility = ["//:internal"],
exports = ["//policy/src/main/java/dev/cel/policy:rule_composer"],
)
3 changes: 2 additions & 1 deletion policy/src/main/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -244,7 +244,6 @@ java_library(
java_library(
name = "rule_composer",
srcs = ["RuleComposer.java"],
visibility = ["//visibility:private"],
deps = [
":compiled_rule",
"//bundle:cel",
Expand All@@ -257,6 +256,8 @@ java_library(
"//common/ast:mutable_expr",
"//common/formats:value_string",
"//common/navigation:mutable_navigation",
"//common/types:cel_types",
"//common/types:type_providers",
"//extensions:optional_library",
"//optimizer:ast_optimizer",
"//optimizer:mutable_ast",
Expand Down
88 changes: 58 additions & 30 deletions policy/src/main/java/dev/cel/policy/RuleComposer.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.stream.Collectors.toCollection;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import dev.cel.bundle.Cel;
Expand All@@ -32,11 +33,14 @@
import dev.cel.common.formats.ValueString;
import dev.cel.common.navigation.CelNavigableMutableAst;
import dev.cel.common.navigation.CelNavigableMutableExpr;
import dev.cel.common.types.CelType;
import dev.cel.common.types.CelTypes;
import dev.cel.extensions.CelOptionalLibrary.Function;
import dev.cel.optimizer.AstMutator;
import dev.cel.optimizer.CelAstOptimizer;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.OutputValue;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
import java.util.ArrayList;
import java.util.Arrays;
Expand DownExpand Up@@ -74,54 +78,70 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) {
}

long lastOutputId = 0;
// The expected output type of the rule, used to verify that all branches agree on the type.
CelType lastOutputType = null;
for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) {
CelAbstractSyntaxTree conditionAst = match.condition();
boolean isTriviallyTrue = match.isConditionTriviallyTrue();
CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst);

long currentSourceId = lastOutputId;

switch (match.result().kind()) {
case OUTPUT:
// If the match has an output, then it is considered a non-optional output since
// it is explicitly stated. If the rule itself is optional, then the base case value
// of output being optional.none() will convert the non-optional value to an optional
// one.
OutputValue matchOutput = match.result().output();
CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast());
Step step = Step.newNonOptionalStep(!isTriviallyTrue, condAst, outAst);
Step step =
Step.newNonOptionalStep(
!isTriviallyTrue, condAst, CelMutableAst.fromCelAst(matchOutput.ast()));
currentSourceId = matchOutput.sourceId();

output = combine(astMutator, step, output);

assertComposedAstIsValid(
cel,
output.expr,
"incompatible output types found.",
matchOutput.sourceId(),
lastOutputId);
lastOutputId = matchOutput.sourceId();
String outputFailureMessage =
String.format(
"incompatible output types: block has output type %s, but previous outputs have"
+ " type %s",
lastOutputType == null ? "" : CelTypes.format(lastOutputType),
CelTypes.format(matchOutput.ast().getResultType()));
lastOutputType =
assertComposedAstIsValid(
cel, output.expr, outputFailureMessage, currentSourceId, lastOutputId)
.getResultType();

break;
case RULE:
// If the match has a nested rule, then compute the rule and whether it has
// an optional return value.
CelCompiledRule matchNestedRule = match.result().rule();
Step nestedRule = optimizeRule(cel, matchNestedRule);
boolean nestedHasOptional = matchNestedRule.hasOptionalOutput();

Step ruleStep =
nestedHasOptional
? Step.newOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr)
: Step.newNonOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr);
new Step(
matchNestedRule.hasOptionalOutput(), !isTriviallyTrue, condAst, nestedRule.expr);
currentSourceId = getFirstOutputSourceId(matchNestedRule);

output = combine(astMutator, ruleStep, output);

assertComposedAstIsValid(
cel,
output.expr,
String.format(
"failed composing the subrule '%s' due to incompatible output types.",
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
lastOutputId);
lastOutputType =
assertComposedAstIsValid(
cel,
output.expr,
String.format(
"failed composing the subrule '%s' due to incompatible output types.",
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
currentSourceId,
lastOutputId)
.getResultType();
break;
}

lastOutputId = currentSourceId;
}

Preconditions.checkState(output != null, "Policy contains no outputs.");
CelMutableAst resultExpr = output.expr;
resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables());
resultExpr = astMutator.renumberIdsConsecutively(resultExpr);
Expand DownExpand Up@@ -266,21 +286,34 @@ private CelMutableAst inlineCompiledVariables(
return mutatedAst;
}

private void assertComposedAstIsValid(
private CelAbstractSyntaxTree assertComposedAstIsValid(
Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) {
assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
return assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
}

private void assertComposedAstIsValid(
private CelAbstractSyntaxTree assertComposedAstIsValid(
Cel cel, CelMutableAst composedAst, String failureMessage, List<Long> ids) {
try {
cel.check(composedAst.toParsedAst()).getAst();
return cel.check(composedAst.toParsedAst()).getAst();
} catch (CelValidationException e) {
ids = ids.stream().filter(id -> id > 0).collect(toCollection(ArrayList::new));
throw new RuleCompositionException(failureMessage, e, ids);
}
}

private static long getFirstOutputSourceId(CelCompiledRule rule) {
for (CelCompiledMatch match : rule.matches()) {
if (match.result().kind() == Result.Kind.OUTPUT) {
return match.result().output().sourceId();
} else if (match.result().kind() == Result.Kind.RULE) {
return getFirstOutputSourceId(match.result().rule());
}
}

// Fallback to the nested rule ID if the policy is invalid and contains no output
return rule.sourceId();
}

// Step represents an intermediate stage of rule and match expression composition.
//
// The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of
Expand DownExpand Up@@ -311,11 +344,6 @@ private Step(
this.expr = expr;
}

private static Step newOptionalStep(
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
return new Step(/* isOptional= */ true, isConditional, cond, expr);
}

private static Step newNonOptionalStep(
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
return new Step(/* isOptional= */ false, isConditional, cond, expr);
Expand Down
2 changes: 2 additions & 0 deletions policy/src/test/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,9 +27,11 @@ java_library(
"//parser:parser_factory",
"//parser:unparser",
"//policy",
"//policy:compiled_rule",
"//policy:compiler_factory",
"//policy:parser",
"//policy:parser_factory",
"//policy:rule_composer",
"//policy:source",
"//policy:validation_exception",
"//policy/testing:k8s_test_tag_handler",
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,7 @@
import dev.cel.bundle.CelEnvironmentYamlParser;
import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.CelOptions;
import dev.cel.common.formats.ValueString;
import dev.cel.common.types.OptionalType;
import dev.cel.common.types.SimpleType;
import dev.cel.expr.conformance.proto3.TestAllTypes;
Expand DownExpand Up@@ -356,6 +357,24 @@ public void evaluateYamlPolicy_withSimpleVariable() throws Exception {
assertThat(evalResult).isFalse();
}

@Test
public void compose_ruleWithNoOutputs_throws() throws Exception {
Cel cel = newCel();
CelCompiledRule emptyRule =
CelCompiledRule.create(
1L,
Optional.of(ValueString.of(2L, "empty_rule")),
ImmutableList.of(),
ImmutableList.of(),
cel);
RuleComposer composer = RuleComposer.newInstance(emptyRule, "variables.", 1000);
CelAbstractSyntaxTree ast = cel.compile("true").getAst();

IllegalStateException e =
assertThrows(IllegalStateException.class, () -> composer.optimize(ast, cel));
assertThat(e).hasMessageThat().isEqualTo("Policy contains no outputs.");
}

private static final class EvaluablePolicyTestData {
private final TestYamlPolicy yamlPolicy;
private final PolicyTestCase testCase;
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types found.
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool
| output: "false"
| .............^
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types found.
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool
| - output: "{'banned': true}"
| .............^
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
ERROR: compose_errors_conflicting_subrule/policy.yaml:34:18: failed composing the subrule 'banned regions' due to incompatible output types.
| output: "true"
| .................^
ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to incompatible output types.
| output: "{'banned': false}"
| .............^
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
6 changes: 6 additions & 0 deletions policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,3 +59,9 @@ java_library(
name = "compiler_builder",
exports = ["//policy/src/main/java/dev/cel/policy:compiler_builder"],
)

java_library(
name = "rule_composer",
visibility = ["//:internal"],
exports = ["//policy/src/main/java/dev/cel/policy:rule_composer"],
)
3 changes: 2 additions & 1 deletion policy/src/main/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -244,7 +244,6 @@ java_library(
java_library(
name = "rule_composer",
srcs = ["RuleComposer.java"],
visibility = ["//visibility:private"],
deps = [
":compiled_rule",
"//bundle:cel",
Expand All@@ -257,6 +256,8 @@ java_library(
"//common/ast:mutable_expr",
"//common/formats:value_string",
"//common/navigation:mutable_navigation",
"//common/types:cel_types",
"//common/types:type_providers",
"//extensions:optional_library",
"//optimizer:ast_optimizer",
"//optimizer:mutable_ast",
Expand Down
88 changes: 58 additions & 30 deletions policy/src/main/java/dev/cel/policy/RuleComposer.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.stream.Collectors.toCollection;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import dev.cel.bundle.Cel;
Expand All@@ -32,11 +33,14 @@
import dev.cel.common.formats.ValueString;
import dev.cel.common.navigation.CelNavigableMutableAst;
import dev.cel.common.navigation.CelNavigableMutableExpr;
import dev.cel.common.types.CelType;
import dev.cel.common.types.CelTypes;
import dev.cel.extensions.CelOptionalLibrary.Function;
import dev.cel.optimizer.AstMutator;
import dev.cel.optimizer.CelAstOptimizer;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.OutputValue;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
import java.util.ArrayList;
import java.util.Arrays;
Expand DownExpand Up@@ -74,54 +78,70 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) {
}

long lastOutputId = 0;
// The expected output type of the rule, used to verify that all branches agree on the type.
CelType lastOutputType = null;
for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) {
CelAbstractSyntaxTree conditionAst = match.condition();
boolean isTriviallyTrue = match.isConditionTriviallyTrue();
CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst);

long currentSourceId = lastOutputId;

switch (match.result().kind()) {
case OUTPUT:
// If the match has an output, then it is considered a non-optional output since
// it is explicitly stated. If the rule itself is optional, then the base case value
// of output being optional.none() will convert the non-optional value to an optional
// one.
OutputValue matchOutput = match.result().output();
CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast());
Step step = Step.newNonOptionalStep(!isTriviallyTrue, condAst, outAst);
Step step =
Step.newNonOptionalStep(
!isTriviallyTrue, condAst, CelMutableAst.fromCelAst(matchOutput.ast()));
currentSourceId = matchOutput.sourceId();

output = combine(astMutator, step, output);

assertComposedAstIsValid(
cel,
output.expr,
"incompatible output types found.",
matchOutput.sourceId(),
lastOutputId);
lastOutputId = matchOutput.sourceId();
String outputFailureMessage =
String.format(
"incompatible output types: block has output type %s, but previous outputs have"
+ " type %s",
lastOutputType == null ? "" : CelTypes.format(lastOutputType),
CelTypes.format(matchOutput.ast().getResultType()));
lastOutputType =
assertComposedAstIsValid(
cel, output.expr, outputFailureMessage, currentSourceId, lastOutputId)
.getResultType();

break;
case RULE:
// If the match has a nested rule, then compute the rule and whether it has
// an optional return value.
CelCompiledRule matchNestedRule = match.result().rule();
Step nestedRule = optimizeRule(cel, matchNestedRule);
boolean nestedHasOptional = matchNestedRule.hasOptionalOutput();

Step ruleStep =
nestedHasOptional
? Step.newOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr)
: Step.newNonOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr);
new Step(
matchNestedRule.hasOptionalOutput(), !isTriviallyTrue, condAst, nestedRule.expr);
currentSourceId = getFirstOutputSourceId(matchNestedRule);

output = combine(astMutator, ruleStep, output);

assertComposedAstIsValid(
cel,
output.expr,
String.format(
"failed composing the subrule '%s' due to incompatible output types.",
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
lastOutputId);
lastOutputType =
assertComposedAstIsValid(
cel,
output.expr,
String.format(
"failed composing the subrule '%s' due to incompatible output types.",
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
currentSourceId,
lastOutputId)
.getResultType();
break;
}

lastOutputId = currentSourceId;
}

Preconditions.checkState(output != null, "Policy contains no outputs.");
CelMutableAst resultExpr = output.expr;
resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables());
resultExpr = astMutator.renumberIdsConsecutively(resultExpr);
Expand DownExpand Up@@ -266,21 +286,34 @@ private CelMutableAst inlineCompiledVariables(
return mutatedAst;
}

private void assertComposedAstIsValid(
private CelAbstractSyntaxTree assertComposedAstIsValid(
Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) {
assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
return assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
}

private void assertComposedAstIsValid(
private CelAbstractSyntaxTree assertComposedAstIsValid(
Cel cel, CelMutableAst composedAst, String failureMessage, List<Long> ids) {
try {
cel.check(composedAst.toParsedAst()).getAst();
return cel.check(composedAst.toParsedAst()).getAst();
} catch (CelValidationException e) {
ids = ids.stream().filter(id -> id > 0).collect(toCollection(ArrayList::new));
throw new RuleCompositionException(failureMessage, e, ids);
}
}

private static long getFirstOutputSourceId(CelCompiledRule rule) {
for (CelCompiledMatch match : rule.matches()) {
if (match.result().kind() == Result.Kind.OUTPUT) {
return match.result().output().sourceId();
} else if (match.result().kind() == Result.Kind.RULE) {
return getFirstOutputSourceId(match.result().rule());
}
}

// Fallback to the nested rule ID if the policy is invalid and contains no output
return rule.sourceId();
}

// Step represents an intermediate stage of rule and match expression composition.
//
// The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of
Expand DownExpand Up@@ -311,11 +344,6 @@ private Step(
this.expr = expr;
}

private static Step newOptionalStep(
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
return new Step(/* isOptional= */ true, isConditional, cond, expr);
}

private static Step newNonOptionalStep(
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
return new Step(/* isOptional= */ false, isConditional, cond, expr);
Expand Down
2 changes: 2 additions & 0 deletions policy/src/test/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,9 +27,11 @@ java_library(
"//parser:parser_factory",
"//parser:unparser",
"//policy",
"//policy:compiled_rule",
"//policy:compiler_factory",
"//policy:parser",
"//policy:parser_factory",
"//policy:rule_composer",
"//policy:source",
"//policy:validation_exception",
"//policy/testing:k8s_test_tag_handler",
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,7 @@
import dev.cel.bundle.CelEnvironmentYamlParser;
import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.CelOptions;
import dev.cel.common.formats.ValueString;
import dev.cel.common.types.OptionalType;
import dev.cel.common.types.SimpleType;
import dev.cel.expr.conformance.proto3.TestAllTypes;
Expand DownExpand Up@@ -356,6 +357,24 @@ public void evaluateYamlPolicy_withSimpleVariable() throws Exception {
assertThat(evalResult).isFalse();
}

@Test
public void compose_ruleWithNoOutputs_throws() throws Exception {
Cel cel = newCel();
CelCompiledRule emptyRule =
CelCompiledRule.create(
1L,
Optional.of(ValueString.of(2L, "empty_rule")),
ImmutableList.of(),
ImmutableList.of(),
cel);
RuleComposer composer = RuleComposer.newInstance(emptyRule, "variables.", 1000);
CelAbstractSyntaxTree ast = cel.compile("true").getAst();

IllegalStateException e =
assertThrows(IllegalStateException.class, () -> composer.optimize(ast, cel));
assertThat(e).hasMessageThat().isEqualTo("Policy contains no outputs.");
}

private static final class EvaluablePolicyTestData {
private final TestYamlPolicy yamlPolicy;
private final PolicyTestCase testCase;
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types found.
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool
| output: "false"
| .............^
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types found.
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool
| - output: "{'banned': true}"
| .............^
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
ERROR: compose_errors_conflicting_subrule/policy.yaml:34:18: failed composing the subrule 'banned regions' due to incompatible output types.
| output: "true"
| .................^
ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to incompatible output types.
| output: "{'banned': false}"
| .............^
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
6 changes: 6 additions & 0 deletions policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,3 +59,9 @@ java_library(
name = "compiler_builder",
exports = ["//policy/src/main/java/dev/cel/policy:compiler_builder"],
)

java_library(
name = "rule_composer",
visibility = ["//:internal"],
exports = ["//policy/src/main/java/dev/cel/policy:rule_composer"],
)
3 changes: 2 additions & 1 deletion policy/src/main/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -244,7 +244,6 @@ java_library(
java_library(
name = "rule_composer",
srcs = ["RuleComposer.java"],
visibility = ["//visibility:private"],
deps = [
":compiled_rule",
"//bundle:cel",
Expand All@@ -257,6 +256,8 @@ java_library(
"//common/ast:mutable_expr",
"//common/formats:value_string",
"//common/navigation:mutable_navigation",
"//common/types:cel_types",
"//common/types:type_providers",
"//extensions:optional_library",
"//optimizer:ast_optimizer",
"//optimizer:mutable_ast",
Expand Down
88 changes: 58 additions & 30 deletions policy/src/main/java/dev/cel/policy/RuleComposer.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.stream.Collectors.toCollection;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import dev.cel.bundle.Cel;
Expand All@@ -32,11 +33,14 @@
import dev.cel.common.formats.ValueString;
import dev.cel.common.navigation.CelNavigableMutableAst;
import dev.cel.common.navigation.CelNavigableMutableExpr;
import dev.cel.common.types.CelType;
import dev.cel.common.types.CelTypes;
import dev.cel.extensions.CelOptionalLibrary.Function;
import dev.cel.optimizer.AstMutator;
import dev.cel.optimizer.CelAstOptimizer;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.OutputValue;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
import java.util.ArrayList;
import java.util.Arrays;
Expand DownExpand Up@@ -74,54 +78,70 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) {
}

long lastOutputId = 0;
// The expected output type of the rule, used to verify that all branches agree on the type.
CelType lastOutputType = null;
for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) {
CelAbstractSyntaxTree conditionAst = match.condition();
boolean isTriviallyTrue = match.isConditionTriviallyTrue();
CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst);

long currentSourceId = lastOutputId;

switch (match.result().kind()) {
case OUTPUT:
// If the match has an output, then it is considered a non-optional output since
// it is explicitly stated. If the rule itself is optional, then the base case value
// of output being optional.none() will convert the non-optional value to an optional
// one.
OutputValue matchOutput = match.result().output();
CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast());
Step step = Step.newNonOptionalStep(!isTriviallyTrue, condAst, outAst);
Step step =
Step.newNonOptionalStep(
!isTriviallyTrue, condAst, CelMutableAst.fromCelAst(matchOutput.ast()));
currentSourceId = matchOutput.sourceId();

output = combine(astMutator, step, output);

assertComposedAstIsValid(
cel,
output.expr,
"incompatible output types found.",
matchOutput.sourceId(),
lastOutputId);
lastOutputId = matchOutput.sourceId();
String outputFailureMessage =
String.format(
"incompatible output types: block has output type %s, but previous outputs have"
+ " type %s",
lastOutputType == null ? "" : CelTypes.format(lastOutputType),
CelTypes.format(matchOutput.ast().getResultType()));
lastOutputType =
assertComposedAstIsValid(
cel, output.expr, outputFailureMessage, currentSourceId, lastOutputId)
.getResultType();

break;
case RULE:
// If the match has a nested rule, then compute the rule and whether it has
// an optional return value.
CelCompiledRule matchNestedRule = match.result().rule();
Step nestedRule = optimizeRule(cel, matchNestedRule);
boolean nestedHasOptional = matchNestedRule.hasOptionalOutput();

Step ruleStep =
nestedHasOptional
? Step.newOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr)
: Step.newNonOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr);
new Step(
matchNestedRule.hasOptionalOutput(), !isTriviallyTrue, condAst, nestedRule.expr);
currentSourceId = getFirstOutputSourceId(matchNestedRule);

output = combine(astMutator, ruleStep, output);

assertComposedAstIsValid(
cel,
output.expr,
String.format(
"failed composing the subrule '%s' due to incompatible output types.",
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
lastOutputId);
lastOutputType =
assertComposedAstIsValid(
cel,
output.expr,
String.format(
"failed composing the subrule '%s' due to incompatible output types.",
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
currentSourceId,
lastOutputId)
.getResultType();
break;
}

lastOutputId = currentSourceId;
}

Preconditions.checkState(output != null, "Policy contains no outputs.");
CelMutableAst resultExpr = output.expr;
resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables());
resultExpr = astMutator.renumberIdsConsecutively(resultExpr);
Expand DownExpand Up@@ -266,21 +286,34 @@ private CelMutableAst inlineCompiledVariables(
return mutatedAst;
}

private void assertComposedAstIsValid(
private CelAbstractSyntaxTree assertComposedAstIsValid(
Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) {
assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
return assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
}

private void assertComposedAstIsValid(
private CelAbstractSyntaxTree assertComposedAstIsValid(
Cel cel, CelMutableAst composedAst, String failureMessage, List<Long> ids) {
try {
cel.check(composedAst.toParsedAst()).getAst();
return cel.check(composedAst.toParsedAst()).getAst();
} catch (CelValidationException e) {
ids = ids.stream().filter(id -> id > 0).collect(toCollection(ArrayList::new));
throw new RuleCompositionException(failureMessage, e, ids);
}
}

private static long getFirstOutputSourceId(CelCompiledRule rule) {
for (CelCompiledMatch match : rule.matches()) {
if (match.result().kind() == Result.Kind.OUTPUT) {
return match.result().output().sourceId();
} else if (match.result().kind() == Result.Kind.RULE) {
return getFirstOutputSourceId(match.result().rule());
}
}

// Fallback to the nested rule ID if the policy is invalid and contains no output
return rule.sourceId();
}

// Step represents an intermediate stage of rule and match expression composition.
//
// The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of
Expand DownExpand Up@@ -311,11 +344,6 @@ private Step(
this.expr = expr;
}

private static Step newOptionalStep(
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
return new Step(/* isOptional= */ true, isConditional, cond, expr);
}

private static Step newNonOptionalStep(
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
return new Step(/* isOptional= */ false, isConditional, cond, expr);
Expand Down
2 changes: 2 additions & 0 deletions policy/src/test/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,9 +27,11 @@ java_library(
"//parser:parser_factory",
"//parser:unparser",
"//policy",
"//policy:compiled_rule",
"//policy:compiler_factory",
"//policy:parser",
"//policy:parser_factory",
"//policy:rule_composer",
"//policy:source",
"//policy:validation_exception",
"//policy/testing:k8s_test_tag_handler",
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,7 @@
import dev.cel.bundle.CelEnvironmentYamlParser;
import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.CelOptions;
import dev.cel.common.formats.ValueString;
import dev.cel.common.types.OptionalType;
import dev.cel.common.types.SimpleType;
import dev.cel.expr.conformance.proto3.TestAllTypes;
Expand DownExpand Up@@ -356,6 +357,24 @@ public void evaluateYamlPolicy_withSimpleVariable() throws Exception {
assertThat(evalResult).isFalse();
}

@Test
public void compose_ruleWithNoOutputs_throws() throws Exception {
Cel cel = newCel();
CelCompiledRule emptyRule =
CelCompiledRule.create(
1L,
Optional.of(ValueString.of(2L, "empty_rule")),
ImmutableList.of(),
ImmutableList.of(),
cel);
RuleComposer composer = RuleComposer.newInstance(emptyRule, "variables.", 1000);
CelAbstractSyntaxTree ast = cel.compile("true").getAst();

IllegalStateException e =
assertThrows(IllegalStateException.class, () -> composer.optimize(ast, cel));
assertThat(e).hasMessageThat().isEqualTo("Policy contains no outputs.");
}

private static final class EvaluablePolicyTestData {
private final TestYamlPolicy yamlPolicy;
private final PolicyTestCase testCase;
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types found.
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool
| output: "false"
| .............^
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types found.
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool
| - output: "{'banned': true}"
| .............^
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
ERROR: compose_errors_conflicting_subrule/policy.yaml:34:18: failed composing the subrule 'banned regions' due to incompatible output types.
| output: "true"
| .................^
ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to incompatible output types.
| output: "{'banned': false}"
| .............^
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
6 changes: 6 additions & 0 deletions policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,3 +59,9 @@ java_library(
name = "compiler_builder",
exports = ["//policy/src/main/java/dev/cel/policy:compiler_builder"],
)

java_library(
name = "rule_composer",
visibility = ["//:internal"],
exports = ["//policy/src/main/java/dev/cel/policy:rule_composer"],
)
3 changes: 2 additions & 1 deletion policy/src/main/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -244,7 +244,6 @@ java_library(
java_library(
name = "rule_composer",
srcs = ["RuleComposer.java"],
visibility = ["//visibility:private"],
deps = [
":compiled_rule",
"//bundle:cel",
Expand All@@ -257,6 +256,8 @@ java_library(
"//common/ast:mutable_expr",
"//common/formats:value_string",
"//common/navigation:mutable_navigation",
"//common/types:cel_types",
"//common/types:type_providers",
"//extensions:optional_library",
"//optimizer:ast_optimizer",
"//optimizer:mutable_ast",
Expand Down
88 changes: 58 additions & 30 deletions policy/src/main/java/dev/cel/policy/RuleComposer.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.stream.Collectors.toCollection;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import dev.cel.bundle.Cel;
Expand All@@ -32,11 +33,14 @@
import dev.cel.common.formats.ValueString;
import dev.cel.common.navigation.CelNavigableMutableAst;
import dev.cel.common.navigation.CelNavigableMutableExpr;
import dev.cel.common.types.CelType;
import dev.cel.common.types.CelTypes;
import dev.cel.extensions.CelOptionalLibrary.Function;
import dev.cel.optimizer.AstMutator;
import dev.cel.optimizer.CelAstOptimizer;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.OutputValue;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
import java.util.ArrayList;
import java.util.Arrays;
Expand DownExpand Up@@ -74,54 +78,70 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) {
}

long lastOutputId = 0;
// The expected output type of the rule, used to verify that all branches agree on the type.
CelType lastOutputType = null;
for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) {
CelAbstractSyntaxTree conditionAst = match.condition();
boolean isTriviallyTrue = match.isConditionTriviallyTrue();
CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst);

long currentSourceId = lastOutputId;

switch (match.result().kind()) {
case OUTPUT:
// If the match has an output, then it is considered a non-optional output since
// it is explicitly stated. If the rule itself is optional, then the base case value
// of output being optional.none() will convert the non-optional value to an optional
// one.
OutputValue matchOutput = match.result().output();
CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast());
Step step = Step.newNonOptionalStep(!isTriviallyTrue, condAst, outAst);
Step step =
Step.newNonOptionalStep(
!isTriviallyTrue, condAst, CelMutableAst.fromCelAst(matchOutput.ast()));
currentSourceId = matchOutput.sourceId();

output = combine(astMutator, step, output);

assertComposedAstIsValid(
cel,
output.expr,
"incompatible output types found.",
matchOutput.sourceId(),
lastOutputId);
lastOutputId = matchOutput.sourceId();
String outputFailureMessage =
String.format(
"incompatible output types: block has output type %s, but previous outputs have"
+ " type %s",
lastOutputType == null ? "" : CelTypes.format(lastOutputType),
CelTypes.format(matchOutput.ast().getResultType()));
lastOutputType =
assertComposedAstIsValid(
cel, output.expr, outputFailureMessage, currentSourceId, lastOutputId)
.getResultType();

break;
case RULE:
// If the match has a nested rule, then compute the rule and whether it has
// an optional return value.
CelCompiledRule matchNestedRule = match.result().rule();
Step nestedRule = optimizeRule(cel, matchNestedRule);
boolean nestedHasOptional = matchNestedRule.hasOptionalOutput();

Step ruleStep =
nestedHasOptional
? Step.newOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr)
: Step.newNonOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr);
new Step(
matchNestedRule.hasOptionalOutput(), !isTriviallyTrue, condAst, nestedRule.expr);
currentSourceId = getFirstOutputSourceId(matchNestedRule);

output = combine(astMutator, ruleStep, output);

assertComposedAstIsValid(
cel,
output.expr,
String.format(
"failed composing the subrule '%s' due to incompatible output types.",
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
lastOutputId);
lastOutputType =
assertComposedAstIsValid(
cel,
output.expr,
String.format(
"failed composing the subrule '%s' due to incompatible output types.",
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
currentSourceId,
lastOutputId)
.getResultType();
break;
}

lastOutputId = currentSourceId;
}

Preconditions.checkState(output != null, "Policy contains no outputs.");
CelMutableAst resultExpr = output.expr;
resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables());
resultExpr = astMutator.renumberIdsConsecutively(resultExpr);
Expand DownExpand Up@@ -266,21 +286,34 @@ private CelMutableAst inlineCompiledVariables(
return mutatedAst;
}

private void assertComposedAstIsValid(
private CelAbstractSyntaxTree assertComposedAstIsValid(
Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) {
assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
return assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
}

private void assertComposedAstIsValid(
private CelAbstractSyntaxTree assertComposedAstIsValid(
Cel cel, CelMutableAst composedAst, String failureMessage, List<Long> ids) {
try {
cel.check(composedAst.toParsedAst()).getAst();
return cel.check(composedAst.toParsedAst()).getAst();
} catch (CelValidationException e) {
ids = ids.stream().filter(id -> id > 0).collect(toCollection(ArrayList::new));
throw new RuleCompositionException(failureMessage, e, ids);
}
}

private static long getFirstOutputSourceId(CelCompiledRule rule) {
for (CelCompiledMatch match : rule.matches()) {
if (match.result().kind() == Result.Kind.OUTPUT) {
return match.result().output().sourceId();
} else if (match.result().kind() == Result.Kind.RULE) {
return getFirstOutputSourceId(match.result().rule());
}
}

// Fallback to the nested rule ID if the policy is invalid and contains no output
return rule.sourceId();
}

// Step represents an intermediate stage of rule and match expression composition.
//
// The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of
Expand DownExpand Up@@ -311,11 +344,6 @@ private Step(
this.expr = expr;
}

private static Step newOptionalStep(
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
return new Step(/* isOptional= */ true, isConditional, cond, expr);
}

private static Step newNonOptionalStep(
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
return new Step(/* isOptional= */ false, isConditional, cond, expr);
Expand Down
2 changes: 2 additions & 0 deletions policy/src/test/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,9 +27,11 @@ java_library(
"//parser:parser_factory",
"//parser:unparser",
"//policy",
"//policy:compiled_rule",
"//policy:compiler_factory",
"//policy:parser",
"//policy:parser_factory",
"//policy:rule_composer",
"//policy:source",
"//policy:validation_exception",
"//policy/testing:k8s_test_tag_handler",
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,7 @@
import dev.cel.bundle.CelEnvironmentYamlParser;
import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.CelOptions;
import dev.cel.common.formats.ValueString;
import dev.cel.common.types.OptionalType;
import dev.cel.common.types.SimpleType;
import dev.cel.expr.conformance.proto3.TestAllTypes;
Expand DownExpand Up@@ -356,6 +357,24 @@ public void evaluateYamlPolicy_withSimpleVariable() throws Exception {
assertThat(evalResult).isFalse();
}

@Test
public void compose_ruleWithNoOutputs_throws() throws Exception {
Cel cel = newCel();
CelCompiledRule emptyRule =
CelCompiledRule.create(
1L,
Optional.of(ValueString.of(2L, "empty_rule")),
ImmutableList.of(),
ImmutableList.of(),
cel);
RuleComposer composer = RuleComposer.newInstance(emptyRule, "variables.", 1000);
CelAbstractSyntaxTree ast = cel.compile("true").getAst();

IllegalStateException e =
assertThrows(IllegalStateException.class, () -> composer.optimize(ast, cel));
assertThat(e).hasMessageThat().isEqualTo("Policy contains no outputs.");
}

private static final class EvaluablePolicyTestData {
private final TestYamlPolicy yamlPolicy;
private final PolicyTestCase testCase;
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types found.
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool
| output: "false"
| .............^
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types found.
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool
| - output: "{'banned': true}"
| .............^
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
ERROR: compose_errors_conflicting_subrule/policy.yaml:34:18: failed composing the subrule 'banned regions' due to incompatible output types.
| output: "true"
| .................^
ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to incompatible output types.
| output: "{'banned': false}"
| .............^
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
6 changes: 6 additions & 0 deletions policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,3 +59,9 @@ java_library(
name = "compiler_builder",
exports = ["//policy/src/main/java/dev/cel/policy:compiler_builder"],
)

java_library(
name = "rule_composer",
visibility = ["//:internal"],
exports = ["//policy/src/main/java/dev/cel/policy:rule_composer"],
)
3 changes: 2 additions & 1 deletion policy/src/main/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -244,7 +244,6 @@ java_library(
java_library(
name = "rule_composer",
srcs = ["RuleComposer.java"],
visibility = ["//visibility:private"],
deps = [
":compiled_rule",
"//bundle:cel",
Expand All@@ -257,6 +256,8 @@ java_library(
"//common/ast:mutable_expr",
"//common/formats:value_string",
"//common/navigation:mutable_navigation",
"//common/types:cel_types",
"//common/types:type_providers",
"//extensions:optional_library",
"//optimizer:ast_optimizer",
"//optimizer:mutable_ast",
Expand Down
88 changes: 58 additions & 30 deletions policy/src/main/java/dev/cel/policy/RuleComposer.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.stream.Collectors.toCollection;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import dev.cel.bundle.Cel;
Expand All@@ -32,11 +33,14 @@
import dev.cel.common.formats.ValueString;
import dev.cel.common.navigation.CelNavigableMutableAst;
import dev.cel.common.navigation.CelNavigableMutableExpr;
import dev.cel.common.types.CelType;
import dev.cel.common.types.CelTypes;
import dev.cel.extensions.CelOptionalLibrary.Function;
import dev.cel.optimizer.AstMutator;
import dev.cel.optimizer.CelAstOptimizer;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.OutputValue;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
import java.util.ArrayList;
import java.util.Arrays;
Expand DownExpand Up@@ -74,54 +78,70 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) {
}

long lastOutputId = 0;
// The expected output type of the rule, used to verify that all branches agree on the type.
CelType lastOutputType = null;
for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) {
CelAbstractSyntaxTree conditionAst = match.condition();
boolean isTriviallyTrue = match.isConditionTriviallyTrue();
CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst);

long currentSourceId = lastOutputId;

switch (match.result().kind()) {
case OUTPUT:
// If the match has an output, then it is considered a non-optional output since
// it is explicitly stated. If the rule itself is optional, then the base case value
// of output being optional.none() will convert the non-optional value to an optional
// one.
OutputValue matchOutput = match.result().output();
CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast());
Step step = Step.newNonOptionalStep(!isTriviallyTrue, condAst, outAst);
Step step =
Step.newNonOptionalStep(
!isTriviallyTrue, condAst, CelMutableAst.fromCelAst(matchOutput.ast()));
currentSourceId = matchOutput.sourceId();

output = combine(astMutator, step, output);

assertComposedAstIsValid(
cel,
output.expr,
"incompatible output types found.",
matchOutput.sourceId(),
lastOutputId);
lastOutputId = matchOutput.sourceId();
String outputFailureMessage =
String.format(
"incompatible output types: block has output type %s, but previous outputs have"
+ " type %s",
lastOutputType == null ? "" : CelTypes.format(lastOutputType),
CelTypes.format(matchOutput.ast().getResultType()));
lastOutputType =
assertComposedAstIsValid(
cel, output.expr, outputFailureMessage, currentSourceId, lastOutputId)
.getResultType();

break;
case RULE:
// If the match has a nested rule, then compute the rule and whether it has
// an optional return value.
CelCompiledRule matchNestedRule = match.result().rule();
Step nestedRule = optimizeRule(cel, matchNestedRule);
boolean nestedHasOptional = matchNestedRule.hasOptionalOutput();

Step ruleStep =
nestedHasOptional
? Step.newOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr)
: Step.newNonOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr);
new Step(
matchNestedRule.hasOptionalOutput(), !isTriviallyTrue, condAst, nestedRule.expr);
currentSourceId = getFirstOutputSourceId(matchNestedRule);

output = combine(astMutator, ruleStep, output);

assertComposedAstIsValid(
cel,
output.expr,
String.format(
"failed composing the subrule '%s' due to incompatible output types.",
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
lastOutputId);
lastOutputType =
assertComposedAstIsValid(
cel,
output.expr,
String.format(
"failed composing the subrule '%s' due to incompatible output types.",
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
currentSourceId,
lastOutputId)
.getResultType();
break;
}

lastOutputId = currentSourceId;
}

Preconditions.checkState(output != null, "Policy contains no outputs.");
CelMutableAst resultExpr = output.expr;
resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables());
resultExpr = astMutator.renumberIdsConsecutively(resultExpr);
Expand DownExpand Up@@ -266,21 +286,34 @@ private CelMutableAst inlineCompiledVariables(
return mutatedAst;
}

private void assertComposedAstIsValid(
private CelAbstractSyntaxTree assertComposedAstIsValid(
Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) {
assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
return assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
}

private void assertComposedAstIsValid(
private CelAbstractSyntaxTree assertComposedAstIsValid(
Cel cel, CelMutableAst composedAst, String failureMessage, List<Long> ids) {
try {
cel.check(composedAst.toParsedAst()).getAst();
return cel.check(composedAst.toParsedAst()).getAst();
} catch (CelValidationException e) {
ids = ids.stream().filter(id -> id > 0).collect(toCollection(ArrayList::new));
throw new RuleCompositionException(failureMessage, e, ids);
}
}

private static long getFirstOutputSourceId(CelCompiledRule rule) {
for (CelCompiledMatch match : rule.matches()) {
if (match.result().kind() == Result.Kind.OUTPUT) {
return match.result().output().sourceId();
} else if (match.result().kind() == Result.Kind.RULE) {
return getFirstOutputSourceId(match.result().rule());
}
}

// Fallback to the nested rule ID if the policy is invalid and contains no output
return rule.sourceId();
}

// Step represents an intermediate stage of rule and match expression composition.
//
// The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of
Expand DownExpand Up@@ -311,11 +344,6 @@ private Step(
this.expr = expr;
}

private static Step newOptionalStep(
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
return new Step(/* isOptional= */ true, isConditional, cond, expr);
}

private static Step newNonOptionalStep(
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
return new Step(/* isOptional= */ false, isConditional, cond, expr);
Expand Down
2 changes: 2 additions & 0 deletions policy/src/test/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,9 +27,11 @@ java_library(
"//parser:parser_factory",
"//parser:unparser",
"//policy",
"//policy:compiled_rule",
"//policy:compiler_factory",
"//policy:parser",
"//policy:parser_factory",
"//policy:rule_composer",
"//policy:source",
"//policy:validation_exception",
"//policy/testing:k8s_test_tag_handler",
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,7 @@
import dev.cel.bundle.CelEnvironmentYamlParser;
import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.CelOptions;
import dev.cel.common.formats.ValueString;
import dev.cel.common.types.OptionalType;
import dev.cel.common.types.SimpleType;
import dev.cel.expr.conformance.proto3.TestAllTypes;
Expand DownExpand Up@@ -356,6 +357,24 @@ public void evaluateYamlPolicy_withSimpleVariable() throws Exception {
assertThat(evalResult).isFalse();
}

@Test
public void compose_ruleWithNoOutputs_throws() throws Exception {
Cel cel = newCel();
CelCompiledRule emptyRule =
CelCompiledRule.create(
1L,
Optional.of(ValueString.of(2L, "empty_rule")),
ImmutableList.of(),
ImmutableList.of(),
cel);
RuleComposer composer = RuleComposer.newInstance(emptyRule, "variables.", 1000);
CelAbstractSyntaxTree ast = cel.compile("true").getAst();

IllegalStateException e =
assertThrows(IllegalStateException.class, () -> composer.optimize(ast, cel));
assertThat(e).hasMessageThat().isEqualTo("Policy contains no outputs.");
}

private static final class EvaluablePolicyTestData {
private final TestYamlPolicy yamlPolicy;
private final PolicyTestCase testCase;
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types found.
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool
| output: "false"
| .............^
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types found.
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool
| - output: "{'banned': true}"
| .............^
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
ERROR: compose_errors_conflicting_subrule/policy.yaml:34:18: failed composing the subrule 'banned regions' due to incompatible output types.
| output: "true"
| .................^
ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to incompatible output types.
| output: "{'banned': false}"
| .............^
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
6 changes: 6 additions & 0 deletions policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,3 +59,9 @@ java_library(
name = "compiler_builder",
exports = ["//policy/src/main/java/dev/cel/policy:compiler_builder"],
)

java_library(
name = "rule_composer",
visibility = ["//:internal"],
exports = ["//policy/src/main/java/dev/cel/policy:rule_composer"],
)
3 changes: 2 additions & 1 deletion policy/src/main/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -244,7 +244,6 @@ java_library(
java_library(
name = "rule_composer",
srcs = ["RuleComposer.java"],
visibility = ["//visibility:private"],
deps = [
":compiled_rule",
"//bundle:cel",
Expand All@@ -257,6 +256,8 @@ java_library(
"//common/ast:mutable_expr",
"//common/formats:value_string",
"//common/navigation:mutable_navigation",
"//common/types:cel_types",
"//common/types:type_providers",
"//extensions:optional_library",
"//optimizer:ast_optimizer",
"//optimizer:mutable_ast",
Expand Down
88 changes: 58 additions & 30 deletions policy/src/main/java/dev/cel/policy/RuleComposer.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.stream.Collectors.toCollection;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import dev.cel.bundle.Cel;
Expand All@@ -32,11 +33,14 @@
import dev.cel.common.formats.ValueString;
import dev.cel.common.navigation.CelNavigableMutableAst;
import dev.cel.common.navigation.CelNavigableMutableExpr;
import dev.cel.common.types.CelType;
import dev.cel.common.types.CelTypes;
import dev.cel.extensions.CelOptionalLibrary.Function;
import dev.cel.optimizer.AstMutator;
import dev.cel.optimizer.CelAstOptimizer;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.OutputValue;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
import java.util.ArrayList;
import java.util.Arrays;
Expand DownExpand Up@@ -74,54 +78,70 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) {
}

long lastOutputId = 0;
// The expected output type of the rule, used to verify that all branches agree on the type.
CelType lastOutputType = null;
for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) {
CelAbstractSyntaxTree conditionAst = match.condition();
boolean isTriviallyTrue = match.isConditionTriviallyTrue();
CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst);

long currentSourceId = lastOutputId;

switch (match.result().kind()) {
case OUTPUT:
// If the match has an output, then it is considered a non-optional output since
// it is explicitly stated. If the rule itself is optional, then the base case value
// of output being optional.none() will convert the non-optional value to an optional
// one.
OutputValue matchOutput = match.result().output();
CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast());
Step step = Step.newNonOptionalStep(!isTriviallyTrue, condAst, outAst);
Step step =
Step.newNonOptionalStep(
!isTriviallyTrue, condAst, CelMutableAst.fromCelAst(matchOutput.ast()));
currentSourceId = matchOutput.sourceId();

output = combine(astMutator, step, output);

assertComposedAstIsValid(
cel,
output.expr,
"incompatible output types found.",
matchOutput.sourceId(),
lastOutputId);
lastOutputId = matchOutput.sourceId();
String outputFailureMessage =
String.format(
"incompatible output types: block has output type %s, but previous outputs have"
+ " type %s",
lastOutputType == null ? "" : CelTypes.format(lastOutputType),
CelTypes.format(matchOutput.ast().getResultType()));
lastOutputType =
assertComposedAstIsValid(
cel, output.expr, outputFailureMessage, currentSourceId, lastOutputId)
.getResultType();

break;
case RULE:
// If the match has a nested rule, then compute the rule and whether it has
// an optional return value.
CelCompiledRule matchNestedRule = match.result().rule();
Step nestedRule = optimizeRule(cel, matchNestedRule);
boolean nestedHasOptional = matchNestedRule.hasOptionalOutput();

Step ruleStep =
nestedHasOptional
? Step.newOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr)
: Step.newNonOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr);
new Step(
matchNestedRule.hasOptionalOutput(), !isTriviallyTrue, condAst, nestedRule.expr);
currentSourceId = getFirstOutputSourceId(matchNestedRule);

output = combine(astMutator, ruleStep, output);

assertComposedAstIsValid(
cel,
output.expr,
String.format(
"failed composing the subrule '%s' due to incompatible output types.",
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
lastOutputId);
lastOutputType =
assertComposedAstIsValid(
cel,
output.expr,
String.format(
"failed composing the subrule '%s' due to incompatible output types.",
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
currentSourceId,
lastOutputId)
.getResultType();
break;
}

lastOutputId = currentSourceId;
}

Preconditions.checkState(output != null, "Policy contains no outputs.");
CelMutableAst resultExpr = output.expr;
resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables());
resultExpr = astMutator.renumberIdsConsecutively(resultExpr);
Expand DownExpand Up@@ -266,21 +286,34 @@ private CelMutableAst inlineCompiledVariables(
return mutatedAst;
}

private void assertComposedAstIsValid(
private CelAbstractSyntaxTree assertComposedAstIsValid(
Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) {
assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
return assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
}

private void assertComposedAstIsValid(
private CelAbstractSyntaxTree assertComposedAstIsValid(
Cel cel, CelMutableAst composedAst, String failureMessage, List<Long> ids) {
try {
cel.check(composedAst.toParsedAst()).getAst();
return cel.check(composedAst.toParsedAst()).getAst();
} catch (CelValidationException e) {
ids = ids.stream().filter(id -> id > 0).collect(toCollection(ArrayList::new));
throw new RuleCompositionException(failureMessage, e, ids);
}
}

private static long getFirstOutputSourceId(CelCompiledRule rule) {
for (CelCompiledMatch match : rule.matches()) {
if (match.result().kind() == Result.Kind.OUTPUT) {
return match.result().output().sourceId();
} else if (match.result().kind() == Result.Kind.RULE) {
return getFirstOutputSourceId(match.result().rule());
}
}

// Fallback to the nested rule ID if the policy is invalid and contains no output
return rule.sourceId();
}

// Step represents an intermediate stage of rule and match expression composition.
//
// The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of
Expand DownExpand Up@@ -311,11 +344,6 @@ private Step(
this.expr = expr;
}

private static Step newOptionalStep(
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
return new Step(/* isOptional= */ true, isConditional, cond, expr);
}

private static Step newNonOptionalStep(
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
return new Step(/* isOptional= */ false, isConditional, cond, expr);
Expand Down
2 changes: 2 additions & 0 deletions policy/src/test/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,9 +27,11 @@ java_library(
"//parser:parser_factory",
"//parser:unparser",
"//policy",
"//policy:compiled_rule",
"//policy:compiler_factory",
"//policy:parser",
"//policy:parser_factory",
"//policy:rule_composer",
"//policy:source",
"//policy:validation_exception",
"//policy/testing:k8s_test_tag_handler",
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,7 @@
import dev.cel.bundle.CelEnvironmentYamlParser;
import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.CelOptions;
import dev.cel.common.formats.ValueString;
import dev.cel.common.types.OptionalType;
import dev.cel.common.types.SimpleType;
import dev.cel.expr.conformance.proto3.TestAllTypes;
Expand DownExpand Up@@ -356,6 +357,24 @@ public void evaluateYamlPolicy_withSimpleVariable() throws Exception {
assertThat(evalResult).isFalse();
}

@Test
public void compose_ruleWithNoOutputs_throws() throws Exception {
Cel cel = newCel();
CelCompiledRule emptyRule =
CelCompiledRule.create(
1L,
Optional.of(ValueString.of(2L, "empty_rule")),
ImmutableList.of(),
ImmutableList.of(),
cel);
RuleComposer composer = RuleComposer.newInstance(emptyRule, "variables.", 1000);
CelAbstractSyntaxTree ast = cel.compile("true").getAst();

IllegalStateException e =
assertThrows(IllegalStateException.class, () -> composer.optimize(ast, cel));
assertThat(e).hasMessageThat().isEqualTo("Policy contains no outputs.");
}

private static final class EvaluablePolicyTestData {
private final TestYamlPolicy yamlPolicy;
private final PolicyTestCase testCase;
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types found.
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool
| output: "false"
| .............^
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types found.
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool
| - output: "{'banned': true}"
| .............^
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
ERROR: compose_errors_conflicting_subrule/policy.yaml:34:18: failed composing the subrule 'banned regions' due to incompatible output types.
| output: "true"
| .................^
ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to incompatible output types.
| output: "{'banned': false}"
| .............^
Loading