diff --git a/cel/cel_test.go b/cel/cel_test.go index 31b2a318..ab4f21a6 100644 --- a/cel/cel_test.go +++ b/cel/cel_test.go @@ -2869,6 +2869,23 @@ func TestOptionalValuesEval(t *testing.T) { expr: `has({?'foo': optional.none()}.foo.value)`, out: "no such key: foo", }, + // has() on non-container types errors instead of returning false. + { + expr: `has(dyn(42).field)`, + out: "no such key: field", + }, + { + expr: `has(dyn('hello').field)`, + out: "no such key: field", + }, + { + expr: `has(dyn(true).field)`, + out: "no such key: field", + }, + { + expr: `has(dyn(null).field)`, + out: "no such key: field", + }, { expr: `{}.?invalid`, out: types.OptionalNone, diff --git a/interpreter/attributes.go b/interpreter/attributes.go index 6b8b5c1b..3b84fa3b 100644 --- a/interpreter/attributes.go +++ b/interpreter/attributes.go @@ -1419,7 +1419,13 @@ func refQualify(adapter types.Adapter, obj any, idx ref.Val, presenceTest, prese return val, true, nil default: if presenceTest && !errorOnBadPresenceTest { - return nil, false, nil + // Optional values and optional field selection (presenceOnly=false) + // report not-present for non-container types. has() macro + // (presenceOnly=true) on non-optional primitives falls through + // to missingKey to avoid silent false on bad presence tests. + if _, isOpt := celVal.(*types.Optional); isOpt || !presenceOnly { + return nil, false, nil + } } return nil, false, missingKey(idx) }