diff --git a/src/domain/javascript/javascriptSemanticAnalysis.getter_receivers.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.getter_receivers.test.ts index c3b38842a6..aed957fe2b 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.getter_receivers.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.getter_receivers.test.ts @@ -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( diff --git a/src/domain/javascript/javascriptSemanticGetterResultWalker.ts b/src/domain/javascript/javascriptSemanticGetterResultWalker.ts index f9049b12f2..e0193ccbef 100644 --- a/src/domain/javascript/javascriptSemanticGetterResultWalker.ts +++ b/src/domain/javascript/javascriptSemanticGetterResultWalker.ts @@ -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; } diff --git a/src/domain/javascript/javascriptSemanticMemberMutations.ts b/src/domain/javascript/javascriptSemanticMemberMutations.ts index bf12e3354a..2ef7c16687 100644 --- a/src/domain/javascript/javascriptSemanticMemberMutations.ts +++ b/src/domain/javascript/javascriptSemanticMemberMutations.ts @@ -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(); 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)) {