Repository navigation
fix(object): retain existing-property attributes in Object.defineProperty - #4697
Merged
Merged
Conversation
…erty Per ES OrdinaryDefineOwnProperty / ValidateAndApplyPropertyDescriptor, redefining an EXISTING own property must retain the attribute fields the descriptor omits — they do NOT reset to the new-property `false` default. `js_object_define_property` previously defaulted every omitted attribute to `false` (writable to `has_accessor`) on every call, so converting an enumerable data property to a getter (or back to a value) silently dropped its enumerability and configurability. This blocked zod v4 under `perry.compilePackages` (discussion #3438): its `$ZodObject` constructor swaps the enumerable `shape` data property for a self-replacing getter via `Object.defineProperty`, expecting `{ ...def }` to still spread `shape`. Pre-fix `shape` became non-enumerable, the spread dropped it, `normalizeDef`'s `value.shape` was undefined, and object parsing crashed with "Cannot read properties of undefined (reading '_zod')". Fix: in both the object and closure define paths, capture the property's current effective attributes before any mutation (all-true when no descriptor side-table entry exists, matching the JS data-property default) and default each omitted descriptor field to the existing value. New properties still default to `false`, preserving prior behavior. Adds test-files/test_issue_defineproperty_retain_attrs.ts (byte-matches `node --experimental-strip-types`).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Object.definePropertyon an existing own property must retain the attribute fields the descriptor omits (per ESOrdinaryDefineOwnProperty/ValidateAndApplyPropertyDescriptor) — they do not reset to the new-propertyfalsedefault. Perry previously defaulted every omitted attribute tofalse(andwritabletohas_accessor) on every define call, so converting an enumerable data property to a getter (or back to a value) silently dropped its enumerability / configurability.This is a general spec-compliance bug, surfaced by zod v4 under
perry.compilePackages(discussion #3438).Root cause
zod's
$ZodObjectswaps the enumerableshapedata property for a self-replacing getter viaObject.defineProperty, then relies on{ ...def }still spreadingshape. Pre-fix, the firstdefineProperty(def, "shape", { get })resetenumerabletofalse, so{ ...def }droppedshape,normalizeDef'svalue.shapewasundefined, and the slow-path object parser dereferencedshape[key]._zodonundefined:(Node never hits this because it takes the JIT
new Functionfastpass; Perry correctly throws onnew Function, so zod'sallowsEvalgate isfalseand it falls to the slow path — which exposed the latent attribute bug.)Fix
In both the object and closure define paths of
js_object_define_property, capture the property's current effective attributes before any mutation (all-truewhen no descriptor side-table entry exists, matching the JS data-property default) and default each omitted descriptor field to the existing value. Brand-new properties still default tofalse, preserving prior behavior.Validation
test-files/test_issue_defineproperty_retain_attrs.ts— byte-matchesnode --experimental-strip-types. Covers value-only redefine, data→getter→value transitions, configurability retention, the new-propertyfalsecontrol, and the exact zodshape-getter→spread pattern.perry.compilePackages: ["zod"]) object / nested-object / array / boolean / optional parsing now works and matches Node — previously everyz.object(...).parse(...)crashed.cargo fmtclean.Scope / follow-ups
This unblocks the core of zod object parsing. Two independent zod runtime bugs remain (separate from this change), to be filed/fixed as follow-ups:
z.string().min(2)(checks) →ch._zod.onattachundefinedsafeParseon invalid input →error is not a functionRefs #3438, #793.