mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 18:52:48 +00:00
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:
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)) {
|
||||
|
||||
Reference in new issue
Block a user