Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
deee22f
Align CEL-Spec conformance behavior
snazy Jul 17, 2026
18d03b1
Prune nulls from protobuf repeated and map fields
snazy Jul 17, 2026
02ba196
Remove stale map Any null-retention skips
snazy Jul 17, 2026
d71f24b
Return CEL errors for invalid protobuf field values
snazy Jul 17, 2026
26ede44
Add type deduction conformance coverage
snazy Jul 17, 2026
9eae487
Enable passing float range conformance cases
snazy Jul 17, 2026
7037784
Enable passing legacy enum range conformance cases
snazy Jul 17, 2026
a75f6dc
Check 32-bit protobuf wrapper ranges
snazy Jul 17, 2026
d3adc4c
Convert large uint values to JSON strings
snazy Jul 17, 2026
66dbcd3
Support protobuf well-known JSON values
snazy Jul 17, 2026
92a2131
Reject float keys in map literals
snazy Jul 17, 2026
1800ac5
Honor protobuf NaN equality semantics
snazy Jul 17, 2026
07aaee0
Preserve nested Any conformance values
snazy Jul 17, 2026
bca2f72
Reject non-string keys for protobuf Struct conversion
snazy Jul 17, 2026
14a0a87
Enable proto3 negative-zero float conformance case
snazy Jul 17, 2026
af652ac
Prefer wrapper types when unifying list elements
snazy Jul 17, 2026
f0b4f82
Preserve nullable types when unifying with null
snazy Jul 17, 2026
24c83ec
Improve checker type parameter inference
snazy Jul 17, 2026
308cd81
Preserve comprehension variable shadowing
snazy Jul 17, 2026
41252f6
Support proto2 extension field access
snazy Jul 17, 2026
bd48e61
Add proto extension helper library
snazy Jul 17, 2026
d36c9ac
Add supported string extension conformance
snazy Jul 17, 2026
25c412e
Complete string extension conformance
snazy Jul 17, 2026
34e0fad
Add optional type-check declarations
snazy Jul 17, 2026
6fd9df0
Narrow strong enum conformance skips
snazy Jul 17, 2026
089cc18
Re-enable deep message literal conformance
snazy Jul 17, 2026
d444c92
Add encoder extension conformance
snazy Jul 17, 2026
9c9b0ac
Add binding macro conformance
snazy Jul 17, 2026
62b74bd
Add math extension conformance
snazy Jul 17, 2026
b45ba93
Add network extension conformance
snazy Jul 17, 2026
53bfe27
Add block extension conformance
snazy Jul 17, 2026
3b43ae1
Add comprehension v2 conformance
snazy Jul 17, 2026
ddd4100
Note about CEL strong-enums
snazy Jul 17, 2026
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The CEL specification can be found [here](https://github.com/google/cel-spec).
- [Motivation](#motivation)
- [Arbitrary Java classes](#arbitrary-java-classes)
- [Unsigned 64-bit `uint`](#unsigned-64-bit-uint)
- [Protobuf enum semantics](#protobuf-enum-semantics)
- [Native image and package verification](#native-image-and-package-verification)
- [Not yet implemented](#not-yet-implemented)
- [Unclear double-to-int rounding behavior](#unclear-double-to-int-rounding-behavior)
Expand Down Expand Up @@ -394,6 +395,24 @@ but `123u == 123u` and `123 == 123` are.
If you have a `uint32` or `uint64` in protobuf objects, or use `uint`s in CEL expressions, wrap those
values with `org.projectnessie.cel.common.ULong`.

### Protobuf enum semantics

CEL-Java follows the CEL-Spec v0.25.2 language definition for protobuf enum values: protobuf enum
constants and enum fields are represented as CEL `int` values.

The upstream CEL-Spec conformance testdata also contains strong-enum cases where enum values
preserve their enum type. CEL-Java does not currently enable those strong-enum conformance cases.
They are not part of the v0.25.2 language-definition baseline and are mutually incompatible with the
legacy enum-as-int conformance sections in a single-mode runtime.

Use numeric enum values directly, or use `int(...)` when writing expressions that should remain clear
if strong enum support is added in the future:

```cel
TestAllTypes.NestedEnum.BAR == 1
int(TestAllTypes.NestedEnum.BAR) == 1
```

### Native image and package verification

Native-image and package behavior must be verified in the consuming application's exact build.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import static org.projectnessie.cel.EnvOption.clearMacros;
import static org.projectnessie.cel.EnvOption.container;
import static org.projectnessie.cel.EnvOption.declarations;
import static org.projectnessie.cel.EnvOption.macros;
import static org.projectnessie.cel.EnvOption.types;
import static org.projectnessie.cel.Library.StdLib;
import static org.projectnessie.cel.common.types.BoolT.True;
Expand All @@ -43,6 +44,12 @@
import static org.projectnessie.cel.common.types.UintT.uintOf;
import static org.projectnessie.cel.common.types.UnknownT.isUnknown;
import static org.projectnessie.cel.common.types.UnknownT.unknownOf;
import static org.projectnessie.cel.extension.EncodersLib.encoders;
import static org.projectnessie.cel.extension.MathLib.math;
import static org.projectnessie.cel.extension.NetworkLib.network;
import static org.projectnessie.cel.extension.OptionalLib.optionals;
import static org.projectnessie.cel.extension.ProtoLib.proto;
import static org.projectnessie.cel.extension.StringsLib.strings;

import com.google.api.expr.v1alpha1.CheckedExpr;
import com.google.api.expr.v1alpha1.Decl;
Expand Down Expand Up @@ -99,6 +106,7 @@
import org.projectnessie.cel.common.types.ref.Val;
import org.projectnessie.cel.common.types.traits.Lister;
import org.projectnessie.cel.common.types.traits.Mapper;
import org.projectnessie.cel.parser.Macro;

class SimpleConformanceTest {

Expand All @@ -109,75 +117,79 @@ class SimpleConformanceTest {
private static final List<String> TEST_FILES =
List.of(
"basic.textproto",
"bindings_ext.textproto",
"block_ext.textproto",
"comparisons.textproto",
"conversions.textproto",
"dynamic.textproto",
"encoders_ext.textproto",
"enums.textproto",
"fields.textproto",
"fp_math.textproto",
"integer_math.textproto",
"lists.textproto",
"logic.textproto",
"macros.textproto",
"macros2.textproto",
"math_ext.textproto",
"namespace.textproto",
"network_ext.textproto",
"parse.textproto",
"plumbing.textproto",
"proto2.textproto",
"proto2_ext.textproto",
"proto3.textproto",
"string.textproto",
"string_ext.textproto",
"timestamps.textproto",
"type_deduction.textproto",
"unknowns.textproto",
"wrappers.textproto");

private static final Set<String> SKIP_TESTS =
SkipList.parse(
// Without the checker, verifying whether an assignment is allowed by the CEL spec is
// difficult, especially from a map to a struct. The checker catches this case, while the
// evaluator currently converts int(1) to string("1").
"dynamic/struct/field_assign_proto2_bad",
"dynamic/struct/field_assign_proto3_bad",
// The test expects -0.0d, but in Java -0.0d == 0.0d, so -0.0d is evaluated as not set.
"dynamic/float/field_assign_proto3_round_to_zero",
// Malicious too-deep protobuf structure.
"parse/nest/message_literal",
// Proto equality specialties do not seem to be in effect for Java.
"comparisons/eq_wrapper/eq_proto_nan_equal",
"comparisons/ne_literal/ne_proto_nan_not_equal",
// TODO Actual known issue: protobuf Any returned by this test is wrapped twice.
"dynamic/any/var",
// New CEL-Spec v0.25.2 expectations that need follow-up parser/runtime changes.
"conversions/bool/string_1,string_t,string_0,string_f,string_true_badcase,string_false_badcase",
"fields/quoted_map_fields/field_access_slash,field_access_dash,field_access_dot,has_field_slash,has_field_dash,has_field_dot",
"namespace/namespace_shadowing/comprehension_shadowing,comprehension_shadowing_disambiguation,comprehension_shadowing_parse_only,comprehension_shadowing_selector,comprehension_shadowing_selector_parse_only,comprehension_shadowing_namespaced_selector,comprehension_shadowing_namespaced_selector_parse_only,comprehension_shadowing_nesting",
"proto2/set_null/single_message,single_duration,single_timestamp,repeated_field_timestamp_null_pruned,repeated_field_duration_null_pruned,repeated_field_wrapper_null_pruned,map_timestamp_null_pruned,map_duration_null_pruned,map_wrapper_null_pruned,map_anytype_null_retained,single_scalar,repeated,map,list_value,single_struct",
"proto2/quoted_fields/set_field_with_quoted_name,get_field_with_quoted_name",
"proto2/extensions_has/package_scoped_int32,package_scoped_nested_ext,package_scoped_test_all_types_ext,package_scoped_test_all_types_nested_enum_ext,package_scoped_repeated_test_all_types,message_scoped_int64,message_scoped_nested_ext,message_scoped_nested_enum_ext,message_scoped_repeated_test_all_types",
"proto2/extensions_get/package_scoped_int32,package_scoped_nested_ext,package_scoped_test_all_types_ext,package_scoped_test_all_types_nested_enum_ext,package_scoped_repeated_test_all_types,message_scoped_int64,message_scoped_nested_ext,message_scoped_nested_enum_ext,message_scoped_repeated_test_all_types",
"proto3/set_null/single_message,single_duration,single_timestamp,repeated_field_timestamp_null_pruned,repeated_field_duration_null_pruned,repeated_field_wrapper_null_pruned,map_timestamp_null_pruned,map_duration_null_pruned,map_wrapper_null_pruned,map_anytype_null_retained,single_scalar,repeated,map,list_value,single_struct",
"proto3/quoted_fields/set_field,get_field",
"timestamps/timestamp_conversions/type_comparison",
"timestamps/duration_conversions/type_comparison",
"wrappers/bool/to_null",
"wrappers/int32/to_null",
"wrappers/int64/to_null",
"wrappers/uint32/to_null",
"wrappers/uint64/to_null",
"wrappers/float/to_null",
"wrappers/double/to_null",
"wrappers/bytes/to_null",
"wrappers/string/to_null",
"dynamic/int32/field_assign_proto2_range,field_assign_proto3_range",
"dynamic/uint32/field_assign_proto2_range,field_assign_proto3_range",
"dynamic/float/field_assign_proto2_range,field_assign_proto3_range",
"enums/legacy_proto2/assign_standalone_int_too_big,assign_standalone_int_too_neg",
"enums/legacy_proto3/assign_standalone_int_too_big,assign_standalone_int_too_neg",
"enums/strong_proto2",
"enums/strong_proto3",
"fields/qualified_identifier_resolution/map_key_float",
"wrappers/uint64/to_json_string",
"wrappers/field_mask/to_json",
"wrappers/timestamp/to_json",
"wrappers/empty/to_json");
// Strong enum semantics require typed enum values rather than treating enum literals as
// ints.
"enums/strong_proto2/literal_global",
"enums/strong_proto2/literal_nested",
"enums/strong_proto2/literal_zero",
"enums/strong_proto2/type_global",
"enums/strong_proto2/type_nested",
"enums/strong_proto2/select_default",
"enums/strong_proto2/field_type",
"enums/strong_proto2/assign_standalone_int",
"enums/strong_proto2/convert_int_inrange",
"enums/strong_proto2/convert_int_big",
"enums/strong_proto2/convert_int_neg",
"enums/strong_proto2/convert_int_too_big",
"enums/strong_proto2/convert_int_too_neg",
"enums/strong_proto2/convert_string",
"enums/strong_proto2/convert_string_bad",
"enums/strong_proto3/literal_global",
"enums/strong_proto3/literal_nested",
"enums/strong_proto3/literal_zero",
"enums/strong_proto3/type_global",
"enums/strong_proto3/type_nested",
"enums/strong_proto3/select_default",
"enums/strong_proto3/select",
"enums/strong_proto3/select_big",
"enums/strong_proto3/select_neg",
"enums/strong_proto3/field_type",
"enums/strong_proto3/assign_standalone_int",
"enums/strong_proto3/assign_standalone_int_big",
"enums/strong_proto3/assign_standalone_int_neg",
"enums/strong_proto3/convert_int_inrange",
"enums/strong_proto3/convert_int_big",
"enums/strong_proto3/convert_int_neg",
"enums/strong_proto3/convert_int_too_big",
"enums/strong_proto3/convert_int_too_neg",
"enums/strong_proto3/convert_string",
"enums/strong_proto3/convert_string_bad",
// Optional list/map/message syntax and runtime support is not implemented yet.
"block_ext/basic/optional_list",
"block_ext/basic/optional_map",
"block_ext/basic/optional_map_chained",
"block_ext/basic/optional_message");

private static final Set<String> matchedSkips = new LinkedHashSet<>();
private static final AtomicInteger total = new AtomicInteger();
Expand Down Expand Up @@ -335,6 +347,9 @@ private static ParsedExpr parse(SimpleTest test) {
if (test.getDisableMacros()) {
parseOptions.add(clearMacros());
}
if (usesTestOnlyBlockMacros(test.getExpr())) {
parseOptions.add(macros(Macro.TestOnlyBlockMacros));
}

Env env = newEnv(parseOptions.toArray(new EnvOption[0]));
AstIssuesTuple astIss = env.parse(sourceText);
Expand All @@ -353,12 +368,8 @@ private static CheckedExpr check(SimpleTest test, ParsedExpr parsedExpr)

Env env =
newCustomEnv(
StdLib(),
container(test.getContainer()),
declarations(typeEnv),
types(
dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance(),
dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()));
conformanceEnvOptions(test, StdLib(), declarations(typeEnv))
.toArray(new EnvOption[0]));

AstIssuesTuple astIss = env.check(parsedExprToAst(parsedExpr));
if (astIss.hasIssues()) {
Expand All @@ -376,12 +387,7 @@ private static ExprValue evalChecked(SimpleTest test, CheckedExpr checkedExpr) {
}

private static ExprValue eval(SimpleTest test, Ast ast) {
Env env =
newEnv(
container(test.getContainer()),
types(
dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance(),
dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()));
Env env = newEnv(conformanceEnvOptions(test).toArray(new EnvOption[0]));

Program program = env.program(ast);
Map<String, Object> args = new HashMap<>();
Expand All @@ -400,6 +406,69 @@ private static ExprValue eval(SimpleTest test, Ast ast) {
.setError(ErrorSet.newBuilder().addErrors(Status.newBuilder().setMessage(err.toString())))
.build();
}

private static List<EnvOption> conformanceEnvOptions(SimpleTest test, EnvOption... options) {
List<EnvOption> envOptions = new ArrayList<>();
envOptions.add(container(test.getContainer()));
envOptions.add(
types(
dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance(),
dev.cel.expr.conformance.proto2.Proto2ExtensionScopedMessage.getDefaultInstance(),
dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()));
if (test.getExpr().startsWith("proto.hasExt(")
|| test.getExpr().startsWith("proto.getExt(")) {
envOptions.add(proto());
}
if (usesStringExtensions(test.getExpr())) {
envOptions.add(strings());
}
if (test.getExpr().contains("base64.")) {
envOptions.add(encoders());
}
if (test.getExpr().contains("math.")) {
envOptions.add(math());
}
if (usesNetworkExtensions(test.getExpr())) {
envOptions.add(network());
}
if (test.getExpr().contains("optional.")) {
envOptions.add(optionals());
}
envOptions.addAll(List.of(options));
return envOptions;
}

private static boolean usesStringExtensions(String expression) {
return expression.contains(".charAt(")
|| expression.contains(".indexOf(")
|| expression.contains(".lastIndexOf(")
|| expression.contains(".lowerAscii(")
|| expression.contains(".upperAscii(")
|| expression.contains(".replace(")
|| expression.contains(".split(")
|| expression.contains(".substring(")
|| expression.contains(".trim(")
|| expression.contains(".join(")
|| expression.contains("strings.quote(")
|| expression.contains(".format(")
|| expression.contains(".reverse(");
}

private static boolean usesNetworkExtensions(String expression) {
return expression.contains("ip(")
|| expression.contains("cidr(")
|| expression.contains("isIP(")
|| expression.contains("ip.isCanonical(")
|| expression.contains("net.IP")
|| expression.contains("net.CIDR");
}

private static boolean usesTestOnlyBlockMacros(String expression) {
return expression.contains("cel.block(")
|| expression.contains("cel.index(")
|| expression.contains("cel.iterVar(")
|| expression.contains("cel.accuVar(");
}
}

private static void match(String testPath, SimpleTest test, ExprValue actual)
Expand Down Expand Up @@ -574,6 +643,8 @@ private static Val valueToRefValue(TypeAdapter adapter, Value v) {
return type;
}
return newObjectTypeValue(typeName);
case ENUM_VALUE:
return intOf(v.getEnumValue().getValue());
default:
throw new IllegalArgumentException("unknown value " + v.getKindCase());
}
Expand Down Expand Up @@ -635,7 +706,9 @@ private static Value refValueToValue(Val res) {
case Object:
Message pb = (Message) res.value();
Value.Builder value = Value.newBuilder();
if (pb instanceof ListValue) {
if (pb instanceof Any) {
value.setObjectValue(unwrapNestedAny((Any) pb));
} else if (pb instanceof ListValue) {
value.setListValue((ListValue) pb);
} else if (pb instanceof MapValue) {
value.setMapValue((MapValue) pb);
Expand All @@ -648,6 +721,22 @@ private static Value refValueToValue(Val res) {
}
}

private static Any unwrapNestedAny(Any any) {
Any current = any;
while (current.is(Any.class)) {
try {
Any next = current.unpack(Any.class);
if (next.equals(current)) {
return current;
}
current = next;
} catch (InvalidProtocolBufferException e) {
return current;
}
}
return current;
}

private static <T extends Message> T convert(Message message, Class<T> targetType)
throws InvalidProtocolBufferException {
try {
Expand Down
Loading