Skip to content

Commit aa212d1

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

7 files changed

Lines changed: 92 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:cel_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: 58 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,14 @@
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;
37+
import dev.cel.common.types.CelTypes;
3538
import dev.cel.extensions.CelOptionalLibrary.Function;
3639
import dev.cel.optimizer.AstMutator;
3740
import dev.cel.optimizer.CelAstOptimizer;
3841
import dev.cel.policy.CelCompiledRule.CelCompiledMatch;
3942
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.OutputValue;
43+
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
4044
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
4145
import java.util.ArrayList;
4246
import java.util.Arrays;
@@ -74,54 +78,70 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) {
7478
}
7579

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

88+
long currentSourceId = lastOutputId;
89+
8290
switch (match.result().kind()) {
8391
case OUTPUT:
8492
// If the match has an output, then it is considered a non-optional output since
8593
// it is explicitly stated. If the rule itself is optional, then the base case value
8694
// of output being optional.none() will convert the non-optional value to an optional
8795
// one.
8896
OutputValue matchOutput = match.result().output();
89-
CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast());
90-
Step step = Step.newNonOptionalStep(!isTriviallyTrue, condAst, outAst);
97+
Step step =
98+
Step.newNonOptionalStep(
99+
!isTriviallyTrue, condAst, CelMutableAst.fromCelAst(matchOutput.ast()));
100+
currentSourceId = matchOutput.sourceId();
101+
91102
output = combine(astMutator, step, output);
92103

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

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);
128+
lastOutputType =
129+
assertComposedAstIsValid(
130+
cel,
131+
output.expr,
132+
String.format(
133+
"failed composing the subrule '%s' due to incompatible output types.",
134+
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
135+
currentSourceId,
136+
lastOutputId)
137+
.getResultType();
121138
break;
122139
}
140+
141+
lastOutputId = currentSourceId;
123142
}
124143

144+
Preconditions.checkState(output != null, "Policy contains no outputs.");
125145
CelMutableAst resultExpr = output.expr;
126146
resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables());
127147
resultExpr = astMutator.renumberIdsConsecutively(resultExpr);
@@ -266,21 +286,34 @@ private CelMutableAst inlineCompiledVariables(
266286
return mutatedAst;
267287
}
268288

269-
private void assertComposedAstIsValid(
289+
private CelAbstractSyntaxTree assertComposedAstIsValid(
270290
Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) {
271-
assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
291+
return assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
272292
}
273293

274-
private void assertComposedAstIsValid(
294+
private CelAbstractSyntaxTree assertComposedAstIsValid(
275295
Cel cel, CelMutableAst composedAst, String failureMessage, List<Long> ids) {
276296
try {
277-
cel.check(composedAst.toParsedAst()).getAst();
297+
return cel.check(composedAst.toParsedAst()).getAst();
278298
} catch (CelValidationException e) {
279299
ids = ids.stream().filter(id -> id > 0).collect(toCollection(ArrayList::new));
280300
throw new RuleCompositionException(failureMessage, e, ids);
281301
}
282302
}
283303

304+
private static long getFirstOutputSourceId(CelCompiledRule rule) {
305+
for (CelCompiledMatch match : rule.matches()) {
306+
if (match.result().kind() == Result.Kind.OUTPUT) {
307+
return match.result().output().sourceId();
308+
} else if (match.result().kind() == Result.Kind.RULE) {
309+
return getFirstOutputSourceId(match.result().rule());
310+
}
311+
}
312+
313+
// Fallback to the nested rule ID if the policy is invalid and contains no output
314+
return rule.sourceId();
315+
}
316+
284317
// Step represents an intermediate stage of rule and match expression composition.
285318
//
286319
// The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of
@@ -311,11 +344,6 @@ private Step(
311344
this.expr = expr;
312345
}
313346

314-
private static Step newOptionalStep(
315-
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
316-
return new Step(/* isOptional= */ true, isConditional, cond, expr);
317-
}
318-
319347
private static Step newNonOptionalStep(
320348
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
321349
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() throws Exception {
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(string, bool), 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(string, bool), 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)