Skip to content

Commit 63ea8df

Browse files
l46kokcopybara-github
authored andcommitted
Track incompatible output types for accurate error reporting
PiperOrigin-RevId: 911470209
1 parent 966b0cf commit 63ea8df

7 files changed

Lines changed: 87 additions & 33 deletions

File tree

policy/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,9 @@ java_library(
5959
name = "compiler_builder",
6060
exports = ["//policy/src/main/java/dev/cel/policy:compiler_builder"],
6161
)
62+
63+
java_library(
64+
name = "rule_composer",
65+
visibility = ["//:internal"],
66+
exports = ["//policy/src/main/java/dev/cel/policy:rule_composer"],
67+
)

policy/src/main/java/dev/cel/policy/BUILD.bazel

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,6 @@ java_library(
244244
java_library(
245245
name = "rule_composer",
246246
srcs = ["RuleComposer.java"],
247-
visibility = ["//visibility:private"],
248247
deps = [
249248
":compiled_rule",
250249
"//bundle:cel",
@@ -257,6 +256,8 @@ java_library(
257256
"//common/ast:mutable_expr",
258257
"//common/formats:value_string",
259258
"//common/navigation:mutable_navigation",
259+
"//common/types",
260+
"//common/types:type_providers",
260261
"//extensions:optional_library",
261262
"//optimizer:ast_optimizer",
262263
"//optimizer:mutable_ast",

policy/src/main/java/dev/cel/policy/RuleComposer.java

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static com.google.common.collect.ImmutableList.toImmutableList;
1919
import static java.util.stream.Collectors.toCollection;
2020

21+
import com.google.common.base.Preconditions;
2122
import com.google.common.collect.ImmutableList;
2223
import com.google.common.collect.Lists;
2324
import dev.cel.bundle.Cel;
@@ -32,11 +33,13 @@
3233
import dev.cel.common.formats.ValueString;
3334
import dev.cel.common.navigation.CelNavigableMutableAst;
3435
import dev.cel.common.navigation.CelNavigableMutableExpr;
36+
import dev.cel.common.types.CelType;
3537
import dev.cel.extensions.CelOptionalLibrary.Function;
3638
import dev.cel.optimizer.AstMutator;
3739
import dev.cel.optimizer.CelAstOptimizer;
3840
import dev.cel.policy.CelCompiledRule.CelCompiledMatch;
3941
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.OutputValue;
42+
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
4043
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
4144
import java.util.ArrayList;
4245
import java.util.Arrays;
@@ -74,54 +77,70 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) {
7477
}
7578

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

87+
long currentSourceId = lastOutputId;
88+
8289
switch (match.result().kind()) {
8390
case OUTPUT:
8491
// If the match has an output, then it is considered a non-optional output since
8592
// it is explicitly stated. If the rule itself is optional, then the base case value
8693
// of output being optional.none() will convert the non-optional value to an optional
8794
// one.
8895
OutputValue matchOutput = match.result().output();
89-
CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast());
90-
Step step = Step.newNonOptionalStep(!isTriviallyTrue, condAst, outAst);
96+
Step step =
97+
Step.newNonOptionalStep(
98+
!isTriviallyTrue, condAst, CelMutableAst.fromCelAst(matchOutput.ast()));
99+
currentSourceId = matchOutput.sourceId();
100+
91101
output = combine(astMutator, step, output);
92102

93-
assertComposedAstIsValid(
94-
cel,
95-
output.expr,
96-
"incompatible output types found.",
97-
matchOutput.sourceId(),
98-
lastOutputId);
99-
lastOutputId = matchOutput.sourceId();
103+
String outputFailureMessage =
104+
String.format(
105+
"incompatible output types: block has output type %s, but previous outputs have"
106+
+ " type %s",
107+
lastOutputType == null ? "" : lastOutputType.name(),
108+
matchOutput.ast().getResultType().name());
109+
lastOutputType =
110+
assertComposedAstIsValid(
111+
cel, output.expr, outputFailureMessage, currentSourceId, lastOutputId)
112+
.getResultType();
113+
100114
break;
101115
case RULE:
102116
// If the match has a nested rule, then compute the rule and whether it has
103117
// an optional return value.
104118
CelCompiledRule matchNestedRule = match.result().rule();
105119
Step nestedRule = optimizeRule(cel, matchNestedRule);
106-
boolean nestedHasOptional = matchNestedRule.hasOptionalOutput();
107-
108120
Step ruleStep =
109-
nestedHasOptional
110-
? Step.newOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr)
111-
: Step.newNonOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr);
121+
new Step(
122+
matchNestedRule.hasOptionalOutput(), !isTriviallyTrue, condAst, nestedRule.expr);
123+
currentSourceId = getFirstOutputSourceId(matchNestedRule);
124+
112125
output = combine(astMutator, ruleStep, output);
113126

114-
assertComposedAstIsValid(
115-
cel,
116-
output.expr,
117-
String.format(
118-
"failed composing the subrule '%s' due to incompatible output types.",
119-
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
120-
lastOutputId);
127+
lastOutputType =
128+
assertComposedAstIsValid(
129+
cel,
130+
output.expr,
131+
String.format(
132+
"failed composing the subrule '%s' due to incompatible output types.",
133+
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
134+
currentSourceId,
135+
lastOutputId)
136+
.getResultType();
121137
break;
122138
}
139+
140+
lastOutputId = currentSourceId;
123141
}
124142

143+
Preconditions.checkState(output != null, "Policy contains no outputs.");
125144
CelMutableAst resultExpr = output.expr;
126145
resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables());
127146
resultExpr = astMutator.renumberIdsConsecutively(resultExpr);
@@ -266,21 +285,30 @@ private CelMutableAst inlineCompiledVariables(
266285
return mutatedAst;
267286
}
268287

269-
private void assertComposedAstIsValid(
288+
private CelAbstractSyntaxTree assertComposedAstIsValid(
270289
Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) {
271-
assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
290+
return assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
272291
}
273292

274-
private void assertComposedAstIsValid(
293+
private CelAbstractSyntaxTree assertComposedAstIsValid(
275294
Cel cel, CelMutableAst composedAst, String failureMessage, List<Long> ids) {
276295
try {
277-
cel.check(composedAst.toParsedAst()).getAst();
296+
return cel.check(composedAst.toParsedAst()).getAst();
278297
} catch (CelValidationException e) {
279298
ids = ids.stream().filter(id -> id > 0).collect(toCollection(ArrayList::new));
280299
throw new RuleCompositionException(failureMessage, e, ids);
281300
}
282301
}
283302

303+
private static long getFirstOutputSourceId(CelCompiledRule rule) {
304+
for (CelCompiledMatch match : rule.matches()) {
305+
if (match.result().kind() == Result.Kind.OUTPUT) {
306+
return match.result().output().sourceId();
307+
}
308+
}
309+
return rule.sourceId();
310+
}
311+
284312
// Step represents an intermediate stage of rule and match expression composition.
285313
//
286314
// The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of
@@ -311,11 +339,6 @@ private Step(
311339
this.expr = expr;
312340
}
313341

314-
private static Step newOptionalStep(
315-
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
316-
return new Step(/* isOptional= */ true, isConditional, cond, expr);
317-
}
318-
319342
private static Step newNonOptionalStep(
320343
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
321344
return new Step(/* isOptional= */ false, isConditional, cond, expr);

policy/src/test/java/dev/cel/policy/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ java_library(
2727
"//parser:parser_factory",
2828
"//parser:unparser",
2929
"//policy",
30+
"//policy:compiled_rule",
3031
"//policy:compiler_factory",
3132
"//policy:parser",
3233
"//policy:parser_factory",
34+
"//policy:rule_composer",
3335
"//policy:source",
3436
"//policy:validation_exception",
3537
"//policy/testing:k8s_test_tag_handler",

policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import dev.cel.bundle.CelEnvironmentYamlParser;
3232
import dev.cel.common.CelAbstractSyntaxTree;
3333
import dev.cel.common.CelOptions;
34+
import dev.cel.common.formats.ValueString;
3435
import dev.cel.common.types.OptionalType;
3536
import dev.cel.common.types.SimpleType;
3637
import dev.cel.expr.conformance.proto3.TestAllTypes;
@@ -356,6 +357,24 @@ public void evaluateYamlPolicy_withSimpleVariable() throws Exception {
356357
assertThat(evalResult).isFalse();
357358
}
358359

360+
@Test
361+
public void compose_ruleWithNoOutputs_throws() {
362+
Cel cel = newCel();
363+
CelCompiledRule emptyRule =
364+
CelCompiledRule.create(
365+
1L,
366+
Optional.of(ValueString.of(2L, "empty_rule")),
367+
ImmutableList.of(),
368+
ImmutableList.of(),
369+
cel);
370+
RuleComposer composer = RuleComposer.newInstance(emptyRule, "variables.", 1000);
371+
CelAbstractSyntaxTree ast = cel.compile("true").getAst();
372+
373+
IllegalStateException e =
374+
assertThrows(IllegalStateException.class, () -> composer.optimize(ast, cel));
375+
assertThat(e).hasMessageThat().isEqualTo("Policy contains no outputs.");
376+
}
377+
359378
private static final class EvaluablePolicyTestData {
360379
private final TestYamlPolicy yamlPolicy;
361380
private final PolicyTestCase testCase;
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types found.
1+
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types: block has output type map, but previous outputs have type bool
22
| output: "false"
33
| .............^
4-
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types found.
4+
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types: block has output type map, but previous outputs have type bool
55
| - output: "{'banned': true}"
66
| .............^
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
ERROR: compose_errors_conflicting_subrule/policy.yaml:34:18: failed composing the subrule 'banned regions' due to incompatible output types.
2+
| output: "true"
3+
| .................^
14
ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to incompatible output types.
25
| output: "{'banned': false}"
36
| .............^

0 commit comments

Comments
 (0)