From d917045111f09db82aeddaeae9b63d4d0554f447 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 12 Jun 2026 10:41:21 -0700 Subject: [PATCH] Ensure chained optional field selection does not repeatedly wrap the optional type Fixes https://github.com/google/cel-java/issues/1083 PiperOrigin-RevId: 931223677 --- .../extensions/CelOptionalLibraryTest.java | 35 +++++++++++++++++++ .../dev/cel/runtime/DefaultInterpreter.java | 8 +++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java index 34c7f89f9..2ba12910f 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java @@ -44,6 +44,7 @@ import dev.cel.common.values.CelByteString; import dev.cel.common.values.NullValue; import dev.cel.compiler.CelCompiler; +import dev.cel.expr.conformance.proto3.NestedTestAllTypes; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import dev.cel.parser.CelMacro; @@ -603,6 +604,16 @@ public void optionalFieldSelection_onMap_returnsOptionalValue() throws Exception assertThat(result).hasValue(2L); } + @Test + public void optionalFieldSelection_onMap_chained_returnsSinglyWrappedOptional() throws Exception { + Cel cel = newCelBuilder().setResultType(OptionalType.create(SimpleType.STRING)).build(); + CelAbstractSyntaxTree ast = compile(cel, "{'foo': {'bar': 'baz'}}.?foo.?bar"); + + Optional result = (Optional) cel.createProgram(ast).eval(); + + assertThat(result).hasValue("baz"); + } + @Test public void optionalFieldSelection_onProtoMessage_returnsOptionalEmpty() throws Exception { Cel cel = @@ -619,6 +630,30 @@ public void optionalFieldSelection_onProtoMessage_returnsOptionalEmpty() throws assertThat(result).isEmpty(); } + @Test + public void optionalFieldSelection_onProtoMessage_chained_returnsSinglyWrappedOptional() + throws Exception { + Cel cel = + newCelBuilder() + .setResultType(OptionalType.create(SimpleType.INT)) + .addVar( + "msg", StructTypeReference.create(NestedTestAllTypes.getDescriptor().getFullName())) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "msg.?payload.?single_int32"); + + Optional result = + (Optional) + cel.createProgram(ast) + .eval( + ImmutableMap.of( + "msg", + NestedTestAllTypes.newBuilder() + .setPayload(TestAllTypes.newBuilder().setSingleInt32(5).build()) + .build())); + + assertThat(result).hasValue(5L); + } + @Test public void optionalFieldSelection_onProtoMessage_returnsOptionalValue() throws Exception { Cel cel = diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java index 9abc3716c..fdab71c3d 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java @@ -783,8 +783,12 @@ private Optional maybeEvalOptionalSelectField( } IntermediateResult result = evalFieldSelect(frame, expr, operand, field, false); - return Optional.of( - IntermediateResult.create(result.attribute(), Optional.of(result.value()))); + // Ensure only one level of optional is wrapped when chaining optional field selections. + Object resultValue = result.value(); + if (!(resultValue instanceof Optional)) { + resultValue = Optional.of(resultValue); + } + return Optional.of(IntermediateResult.create(result.attribute(), resultValue)); } private IntermediateResult evalBoolean(ExecutionFrame frame, CelExpr expr, boolean strict)