fix(javascript): project class definition-time effects through getters

Computed keys and superclass expressions evaluate against the enclosing
getter receiver. Visit those definition-time expressions when collecting
accessor effects, and walk past class/method boundaries in #ownsThis when
the path arrives from heritage or a computed key so receiver writes are
not dropped.

Fixes #9229

Supersedes #9233

Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
SahinurDEVandgensokyo-dev authored and Ysopwnable committed 2026-11-14 14:44:47 +01:00
1 parent 4d6262303d
commit 4a42707544
3 files changed
+86 -9

No files matched your search

@@ -186,6 +186,41 @@ describe("getter receiver mutation aliases (#1665, #2654)", () => {
])("preserves a known literal for %s", (_shape, body) => {
expect(resultValue(body)).toMatchObject({ status: "literal", value: 1 });
});
it.each([
["class computed method key", "class { [(this.x = 2, 'k')]() {} }"],
["class computed field key", "class { [(this.x = 2, 'k')] = 0; }"],
["superclass expression", "class extends (this.x = 2, Object) {}"],
[
"nested class key",
"class extends (class { [(this.x = 2, 'k')]() {} }) {}",
],
["object method computed key", "({ [(this.x = 2, 'k')]() {} })"],
])("invalidates the receiver after a %s write", (_label, result) => {
expect(
resultValue(
`const box = { x: 1, get trigger() { return ${result}; } }; box.trigger; return box.x;`,
),
).toMatchObject({ status: "unknown" });
});
it.each([
["uninvoked class method", "class { method() { this.x = 2; } }"],
[
"uninvoked computed class method",
"class { [(0, 'k')]() { this.x = 2; } }",
],
["uninvoked computed object method", "({ [(0, 'k')]() { this.x = 2; } })"],
["instance field initializer", "class { f = (this.x = 2); }"],
["static field receiver", "class { static s = (this.x = 2); }"],
["static block receiver", "class { static { this.x = 2; } }"],
])("keeps the receiver value through a %s", (_label, result) => {
expect(
resultValue(
`const box = { x: 1, get trigger() { return ${result}; } }; box.trigger; return box.x;`,
),
).toMatchObject({ status: "literal", value: 1 });
});
it("terminates recursive receiver getters", () => {
expect(
resultValue(
@@ -77,6 +77,17 @@ const resultBranches = (node: t.Node): readonly t.Node[] | undefined => {
return undefined;
};
/** Heritage and computed keys see the `this` around their definition. */
const evaluatesInOuterThis = (owner: t.Node, child: t.Node): boolean =>
t.isClass(owner)
? child === owner.superClass
: (t.isObjectMethod(owner) ||
t.isClassMethod(owner) ||
t.isClassProperty(owner) ||
t.isClassAccessorProperty(owner)) &&
owner.computed === true &&
child === owner.key;
/** Each walk owns local aliases; getter-to-getter traversal belongs to the query worklist. */
export class GetterResultWalker {
readonly #pending: GetterWork[] = [];
@@ -229,13 +240,22 @@ export class GetterResultWalker {
}
#ownsThis(expression: t.ThisExpression | t.Super): boolean {
let owner = this.query.state.parentsByNode.get(expression);
while (
owner !== undefined &&
!t.isClass(owner) &&
!(t.isFunction(owner) && !t.isArrowFunctionExpression(owner))
)
owner = this.query.state.parentsByNode.get(owner);
const parents = this.query.state.parentsByNode;
let child: t.Node = expression;
let owner = parents.get(child);
while (owner !== undefined) {
if (evaluatesInOuterThis(owner, child)) {
// A computed member key runs outside its class, not just its member.
const body = parents.get(owner);
child = t.isClassBody(body) ? parents.get(body)! : owner;
} else if (
t.isClass(owner) ||
(t.isFunction(owner) && !t.isArrowFunctionExpression(owner))
)
break;
else child = owner;
owner = parents.get(child);
}
return owner === this.job.invocation.getter;
}
@@ -393,6 +393,23 @@ const accessorOperands = new WeakMap<
readonly AccessorEffectOperand[]
>();
/** Expressions a definition runs immediately; deferred bodies and class-receiver initializers stay out. */
const definitionTimeExpressions = (node: t.Function | t.Class): t.Node[] => {
if (t.isObjectMethod(node)) return node.computed ? [node.key] : [];
if (!t.isClass(node)) return [];
return [
...(node.superClass == null ? [] : [node.superClass]),
...node.body.body.flatMap((member) =>
(t.isClassMethod(member) ||
t.isClassProperty(member) ||
t.isClassAccessorProperty(member)) &&
member.computed
? [member.key]
: [],
),
];
};
/** Collect invocation operands once; receiver interpretation belongs to the getter owner. */
const accessorEffectOperands = (
method: t.Function,
@@ -406,8 +423,13 @@ const accessorEffectOperands = (
const writeOnly = new WeakSet<t.Node>();
while (pending.length > 0) {
const { node, parent } = pending.pop()!;
// Defining a nested callable does not invoke it.
if (t.isFunction(node) || t.isClass(node)) continue;
// Defining a nested callable does not invoke it, but the definition itself
// evaluates heritage and computed keys against the enclosing receiver.
if (t.isFunction(node) || t.isClass(node)) {
for (const child of definitionTimeExpressions(node).reverse())
pending.push({ node: child, parent: node });
continue;
}
const write = semanticWriteSiteTarget(node);
if (write !== null) {
for (const target of semanticWriteTargets(write)) {