From 163565be123fddc047346d121007bad6f54616e3 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Sat, 28 Feb 2026 00:33:23 -0800 Subject: [PATCH] Null assignability fix for repeated and map fields PiperOrigin-RevId: 876590274 --- .../dev/cel/common/internal/ProtoAdapter.java | 31 ++++++++++++++++--- .../test/resources/nullAssignability.baseline | 29 +++++++++++++++++ .../dev/cel/testing/BaseInterpreterTest.java | 27 ++++++++++++++++ 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java b/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java index b6648a5b8..c37c2a2a7 100644 --- a/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java +++ b/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java @@ -222,9 +222,7 @@ public Optional adaptValueToFieldType( throw new IllegalArgumentException("Unsupported field type"); } - String typeFullName = fieldDescriptor.getMessageType().getFullName(); - if (!WellKnownProto.ANY_VALUE.typeName().equals(typeFullName) - && !WellKnownProto.JSON_VALUE.typeName().equals(typeFullName)) { + if (!isAnyOrJsonValue(fieldDescriptor)) { return Optional.empty(); } } @@ -242,7 +240,11 @@ public Optional adaptValueToFieldType( getDefaultValueForMaybeMessage(keyDescriptor), valueDescriptor.getLiteType(), getDefaultValueForMaybeMessage(valueDescriptor)); + boolean isValueAnyOrJson = isAnyOrJsonValue(valueDescriptor); for (Map.Entry entry : ((Map) fieldValue).entrySet()) { + if (!isValueAnyOrJson && entry.getValue() instanceof NullValue) { + continue; + } mapEntries.add( protoMapEntry.toBuilder() .setKey(keyConverter.backwardConverter().convert(entry.getKey())) @@ -252,15 +254,36 @@ public Optional adaptValueToFieldType( return Optional.of(mapEntries); } if (fieldDescriptor.isRepeated()) { + List listValue = (List) fieldValue; + if (listValue.contains(NullValue.NULL_VALUE)) { + if (!isAnyOrJsonValue(fieldDescriptor)) { + List filteredList = new ArrayList<>(listValue.size()); + for (Object elem : listValue) { + if (!(elem instanceof NullValue)) { + filteredList.add(elem); + } + } + listValue = filteredList; + } + } return Optional.of( AdaptingTypes.adaptingList( - (List) fieldValue, fieldToValueConverter(fieldDescriptor).reverse())); + listValue, fieldToValueConverter(fieldDescriptor).reverse())); } return Optional.of( fieldToValueConverter(fieldDescriptor).backwardConverter().convert(fieldValue)); } + private boolean isAnyOrJsonValue(FieldDescriptor fieldDescriptor) { + String typeFullName = + fieldDescriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE + ? fieldDescriptor.getMessageType().getFullName() + : ""; + return WellKnownProto.ANY_VALUE.typeName().equals(typeFullName) + || WellKnownProto.JSON_VALUE.typeName().equals(typeFullName); + } + @SuppressWarnings("rawtypes") private BidiConverter fieldToValueConverter(FieldDescriptor fieldDescriptor) { switch (fieldDescriptor.getType()) { diff --git a/runtime/src/test/resources/nullAssignability.baseline b/runtime/src/test/resources/nullAssignability.baseline index 47b9c7a0d..b60f434ea 100644 --- a/runtime/src/test/resources/nullAssignability.baseline +++ b/runtime/src/test/resources/nullAssignability.baseline @@ -33,3 +33,32 @@ Source: has(TestAllTypes{single_timestamp: null}.single_timestamp) bindings: {} result: false +Source: TestAllTypes{repeated_timestamp: [timestamp(1), null]}.repeated_timestamp == [timestamp(1)] +=====> +bindings: {} +result: true + +Source: TestAllTypes{map_bool_timestamp: {true: null, false: timestamp(1)}}.map_bool_timestamp == {false: timestamp(1)} +=====> +bindings: {} +result: true + +Source: TestAllTypes{repeated_any: [1, null]}.repeated_any == [1, null] +=====> +bindings: {} +result: true + +Source: TestAllTypes{map_bool_any: {true: null, false: 1}}.map_bool_any == {true: null, false: 1} +=====> +bindings: {} +result: true + +Source: TestAllTypes{repeated_value: [google.protobuf.Value{bool_value: true}, null]}.repeated_value == [true, null] +=====> +bindings: {} +result: true + +Source: TestAllTypes{map_bool_value: {true: null, false: google.protobuf.Value{bool_value: true}}}.map_bool_value == {true: null, false: true} +=====> +bindings: {} +result: true \ No newline at end of file diff --git a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java index 144ada5a8..ce88f90fc 100644 --- a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java +++ b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java @@ -2145,6 +2145,33 @@ public void nullAssignability() throws Exception { source = "has(TestAllTypes{single_timestamp: null}.single_timestamp)"; runTest(); + + source = + "TestAllTypes{repeated_timestamp: [timestamp(1), null]}.repeated_timestamp ==" + + " [timestamp(1)]"; + runTest(); + + source = + "TestAllTypes{map_bool_timestamp: {true: null, false: timestamp(1)}}.map_bool_timestamp ==" + + " {false: timestamp(1)}"; + runTest(); + + source = "TestAllTypes{repeated_any: [1, null]}.repeated_any == [1, null]"; + runTest(); + + source = + "TestAllTypes{map_bool_any: {true: null, false: 1}}.map_bool_any == {true: null, false: 1}"; + runTest(); + + source = + "TestAllTypes{repeated_value: [google.protobuf.Value{bool_value: true}," + + " null]}.repeated_value == [true, null]"; + runTest(); + + source = + "TestAllTypes{map_bool_value: {true: null, false: google.protobuf.Value{bool_value:" + + " true}}}.map_bool_value == {true: null, false: true}"; + runTest(); } @Test