From 4a7ccbf75ab7e42db5c4d6d2dd3ac77f0144b6be Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 29 Jul 2026 14:16:00 +0300 Subject: [PATCH] Answer field existence questions without mapping the field value ContentFieldProperty is a CustomJsValue descriptor whose CustomValue maps the stored JsonValue to a JsValue on first read. Existence and enumerability questions never need that value, but they used to pay for it: `in`, hasOwnProperty, propertyIsEnumerable, Object.keys/values/entries, Object.assign, object spread and JSON.stringify all reached the object through GetOwnProperty, which materializes the descriptor whose value the caller then reads or discards. Jint lets a host answer those questions directly through ProbeOwnProperty. The override deliberately mirrors GetOwnProperty line for line, minus the descriptor: same initialization, same toJSON exclusion, same lookup, and the enumerable flag read off the descriptor rather than off its value. The engine trusts the probe without re-verifying it on the hot path, so a wrong Missing would silently drop the key from every enumeration above - which is why the two are kept adjacent in the file, pinned by tests covering `in`, hasOwnProperty, propertyIsEnumerable, Object.keys, spread and JSON.stringify plus a delete and an add, and checked on every test run by the host-contract verification enabled earlier in this branch. ContentDataObject deliberately does not get the same override: its GetOwnProperty auto-creates a field for any name probed, so a probe that agreed with it at the same instant would have to do the same, and that quirk is pre-existing tenant-visible behaviour this change has no business altering. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016uV6H9cTntzsoKiaJRBn4f --- .../ContentWrapper/ContentFieldObject.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs index ce162f11c..5f05220a9 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs @@ -132,6 +132,29 @@ public sealed class ContentFieldObject : ObjectInstance return valueProperties?.GetValueOrDefault(propertyName) ?? PropertyDescriptor.Undefined; } + protected override OwnPropertyProbe ProbeOwnProperty(JsValue property) + { + // Deliberately mirrors GetOwnProperty above, minus the descriptor: the flags are on the descriptor + // itself, so an existence or enumerability question is answered without ever reading CustomValue, + // which is what maps the JSON value to a JsValue. The engine trusts the answer without verifying it, + // so the two must stay in step. + EnsurePropertiesInitialized(); + + var propertyName = property.AsString(); + + if (propertyName.Equals("toJSON", StringComparison.OrdinalIgnoreCase)) + { + return OwnPropertyProbe.Missing; + } + + if (!valueProperties.TryGetValue(propertyName, out var propertyDescriptor)) + { + return OwnPropertyProbe.Missing; + } + + return propertyDescriptor.Enumerable ? OwnPropertyProbe.Enumerable : OwnPropertyProbe.NonEnumerable; + } + public override IEnumerable> GetOwnProperties() { EnsurePropertiesInitialized();