diff --git a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java index 2cfe62ec3..21dec65cd 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java @@ -2119,17 +2119,11 @@ public void eval_withJsonFieldName_runtimeOptionDisabled_throws() throws Excepti CelAbstractSyntaxTree ast = celCompiler.compile("file.int64CamelCaseJsonName").getAst(); CelEvaluationException e = - assertThrows( - CelEvaluationException.class, - () -> - celRuntime - .createProgram(ast) - .eval(ImmutableMap.of("file", SingleFile.getDefaultInstance()))); + assertThrows(CelEvaluationException.class, () -> celRuntime.createProgram(ast)); + assertThat(e) .hasMessageThat() - .contains( - "field 'int64CamelCaseJsonName' is not declared in message" - + " 'dev.cel.testing.testdata.SingleFile"); + .contains("json_name extension requires CelOptions.enableJsonFieldNames(true)"); } @Test diff --git a/common/src/main/java/dev/cel/common/CelSource.java b/common/src/main/java/dev/cel/common/CelSource.java index 4ea2b7a1e..cff3bcb3a 100644 --- a/common/src/main/java/dev/cel/common/CelSource.java +++ b/common/src/main/java/dev/cel/common/CelSource.java @@ -367,19 +367,19 @@ public CelSource build() { public abstract static class Extension { /** Identifier for the extension. Example: constant_folding */ - abstract String id(); + public abstract String id(); /** * Version info. May be skipped if it isn't meaningful for the extension. (for example * constant_folding might always be v0.0). */ - abstract Version version(); + public abstract Version version(); /** * If set, the listed components must understand the extension for the expression to evaluate * correctly. */ - abstract ImmutableList affectedComponents(); + public abstract ImmutableList affectedComponents(); /** Version of the extension */ @AutoValue @@ -390,18 +390,20 @@ public abstract static class Version { * Major version changes indicate different required support level from the required * components. */ - abstract long major(); + public abstract long major(); /** * Minor version changes must not change the observed behavior from existing implementations, * but may be provided informational. */ - abstract long minor(); + public abstract long minor(); /** Create a new instance of Version with the provided major and minor values. */ public static Version of(long major, long minor) { return new AutoValue_CelSource_Extension_Version(major, minor); } + + Version() {} } /** CEL component specifier. */ @@ -427,5 +429,7 @@ public static Extension create(String id, Version version, Iterable c public static Extension create(String id, Version version, Component... components) { return create(id, version, Arrays.asList(components)); } + + Extension() {} } } diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index 67dcac623..7c971ef6b 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -17,6 +17,8 @@ java_library( deps = [ "//:java_truth", "//checker:checker_builder", + "//common:cel_ast", + "//common:cel_source", "//common:compiler_common", "//common:container", "//common:options", diff --git a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java index f1d234101..0e7f12f26 100644 --- a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java +++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java @@ -31,9 +31,13 @@ import com.google.protobuf.TypeRegistry; import dev.cel.checker.CelChecker; import dev.cel.checker.CelCheckerBuilder; +import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelIssue; import dev.cel.common.CelOptions; +import dev.cel.common.CelSource.Extension; +import dev.cel.common.CelSource.Extension.Component; +import dev.cel.common.CelSource.Extension.Version; import dev.cel.common.CelValidationResult; import dev.cel.common.CelVarDecl; import dev.cel.common.ast.CelBlock; @@ -227,7 +231,19 @@ public void evaluate() throws Throwable { response = getChecker(test).check(response.getAst()); } assertThat(response.getErrors()).isEmpty(); - Type resultType = CelProtoTypes.celTypeToType(response.getAst().getResultType()); + CelAbstractSyntaxTree ast = response.getAst(); + if (ast.getExpr().callOrDefault().function().equals(CelBlock.FUNCTION_NAME)) { + ast = + CelAbstractSyntaxTree.newCheckedAst( + ast.getExpr(), + ast.getSource().toBuilder() + .addAllExtensions( + Extension.create("cel_block", Version.of(1, 1), Component.COMPONENT_RUNTIME)) + .build(), + ast.getReferenceMap(), + ast.getTypeMap()); + } + Type resultType = CelProtoTypes.celTypeToType(ast.getResultType()); if (test.getCheckOnly()) { assertThat(test.hasTypedResult()).isTrue(); @@ -244,8 +260,8 @@ public void evaluate() throws Throwable { ExprValue result = null; CelEvaluationException error = null; try { - Program program = runtime.createProgram(response.getAst()); - result = toExprValue(program.eval(getBindings(test)), response.getAst().getResultType()); + Program program = runtime.createProgram(ast); + result = toExprValue(program.eval(getBindings(test)), ast.getResultType()); } catch (CelEvaluationException e) { error = e; } diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java index a5668a2c9..c3da2c292 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java @@ -827,10 +827,17 @@ private CelAbstractSyntaxTree compileUsingInternalFunctions(String expression, b indexExpr.ident().setName(internalIdentName); }); + CelAbstractSyntaxTree parsedAst = + CelAbstractSyntaxTree.newParsedAst( + mutableAst.toParsedAst().getExpr(), + astToModify.getSource().toBuilder() + .addAllExtensions( + Extension.create("cel_block", Version.of(1L, 1L), Component.COMPONENT_RUNTIME)) + .build()); if (parsedOnly) { - return mutableAst.toParsedAst(); + return parsedAst; } - return celForEvaluatingBlock.check(mutableAst.toParsedAst()).getAst(); + return celForEvaluatingBlock.check(parsedAst).getAst(); } private CelAbstractSyntaxTree compileUsingInternalFunctions(String expression) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index c93fffaa5..3e7e4d21e 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -46,6 +46,7 @@ java_library( ":planner_helpers", "//:auto_value", "//common:cel_ast", + "//common:cel_source", "//common:container", "//common:operator", "//common:options", @@ -555,16 +556,13 @@ java_library( ":planned_interpretable", ":planner_helpers", "//common/ast", - "//common/types", "//common/types:cel_types", - "//common/types:type_providers", "//common/values", "//common/values:cel_byte_string", "//common/values:optimized_select_traversal", "//common/values:select_field", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", - "@maven//:org_jspecify_jspecify", ], ) @@ -720,6 +718,7 @@ cel_android_library( ":planner_helpers_android", "//:auto_value", "//common:cel_ast_android", + "//common:cel_source_android", "//common:container_android", "//common:operator_android", "//common:options", @@ -1232,14 +1231,11 @@ cel_android_library( ":planner_helpers_android", "//common/ast:ast_android", "//common/types:cel_types_android", - "//common/types:type_providers_android", - "//common/types:types_android", "//common/values:cel_byte_string", "//common/values:optimized_select_traversal_android", "//common/values:select_field_android", "//common/values:values_android", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:org_jspecify_jspecify", "@maven_android//:com_google_guava_guava", ], ) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index c34b2924b..5851ee423 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -14,6 +14,7 @@ package dev.cel.runtime.planner; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.auto.value.AutoValue; @@ -28,6 +29,9 @@ import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelOptions; +import dev.cel.common.CelSource.Extension; +import dev.cel.common.CelSource.Extension.Component; +import dev.cel.common.CelSource.Extension.Version; import dev.cel.common.Operator; import dev.cel.common.annotations.Internal; import dev.cel.common.ast.CelBlock; @@ -60,6 +64,7 @@ import dev.cel.runtime.RuntimeEquality; import java.util.Arrays; import java.util.HashMap; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; import org.jspecify.annotations.Nullable; @@ -72,6 +77,15 @@ @Internal public final class ProgramPlanner { private static final String MAP_INSERT_FUNCTION = "cel.@mapInsert"; + private static final String CEL_BLOCK_EXTENSION = "cel_block"; + private static final String JSON_NAME_EXTENSION = "json_name"; + private static final String SELECT_OPTIMIZATION_EXTENSION = "select_optimization"; + + private static final ImmutableMap SUPPORTED_RUNTIME_EXTENSIONS = + ImmutableMap.of( + CEL_BLOCK_EXTENSION, Version.of(1, 1), + JSON_NAME_EXTENSION, Version.of(1, 1), + SELECT_OPTIMIZATION_EXTENSION, Version.of(1, 0)); private final CelTypeProvider typeProvider; private final CelValueProvider valueProvider; @@ -102,7 +116,7 @@ public PlannedProgram plan(CelAbstractSyntaxTree ast) throws CelEvaluationExcept ErrorMetadata errorMetadata = ErrorMetadata.create(ast.getSource().getPositionsMap(), ast.getSource().getDescription()); try { - PlannerContext ctx = PlannerContext.create(ast); + PlannerContext ctx = PlannerContext.create(ast, options); plannedInterpretable = CelBlock.extract(ast) .map(celBlock -> planBlock(celBlock, ctx)) @@ -248,6 +262,11 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { // map key lookup on Map, and SelectableValue fallback). if (functionName.equals(OptimizedSelectPlanner.CEL_ATTRIBUTE_FUNCTION_NAME) || functionName.equals(OptimizedSelectPlanner.CEL_HAS_FIELD_FUNCTION_NAME)) { + checkArgument( + ctx.runtimeExtensions.contains(SELECT_OPTIMIZATION_EXTENSION), + "%s requires the %s AST extension", + functionName, + SELECT_OPTIMIZATION_EXTENSION); return optimizedSelectPlanner.plan(expr, functionName, operandExpr -> plan(operandExpr, ctx)); } @@ -382,6 +401,11 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { } private PlannedInterpretable planBlock(CelBlock celBlock, PlannerContext ctx) { + checkArgument( + ctx.runtimeExtensions.contains(CEL_BLOCK_EXTENSION), + "%s requires the %s AST extension", + CelBlock.FUNCTION_NAME, + CEL_BLOCK_EXTENSION); ImmutableList indices = celBlock.indices(); PlannedInterpretable[] slotExprs = new PlannedInterpretable[indices.size()]; @@ -716,23 +740,20 @@ private static Builder newBuilder() { } } - static final class PlannerContext { + private static final class PlannerContext { private final CelAbstractSyntaxTree ast; - private final HashMap localVars = new HashMap<>(); - - CelAbstractSyntaxTree ast() { - return ast; - } + private final ImmutableSet runtimeExtensions; + private final Map localVars; - ImmutableMap referenceMap() { + private ImmutableMap referenceMap() { return ast.getReferenceMap(); } - ImmutableMap typeMap() { + private ImmutableMap typeMap() { return ast.getTypeMap(); } - boolean isChecked() { + private boolean isChecked() { return ast.isChecked(); } @@ -766,12 +787,35 @@ private boolean isLocalVar(String name) { return localVars.containsKey(name); } - static PlannerContext create(CelAbstractSyntaxTree ast) { - return new PlannerContext(ast); + private static PlannerContext create(CelAbstractSyntaxTree ast, CelOptions options) { + ImmutableSet.Builder runtimeExtensions = ImmutableSet.builder(); + for (Extension extension : ast.getSource().getExtensions()) { + if (!extension.affectedComponents().contains(Component.COMPONENT_RUNTIME)) { + continue; + } + Version version = extension.version(); + Version supportedVersion = SUPPORTED_RUNTIME_EXTENSIONS.get(extension.id()); + checkArgument( + supportedVersion != null + && version.major() == supportedVersion.major() + && version.minor() <= supportedVersion.minor(), + "unsupported CEL extension: %s@%s.%s", + extension.id(), + version.major(), + version.minor()); + runtimeExtensions.add(extension.id()); + } + ImmutableSet extensions = runtimeExtensions.build(); + checkArgument( + !extensions.contains(JSON_NAME_EXTENSION) || options.enableJsonFieldNames(), + "json_name extension requires CelOptions.enableJsonFieldNames(true)"); + return new PlannerContext(ast, extensions); } - private PlannerContext(CelAbstractSyntaxTree ast) { + private PlannerContext(CelAbstractSyntaxTree ast, ImmutableSet runtimeExtensions) { this.ast = checkNotNull(ast); + this.runtimeExtensions = runtimeExtensions; + this.localVars = new HashMap<>(); } } diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index 0371947e5..1dbb053c0 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -40,6 +40,7 @@ java_library( "//common:container", "//common:error_codes", "//common:options", + "//common:proto_ast", "//common:proto_v1alpha1_ast", "//common/ast", "//common/ast:cel_block", @@ -111,6 +112,7 @@ java_library( "//testing/protos:single_file_java_proto", "//testing/protos:test_all_types_cel_java_proto2", "//testing/protos:test_all_types_cel_java_proto3", + "@cel_spec//proto/cel/expr:checked_java_proto", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", diff --git a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java index e9f9e0d67..e850a6817 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java @@ -18,7 +18,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertThrows; -import com.google.api.expr.v1alpha1.CheckedExpr; +import dev.cel.expr.CheckedExpr; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -40,7 +40,7 @@ import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; import dev.cel.common.CelOverloadDecl; -import dev.cel.common.CelProtoV1Alpha1AbstractSyntaxTree; +import dev.cel.common.CelProtoAbstractSyntaxTree; import dev.cel.common.ast.CelBlock; import dev.cel.common.internal.ProtoTimeUtils; import dev.cel.common.types.ListType; @@ -1879,11 +1879,11 @@ public void checkedExprWireRoundTrip_preservesOptimizedSelectsUnderVersionSkew() .getAst(); CelAbstractSyntaxTree optimizedAst = serverOptimizer.optimize(ast); byte[] serializedCheckedExpr = - CelProtoV1Alpha1AbstractSyntaxTree.fromCelAst(optimizedAst).toCheckedExpr().toByteArray(); + CelProtoAbstractSyntaxTree.fromCelAst(optimizedAst).toCheckedExpr().toByteArray(); CheckedExpr deserializedCheckedExpr = CheckedExpr.parseFrom(serializedCheckedExpr, ExtensionRegistryLite.getEmptyRegistry()); CelAbstractSyntaxTree deserializedAst = - CelProtoV1Alpha1AbstractSyntaxTree.fromCheckedExpr(deserializedCheckedExpr).getAst(); + CelProtoAbstractSyntaxTree.fromCheckedExpr(deserializedCheckedExpr).getAst(); Object result = clientRuntime diff --git a/runtime/src/test/java/dev/cel/runtime/planner/OptimizedSelectPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/OptimizedSelectPlannerTest.java index 9b8fb45d1..9cf457cfe 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/OptimizedSelectPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/OptimizedSelectPlannerTest.java @@ -32,6 +32,9 @@ import dev.cel.common.CelDescriptorUtil; import dev.cel.common.CelOptions; import dev.cel.common.CelSource; +import dev.cel.common.CelSource.Extension; +import dev.cel.common.CelSource.Extension.Component; +import dev.cel.common.CelSource.Extension.Version; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; import dev.cel.common.exceptions.CelAttributeNotFoundException; @@ -156,6 +159,13 @@ public final class OptimizedSelectPlannerTest { NestedTestAllTypes.getDescriptor().getFile())) .build(); + private static final CelSource SELECT_OPTIMIZED_SOURCE = + CelSource.newBuilder() + .addAllExtensions( + Extension.create( + "select_optimization", Version.of(1, 0), Component.COMPONENT_RUNTIME)) + .build(); + @Test public void plan_celAttribute_populatedField_returnsPopulatedValue() throws Exception { CelAbstractSyntaxTree ast = optimizeSelectAst("msg.single_int64"); @@ -444,7 +454,7 @@ public void plan_optionalSelect_onOptimizedPrefix_withPartialVarsUnknown_returns .setCall(selectCall.call().toBuilder().setArg(0, prefixAst.getExpr()).build()) .build(); CelAbstractSyntaxTree combinedAst = - CelAbstractSyntaxTree.newParsedAst(combinedExpr, wrapperAst.getSource()); + CelAbstractSyntaxTree.newParsedAst(combinedExpr, prefixAst.getSource()); Program program = PLANNER.plan(combinedAst); TestAllTypes msg = TestAllTypes.newBuilder() @@ -462,6 +472,20 @@ public void plan_optionalSelect_onOptimizedPrefix_withPartialVarsUnknown_returns .containsExactly(CelAttribute.fromQualifiedIdentifier("msg.single_nested_message.bb")); } + @Test + public void plan_missingSelectOptimizationExtension_throwsEvaluationException( + @TestParameter({"msg.single_int64", "has(msg.single_nested_message.bb)"}) String expression) + throws Exception { + CelAbstractSyntaxTree optimizedAst = optimizeSelectAst(expression); + CelAbstractSyntaxTree ast = + CelAbstractSyntaxTree.newParsedAst(optimizedAst.getExpr(), CelSource.newBuilder().build()); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); + + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e).hasMessageThat().contains("requires the select_optimization AST extension"); + } + @Test public void plan_invalidAst_wrongArgCount_throwsEvaluationException() { CelAbstractSyntaxTree ast = @@ -470,7 +494,7 @@ public void plan_invalidAst_wrongArgCount_throwsEvaluationException() { 1L, OptimizedSelectPlanner.CEL_ATTRIBUTE_FUNCTION_NAME, ImmutableList.of(CelExpr.ofIdent(2L, "msg"))), - CelSource.newBuilder().build()); + SELECT_OPTIMIZED_SOURCE); CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); @@ -496,7 +520,7 @@ public void plan_invalidAst_malformedHop_throwsEvaluationException() { ImmutableList.of())), ImmutableList.of()), CelExpr.ofConstant(6L, CelConstant.ofValue(0L)))), - CelSource.newBuilder().build()); + SELECT_OPTIMIZED_SOURCE); CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); @@ -528,7 +552,7 @@ public void plan_invalidAst_messageTypeCodeWithScalarDummy_throwsEvaluationExcep ImmutableList.of())), ImmutableList.of()), CelExpr.ofConstant(8L, CelConstant.ofValue(0L)))), - CelSource.newBuilder().build()); + SELECT_OPTIMIZED_SOURCE); CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); @@ -557,7 +581,7 @@ public void plan_invalidAst_unsupportedWellKnownType_throwsEvaluationException() ImmutableList.of())), ImmutableList.of()), CelExpr.ofStruct(8L, "google.protobuf.Any", ImmutableList.of()))), - CelSource.newBuilder().build()); + SELECT_OPTIMIZED_SOURCE); CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); @@ -592,7 +616,7 @@ public void plan_invalidAst_nonMapHopWithFourElements_throwsEvaluationException( 9L, ImmutableList.of(CelExpr.ofConstant(10L, CelConstant.ofValue(0L))), ImmutableList.of()))), - CelSource.newBuilder().build()); + SELECT_OPTIMIZED_SOURCE); CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); @@ -629,7 +653,7 @@ public void plan_invalidAst_mapHopWithMissingEntrySpec_throwsEvaluationException CelExpr.ofConstant(10L, CelConstant.ofValue("")), CelExpr.ofConstant(11L, CelConstant.ofValue("")), /* isOptionalEntry= */ false))))), - CelSource.newBuilder().build()); + SELECT_OPTIMIZED_SOURCE); CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); @@ -673,7 +697,7 @@ public void plan_invalidAst_mapEntrySpecMalformed_throwsEvaluationException() { CelExpr.ofConstant(14L, CelConstant.ofValue("")), CelExpr.ofConstant(15L, CelConstant.ofValue("")), /* isOptionalEntry= */ false))))), - CelSource.newBuilder().build()); + SELECT_OPTIMIZED_SOURCE); CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); @@ -707,7 +731,7 @@ public void plan_invalidAst_repeatedFieldWithEmptyListDummy_throwsEvaluationExce ImmutableList.of())), ImmutableList.of()), CelExpr.ofList(8L, ImmutableList.of(), ImmutableList.of()))), - CelSource.newBuilder().build()); + SELECT_OPTIMIZED_SOURCE); CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); @@ -790,7 +814,7 @@ public void plan_invalidAst_scalarDummyIncompatibleWithTypeCode_throwsEvaluation CelExpr.ofList(4L, testCase.hopElements, ImmutableList.of())), ImmutableList.of()), testCase.dummy)), - CelSource.newBuilder().build()); + SELECT_OPTIMIZED_SOURCE); CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); @@ -826,7 +850,7 @@ public void plan_invalidAst_mapFieldWithEmptyMapDummy_throwsEvaluationException( ImmutableList.of())), ImmutableList.of()), CelExpr.ofMap(11L, ImmutableList.of()))), - CelSource.newBuilder().build()); + SELECT_OPTIMIZED_SOURCE); CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); @@ -863,7 +887,7 @@ public void plan_invalidAst_mapFieldWithEmptyMapDummy_throwsEvaluationException( ImmutableList.of( CelExpr.ofStruct(9L, "google.protobuf.Any", ImmutableList.of())), ImmutableList.of()))), - CelSource.newBuilder().build()); + SELECT_OPTIMIZED_SOURCE); CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); @@ -898,7 +922,7 @@ public void plan_invalidAst_messageStructWithEmptyMessageName_throwsEvaluationEx 8L, ImmutableList.of(CelExpr.ofStruct(9L, "", ImmutableList.of())), ImmutableList.of()))), - CelSource.newBuilder().build()); + SELECT_OPTIMIZED_SOURCE); CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index ad44a9f51..9114e5676 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -39,6 +39,9 @@ import dev.cel.common.CelErrorCode; import dev.cel.common.CelOptions; import dev.cel.common.CelSource; +import dev.cel.common.CelSource.Extension; +import dev.cel.common.CelSource.Extension.Component; +import dev.cel.common.CelSource.Extension.Version; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; import dev.cel.common.ast.CelExpr.CelComprehension; @@ -349,6 +352,80 @@ public void plan_notSet_throws() { assertThat(e).hasMessageThat().contains("evaluation error: Unsupported kind: NOT_SET"); } + @Test + @TestParameters("{id: 'unknown', major: 1, minor: 0}") + @TestParameters("{id: 'select_optimization', major: 2, minor: 0}") + @TestParameters("{id: 'select_optimization', major: 1, minor: 1}") + @TestParameters("{id: 'cel_block', major: 0, minor: 0}") + @TestParameters("{id: 'cel_block', major: 1, minor: 2}") + @TestParameters("{id: 'json_name', major: 1, minor: 2}") + public void plan_unsupportedRuntimeExtension_throws(String id, int major, int minor) { + CelAbstractSyntaxTree ast = + newAstWithExtensions( + Extension.create(id, Version.of(major, minor), Component.COMPONENT_RUNTIME)); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); + + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e) + .hasMessageThat() + .contains(String.format("unsupported CEL extension: %s@%d.%d", id, major, minor)); + } + + @Test + public void plan_supportedOrNonRuntimeExtensions_succeeds() throws Exception { + CelAbstractSyntaxTree ast = + newAstWithExtensions( + Extension.create("cel_block", Version.of(1, 0), Component.COMPONENT_RUNTIME), + Extension.create("cel_block", Version.of(1, 1), Component.COMPONENT_RUNTIME), + Extension.create("json_name", Version.of(1, 1), Component.COMPONENT_RUNTIME), + Extension.create("select_optimization", Version.of(1, 0), Component.COMPONENT_RUNTIME), + // Extensions not affecting the runtime are ignored. + Extension.create("unknown", Version.of(2, 0), Component.COMPONENT_PARSER)); + ProgramPlanner planner = + newPlannerWithOptions(CelOptions.current().enableJsonFieldNames(true).build()); + + Object result = planner.plan(ast).eval(); + + assertThat(result).isEqualTo(true); + } + + @Test + public void plan_celBlock_missingExtension_throws() { + CelAbstractSyntaxTree ast = + CelAbstractSyntaxTree.newParsedAst( + CelExpr.ofCall( + 1L, + Optional.empty(), + "cel.@block", + ImmutableList.of( + CelExpr.ofList( + 2L, + ImmutableList.of(CelExpr.ofConstant(3L, CelConstant.ofValue(true))), + ImmutableList.of()), + CelExpr.ofIdent(4L, "@index0"))), + CelSource.newBuilder().build()); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); + + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e).hasMessageThat().contains("cel.@block requires the cel_block AST extension"); + } + + @Test + public void plan_jsonNameExtension_optionDisabled_throws() { + CelAbstractSyntaxTree ast = + newAstWithExtensions( + Extension.create("json_name", Version.of(1, 1), Component.COMPONENT_RUNTIME)); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); + + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e) + .hasMessageThat() + .contains("json_name extension requires CelOptions.enableJsonFieldNames(true)"); + } + @Test public void plan_constant(@TestParameter ConstantTestCase testCase) throws Exception { CelAbstractSyntaxTree ast = compile(testCase.expression); @@ -1895,6 +1972,12 @@ private CelAbstractSyntaxTree compile(CelCompiler compiler, String expression) t return compiler.check(ast).getAst(); } + private static CelAbstractSyntaxTree newAstWithExtensions(Extension... extensions) { + return CelAbstractSyntaxTree.newParsedAst( + CelExpr.ofConstant(1L, CelConstant.ofValue(true)), + CelSource.newBuilder().addAllExtensions(extensions).build()); + } + private static CelByteString concatenateByteArrays(CelByteString bytes1, CelByteString bytes2) { if (bytes1.isEmpty()) { return bytes2;