-
-
Notifications
You must be signed in to change notification settings - Fork 159
fix(runtime): Object.defineProperty on a class installs a static own property (#7190) #7798
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| **`Object.defineProperty(SomeClass, key, descriptor)` now installs a static own property** (#7190). It was silently dropped — not misfiled, dropped: `C.zzz` came back `undefined` and so did `new C().zzz`, so the value went nowhere at all. | ||
|
|
||
| The cause is that `C` and `C.prototype` answer `class_ref_id` with the **same class id** — Perry maps a prototype ref back to its class — so the define path could not tell the two receivers apart and treated every one as a prototype install. That is correct for `Object.defineProperty(C.prototype, …)`, the drizzle `applyMixins` case the arm was written for, and wrong for the class itself. `class_prototype_ref_id` is the discriminator, and `descriptors.rs` was already using it to tell the two apart when reporting descriptors; the define path now does the same and routes a bare class ref into `CLASS_DYNAMIC_PROPS`, the table `static x = …` already writes to, so the existing static read path finds it with no new lookup. | ||
|
|
||
| The user-visible form was zod: it renames constructors with `Object.defineProperty(Cls, "name", { value })`, and Perry kept resolving `.name` through the class registry, so class errors reported `constructor.name === "Definition"`. | ||
|
|
||
| Two things that had to come with it, both found by the oracle rather than by reasoning: | ||
|
|
||
| * **Attributes.** A declared `static x = …` is writable and enumerable (CreateDataPropertyOrThrow); a `defineProperty` data descriptor is neither. Both now live in one table, so the descriptor-installed ones carry their `(writable, enumerable, configurable)` bits and an *absent* entry keeps the previous `(true, true, true)` reporting for declared fields. Without this, `Object.keys(C)` gained a key Node does not report — the first cut of this fix did exactly that, leaking a non-enumerable `hidden` into both `Object.keys` and `for…in`. | ||
| * **`configurable` is retain-or-default, not default.** ECMA-262 `[[DefineOwnProperty]]` defaults an omitted field to `false` on a NEW property but RETAINS it on an existing one. The built-in `name`/`length` slots are `configurable: true`, so redefining `name` without saying `configurable` must stay configurable while a brand-new key must not. Hardcoding either answer fails one of the two, and both appear in the same test. | ||
|
|
||
| `getOwnPropertyDescriptor(C, "name")` now agrees with `C.name` too — previously the value read reported the redefined string while the descriptor still reported the declared one, which is the state that makes a define look like it never happened. | ||
|
|
||
| Verified against Node v26.5.1: the new gap test `test_gap_class_static_define_property_7190.ts` passes byte-for-byte, covering all three receivers (function, class, class prototype), an arbitrary key as well as `name`, subclasses, class expressions, and the enumerability/descriptor bits. `test_gap_class` (25) and `test_gap_static` (3) stay green. | ||
|
|
||
| Two pre-existing failures were checked rather than assumed: `test_gap_2159_defineproperty_class_prototype` fails on clean `main` in the release sweep and its diff is an unsettled top-level await, not a descriptor; and the runtime lib suite's intermittent failure is #7365 — `obj_dispatch_ic_tests::a_hit_requires_matching_name_bytes_not_a_matching_address` fails **10 of 12** isolated runs on clean `main` against 7 of 12 with this change, so it is order-dependent flake and not fallout here. |
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
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
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
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| // Gap: `Object.defineProperty(SomeClass, key, descriptor)` (#7190). | ||
| // | ||
| // A static define on a CLASS was silently dropped — not misfiled, dropped: | ||
| // `C.zzz` was `undefined` and so was `new C().zzz`, so the value went nowhere. | ||
| // The cause is that `C` and `C.prototype` answer `class_ref_id` with the SAME | ||
| // class id (Perry maps the prototype ref back to its class), so the define path | ||
| // could not tell the two receivers apart and treated every one as a prototype | ||
| // install. That is right for `defineProperty(C.prototype, …)` — the drizzle | ||
| // `applyMixins` case that arm was written for — and wrong for `defineProperty(C, …)`. | ||
| // | ||
| // The user-visible form was zod: class errors reported | ||
| // `constructor.name === "Definition"` because the library renames constructors | ||
| // with `Object.defineProperty(Cls, "name", { value })`, and Perry kept | ||
| // resolving `.name` through the class registry. | ||
| // | ||
| // This test asserts all three receivers stay distinct — function, class, | ||
| // class prototype — and covers the attribute bits, because a static field and a | ||
| // `defineProperty` data descriptor share one side table but have opposite | ||
| // defaults: `static x = …` is writable+enumerable (CreateDataPropertyOrThrow), | ||
| // a data descriptor is neither. Getting that wrong does not show up in the | ||
| // value, only in `Object.keys` and the descriptor — which is exactly the shape | ||
| // that hid the original bug. | ||
|
|
||
| class D {} | ||
| Object.defineProperty(D, "name", { value: "Renamed" }); | ||
| console.log("class-name:", D.name); | ||
| console.log("class-desc:", JSON.stringify(Object.getOwnPropertyDescriptor(D, "name"))); | ||
|
|
||
| // A plain function was always correct; it must stay correct. | ||
| function f() {} | ||
| Object.defineProperty(f, "name", { value: "RenamedFn" }); | ||
| console.log("fn-name:", f.name); | ||
|
|
||
| // Subclass, and the instance's view of it. | ||
| class Base {} | ||
| class Sub extends Base {} | ||
| Object.defineProperty(Sub, "name", { value: "SubRenamed" }); | ||
| console.log("sub-name:", Sub.name); | ||
| console.log("ctor-name:", (new (Sub as any)() as any).constructor.name); | ||
|
|
||
| // Class expression. | ||
| const E = class {}; | ||
| Object.defineProperty(E, "name", { value: "ExprRenamed" }); | ||
| console.log("expr-name:", (E as any).name); | ||
|
|
||
| // An arbitrary key, not just `name` — the drop was general. | ||
| class G {} | ||
| Object.defineProperty(G, "zzz", { value: 7, configurable: true }); | ||
| console.log("static-zzz:", (G as any).zzz); | ||
| // ...and it must NOT have landed on the prototype. | ||
| console.log("instance-zzz:", (new G() as any).zzz); | ||
|
|
||
| // Defining on the prototype still installs an instance member. | ||
| class H {} | ||
| Object.defineProperty(H.prototype, "pm", { value: 5, configurable: true }); | ||
| console.log("proto-pm:", (new H() as any).pm); | ||
|
|
||
| // Enumerability: a data descriptor defaults to non-enumerable, a declared | ||
| // static field is enumerable, and both live in the same table. | ||
| class K { | ||
| static declared = 1; | ||
| } | ||
| Object.defineProperty(K, "hidden", { value: 7 }); | ||
| Object.defineProperty(K, "shown", { value: 8, enumerable: true }); | ||
| console.log("keys:", JSON.stringify(Object.keys(K).sort())); | ||
| const seen: string[] = []; | ||
| for (const k in K) seen.push(k); | ||
| console.log("forin:", JSON.stringify(seen.sort())); | ||
| console.log("values:", (K as any).hidden, (K as any).shown, K.declared); | ||
| console.log("hidden-desc:", JSON.stringify(Object.getOwnPropertyDescriptor(K, "hidden"))); | ||
| console.log("shown-desc:", JSON.stringify(Object.getOwnPropertyDescriptor(K, "shown"))); | ||
| console.log("declared-desc:", JSON.stringify(Object.getOwnPropertyDescriptor(K, "declared"))); | ||
| console.log("names:", JSON.stringify(Object.getOwnPropertyNames(K).sort())); |
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 31040
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 50370
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 50369
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 49865
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 508
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 508
Clear
CLASS_STATIC_DEFINED_ATTRSwhen a static property is deleted.delete C.xremoves the value but leaves its descriptor metadata. AfterC.x = 2,Object.getOwnPropertyDescriptor(C, "x")can report stale attributes, andObject.keys(C)can omitx. Clear the metadata during deletion or when assignment recreates the property.🤖 Prompt for AI Agents