From cbeeeda97b33c1ea7e00890b17b9b5870389043f Mon Sep 17 00:00:00 2001 From: Ahmed Date: Tue, 27 Oct 2026 18:31:59 +0500 Subject: [PATCH] fix(javascript): resolve preceding exported variable initializers and computed method keys (#5387) fix(javascript): resolve variable initializers before computed method keys --- .../javascriptSemanticCallResolution.test.ts | 53 +++++++++++++++++++ ...scriptSemanticMutationInitializers.test.ts | 28 +++++++++- .../javascriptSemanticMutationInitializers.ts | 7 ++- 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/domain/javascript/javascriptSemanticCallResolution.test.ts b/src/domain/javascript/javascriptSemanticCallResolution.test.ts index 754b373850..af65fb35ac 100644 --- a/src/domain/javascript/javascriptSemanticCallResolution.test.ts +++ b/src/domain/javascript/javascriptSemanticCallResolution.test.ts @@ -486,3 +486,56 @@ it("closes cycles at full-binding reads with bounded lookup growth", () => { expect(ten).toBeLessThan(3 * eight); expect(twelve).toBeLessThan(3 * ten); }); + +it("resolves calls to preceding exported variable initializers and computed method keys", () => { + const context = indexed(` + export const pick = () => "target"; + const obj = { [pick()]() { return 1; } }; + pick(); + `); + const objStatement = context.state.program.body[1]; + if ( + !t.isVariableDeclaration(objStatement) || + !t.isVariableDeclarator(objStatement.declarations[0]) || + !t.isObjectExpression(objStatement.declarations[0].init) + ) + throw new Error("Missing obj declaration"); + const method = objStatement.declarations[0].init.properties[0]; + if (!t.isObjectMethod(method) || !t.isCallExpression(method.key)) + throw new Error("Missing computed key call"); + const target = context.state.callables.find( + (callable) => callable.name === "pick", + ); + if (target === undefined) throw new Error("Missing target"); + expect(context.resolve(method.key.callee)).toMatchObject({ + complete: true, + callableIds: [target.callableId], + }); + + const callStatement = context.state.program.body[2]; + if ( + !t.isExpressionStatement(callStatement) || + !t.isCallExpression(callStatement.expression) + ) + throw new Error("Missing pick call"); + expect(context.resolve(callStatement.expression.callee)).toMatchObject({ + complete: true, + callableIds: [target.callableId], + }); +}); + +it("resolves calls in computed method keys before subsequent pattern writes", () => { + const analysis = analyzeJavaScriptSemantics(` + function original() {} + function replacement() {} + let make = original; + const obj = { [make()]() { return 1; } }; + if (flag) [make] = [replacement]; + `); + const original = analysis.callables.find(({ name }) => name === "original"); + expect(original).toBeDefined(); + expect(analysis.callSites[0]).toMatchObject({ + resolution: "exact", + calleeCallableIds: [original?.callableId], + }); +}); diff --git a/src/domain/javascript/javascriptSemanticMutationInitializers.test.ts b/src/domain/javascript/javascriptSemanticMutationInitializers.test.ts index d1b89be7c6..8104b08874 100644 --- a/src/domain/javascript/javascriptSemanticMutationInitializers.test.ts +++ b/src/domain/javascript/javascriptSemanticMutationInitializers.test.ts @@ -19,6 +19,7 @@ it.each([ ["accessor value", true, true], ["public key", false, true], ["accessor key", false, true], + ["method key", false, true], ] as const)( "orders %s reads with static=%s only when evaluated at class definition", (kind, isStatic, ordered) => { @@ -31,7 +32,9 @@ it.each([ ? t.classPrivateProperty(t.privateName(t.identifier("field")), value) : kind.startsWith("accessor") ? t.classAccessorProperty(key, value, null, null, inKey) - : t.classProperty(key, value, null, null, inKey); + : kind.startsWith("method") + ? t.classMethod("method", key, [], t.blockStatement([]), inKey) + : t.classProperty(key, value, null, null, inKey); field.static = isStatic; const program = t.program([ t.variableDeclaration("let", [ @@ -69,6 +72,29 @@ it.each([ }, ); +it("orders computed method key reads evaluated during object literal definition", () => { + const initializer = t.numericLiteral(1); + const read = t.identifier("child"); + const method = t.objectMethod("method", read, [], t.blockStatement([]), true); + const program = t.program([ + t.variableDeclaration("let", [ + t.variableDeclarator(t.identifier("child"), initializer), + ]), + t.variableDeclaration("const", [ + t.variableDeclarator(t.identifier("obj"), t.objectExpression([method])), + ]), + ]); + const parents = new WeakMap(); + traverseJavaScriptAst(program, { + enter: (node, parent) => { + if (parent !== null) parents.set(node, parent); + }, + }); + expect(semanticTopLevelEffectFollowsCapture(initializer, read, parents)).toBe( + true, + ); +}); + describe("parameter references copied into function body bindings", () => { it.each([ ["var arg = []; mutate(arg);", false], diff --git a/src/domain/javascript/javascriptSemanticMutationInitializers.ts b/src/domain/javascript/javascriptSemanticMutationInitializers.ts index 8953f9b27b..01eb3b1383 100644 --- a/src/domain/javascript/javascriptSemanticMutationInitializers.ts +++ b/src/domain/javascript/javascriptSemanticMutationInitializers.ts @@ -131,7 +131,12 @@ export const semanticTopLevelEffectFollowsCapture = ( let declarator: t.VariableDeclarator | undefined; let child: t.Node | undefined; while (node !== undefined) { - if (t.isFunction(node) || t.isLoop(node)) return null; + if ( + (t.isFunction(node) && + (child === undefined || !semanticComputedMethodKey(node, child))) || + t.isLoop(node) + ) + return null; if (delayedInstanceFieldValue(node, child)) return null; if (t.isVariableDeclarator(node)) declarator = node; if (t.isStatement(node)) {