fix(javascript): project getter class static work onto the captured receiver

Static blocks and static field values run when a class is defined, so a
getter that returns a class whose static work writes through an alias of the
getter's receiver changes that receiver. The definition-time walk now visits
static work too; `this` inside it stays the class.

Refs #9229

Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
gensokyo-devandgensokyo-dev authored and Ysopwnable committed 2026-11-14 14:44:47 +01:00
1 parent 91297ca745
commit 39c820c90d
2 files changed
+34 -1

No files matched your search

@@ -233,6 +233,24 @@ describe("getter receiver mutation aliases (#1665, #2654)", () => {
).toMatchObject({ status: "literal", value: 1 });
});
it.each([
["static block", "class { static { self.x = 2; } }"],
["static field value", "class { static s = (self.x = 2); }"],
[
"static block of a nested heritage class",
"class extends (class { static { self.x = 2; } }) {}",
],
])(
"invalidates the receiver after a %s writes through a captured alias",
(_label, result) => {
expect(
resultValue(
`const box = { x: 1, get trigger() { const self = this; return ${result}; } }; box.trigger; return box.x;`,
),
).toMatchObject({ status: "unknown" });
},
);
it("terminates recursive receiver getters", () => {
expect(
resultValue(
@@ -405,12 +405,27 @@ const computedDefinitionMembers = (node: t.Function | t.Class): t.Node[] => {
);
};
/** Expressions a definition runs immediately; deferred bodies and class-receiver initializers stay out. */
/**
* Expressions a definition runs immediately: heritage, computed keys, static
* blocks and static field values. Deferred bodies and instance initializers
* stay out; `this` inside static work is the class, not the enclosing receiver.
*/
const definitionTimeExpressions = (node: t.Function | t.Class): t.Node[] => [
...(t.isClass(node) && node.superClass != null ? [node.superClass] : []),
...computedDefinitionMembers(node).flatMap((member) =>
"key" in member ? [member.key] : [],
),
...(t.isClass(node)
? node.body.body.flatMap((member): t.Node[] =>
t.isStaticBlock(member)
? [member]
: (t.isClassProperty(member) || t.isClassPrivateProperty(member)) &&
member.static &&
member.value != null
? [member.value]
: [],
)
: []),
];
/** Collect invocation operands once; receiver interpretation belongs to the getter owner. */