Files
reverse-engineering-anything/src/domain/javascript/javascriptSemanticObjectReferences.test.ts
T
morlutoandCursor Agent a896e35c47 fix(javascript): preserve semantic effects and graph evidence (#9900)
* fix(javascript): preserve semantic facts across writes and implicit effects

* fix(javascript): preserve bundler identities and helper bindings

* fix(javascript): retain semantic graph evidence and coverage

* test(javascript): include the request link in endpoint traces

Response consumers now also link the originating request to the response, so an endpoint trace reports both that association and the response's json consumer.

* fix(javascript): keep source accessors known after a copied value escapes

An unknown computed getter makes accessor lookup consider every property.
A spread copy already records a value escape, and that escape was treated
as an unknown getter on the source, so the next read escaped the whole
object. Accessor lookup still fails closed for unknown writes and calls.

* fix(javascript): keep Object.defineProperty identified at its own call

The call that escapes Object was treated as a prior replacement of
defineProperty, so a descriptor value never replaced the exported sender
and bundled IPC paths were dropped.

* fix(javascript): keep Vite preload calls that precede reassignment

Helper identity used every write to the binding, so a later assignment
erased a call that had already read the canonical helper. Use the binding
value at the call when that order is known.

* fix(javascript): apply Vite cache mutations before a later reassignment

A later helper write made the whole-binding initializer check ignore an earlier cache mutation. Identity at that mutation uses the same ordered read as the call.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-11-11 17:33:47 +08:00

273 lines
9.1 KiB
TypeScript

import * as t from "@babel/types";
import { expect, it } from "vitest";
import { indexJavaScriptBindingsSteps } from "./javascriptSemanticAnalysis.js";
import {
semanticClassPropertyReferences,
semanticNativePrototypeChain,
semanticObjectPropertyReferences,
} from "./javascriptSemanticObjectReferences.js";
import { completeSemanticSteps } from "./javascriptSemanticTraversal.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import { getterExpansionSource } from "../../../tests/fixtures/javascriptGetterExpansion.js";
const objectAndState = (source: string) => {
const parsed = parseJavaScriptSource(source, "object.js");
if (parsed === null) throw new Error("Expected a parsed fixture");
const declaration = parsed.program.body[0];
if (!t.isVariableDeclaration(declaration))
throw new Error("Expected a declaration");
const init = declaration.declarations[0]?.init;
if (!t.isObjectExpression(init)) throw new Error("Expected an object");
return {
object: init,
source,
state: completeSemanticSteps(indexJavaScriptBindingsSteps(parsed.program)),
};
};
const spelled = (
fixture: ReturnType<typeof objectAndState>,
key: string | number | null,
) =>
semanticObjectPropertyReferences(fixture.object, [key], fixture.state).map(
({ node, path }) => [
fixture.source.slice(node.start ?? 0, node.end ?? 0),
JSON.stringify(path),
],
);
it.each([
["{}", "then", ["Object"]],
["{then:null}", "then", []],
["{then}", "then", []],
["{...{then:null}}", "then", []],
["{...unknown}", "then", ["Object"]],
["{__proto__:null}", "then", []],
["{__proto__:{}}", "then", []],
["{__proto__:0}", "then", ["Object"]],
["{__proto__:unknown}", "then", ["Object"]],
['{["__proto__"]:null}', "then", ["Object"]],
["[]", "then", ["Array", "Object"]],
["[]", "length", []],
["[value]", "0", []],
["[,value]", "0", ["Array", "Object"]],
["[value,...unknown]", "0", []],
["[...unknown,value]", "0", ["Array", "Object"]],
] as const)(
"retains possible native prototype lookup for %s[%s]",
(value, key, expected) => {
const parsed = parseJavaScriptSource(`const o=${value};`, "prototype.mjs");
const declaration = parsed?.program.body[0];
if (!t.isVariableDeclaration(declaration))
throw new Error("Expected declaration");
const node = declaration.declarations[0]?.init;
if (!t.isObjectExpression(node) && !t.isArrayExpression(node))
throw new Error("Expected literal");
expect(semanticNativePrototypeChain(node, key)).toEqual(expected);
},
);
it("resolves exact keys with source-order overrides, spreads and accessors", () => {
const fixture = objectAndState(
`const o = { a: 1, ...base, b: 2, a: 3, [dynamic]: 4, __proto__: proto, ...{ b: 5 }, get c() { return 6; }, c: 7 };`,
);
// Later definite own fields exclude earlier computed-key alternatives.
expect(spelled(fixture, "a")).toEqual([
["{ b: 5 }", '["a"]'],
["4", "[]"],
["3", "[]"],
]);
expect(spelled(fixture, "b")).toEqual([["{ b: 5 }", '["b"]']]);
expect(spelled(fixture, "c")).toEqual([["7", "[]"]]);
expect(spelled(fixture, "missing")).toEqual([
["{ b: 5 }", '["missing"]'],
["proto", '["missing"]'],
["4", "[]"],
["base", '["missing"]'],
]);
// A wildcard keeps possible slots, excluding data replaced by a known spread.
expect(spelled(fixture, null)).toEqual([
["7", "[]"],
["{ b: 5 }", '[{"excludedKeys":["c"]}]'],
["proto", "[null]"],
["4", "[]"],
["3", "[]"],
["base", '[{"excludedKeys":["a","b","c"]}]'],
]);
// Numeric keys use the same exact spelling as string keys.
expect(
spelled(objectAndState(`const o = { 1: "one", 2: "two" };`), 1),
).toEqual([['"one"', "[]"]]);
});
it.each([1, "1"])(
"preserves inherited numeric property selection %j and own shadowing",
(key) => {
expect(
spelled(objectAndState("const o = { __proto__: prototype };"), key),
).toEqual([["prototype", JSON.stringify([String(key)])]]);
expect(
spelled(
objectAndState('const o = { __proto__: prototype, 1: "own" };'),
key,
),
).toEqual([['"own"', "[]"]]);
},
);
it.each([
[
"static",
"static method(){return 1;} method(){return 2;}",
"static method(){return 1;}",
],
[
"instance",
"static method(){return 1;} method(){return 2;}",
"method(){return 2;}",
],
[
"prototype",
"static method(){return 1;} method(){return 2;}",
"method(){return 2;}",
],
["static", "static method(){return 1;} static set method(value){}", null],
["static", "static method=child; static method(){return 1;}", "child"],
[
"static",
"static get method(){return child;} static method(){return 1;}",
"static method(){return 1;}",
],
[
"static",
"static method(){return 1;} static get method(){return child;}",
"child",
],
[
"static",
"static get method(){return child;} static method(){return 1;} static set method(value){}",
null,
],
] as const)(
"selects method-valued %s descriptors with runtime precedence: %s",
(kind, body, expected) => {
const source = `class Box { ${body} }`;
const parsed = parseJavaScriptSource(source, "methods.mjs");
if (parsed === null) throw new Error("Expected a parsed class");
const owner = parsed.program.body[0];
if (!t.isClassDeclaration(owner)) throw new Error("Expected a class");
const state = completeSemanticSteps(
indexJavaScriptBindingsSteps(parsed.program),
);
expect(
semanticClassPropertyReferences({ owner, kind }, ["method"], state).map(
({ node }) => source.slice(node.start ?? 0, node.end ?? 0),
),
).toEqual(expected === null ? [] : [expected]);
},
);
it.each(["method(){return 1;}", "...{method: child}"])(
"keeps an intervening object data definition between accessor pairs: %s",
(definition) => {
expect(
spelled(
objectAndState(
`const o = { get method(){return child;} , ${definition}, set method(value){} };`,
),
"method",
),
).toEqual([]);
},
);
it.each(["static", "instance"] as const)(
"retains inherited %s field ownership for enumerable copies",
(kind) => {
const source = `class Base { ${kind === "static" ? "static" : ""} method=child; } class Box extends Base {}`;
const parsed = parseJavaScriptSource(source, "fields.mjs");
if (parsed === null) throw new Error("Expected a parsed class");
const owner = parsed.program.body[1];
if (!t.isClassDeclaration(owner)) throw new Error("Expected derived class");
const state = completeSemanticSteps(
indexJavaScriptBindingsSteps(parsed.program),
);
const references = semanticClassPropertyReferences(
{ owner, kind, fieldsOnly: true },
["method"],
state,
);
expect(
references.map(({ node }) =>
source.slice(node.start ?? 0, node.end ?? 0),
),
).toEqual(["child"]);
expect(references[0]?.inherited).toBe(kind === "static" ? true : undefined);
},
);
it("preserves references through a small static getter permutation", () => {
const getters = Array.from(
{ length: 5 },
(_, index) =>
`static get field${index}() { return this[key].field${index}; }`,
).join("\n");
const source = `const key = getKey(); const shared = {}; class Request {
${getters}
static get state() { return shared; }
}`;
const parsed = parseJavaScriptSource(source, "getters.js");
if (parsed === null) throw new Error("Expected a parsed fixture");
const klass = parsed.program.body.find((node): node is t.ClassDeclaration =>
t.isClassDeclaration(node),
);
if (klass === undefined) throw new Error("Expected a class declaration");
const state = completeSemanticSteps(
indexJavaScriptBindingsSteps(parsed.program),
);
const references = semanticClassPropertyReferences(
{ owner: klass, kind: "static" },
["field0"],
state,
);
expect(
references.some(
({ node }) => source.slice(node.start ?? 0, node.end ?? 0) === "shared",
),
).toBe(true);
const distinctReferences = new Set(
references.map(({ node, path, escapes }) =>
JSON.stringify([node.start, node.end, path, escapes]),
),
);
expect(references).toHaveLength(distinctReferences.size);
});
it("widens a large static getter permutation to one escaped reference", () => {
const source = getterExpansionSource(30).replaceAll("get ", "static get ");
const parsed = parseJavaScriptSource(source, "getters.js");
if (parsed === null) throw new Error("Expected a parsed fixture");
const klass = parsed.program.body.find((node): node is t.ClassDeclaration =>
t.isClassDeclaration(node),
);
if (klass === undefined) throw new Error("Expected a class declaration");
const state = completeSemanticSteps(
indexJavaScriptBindingsSteps(parsed.program),
);
const references = semanticClassPropertyReferences(
{ owner: klass, kind: "static" },
["field0"],
state,
);
expect(references).toHaveLength(1);
expect(references[0]).toMatchObject({
node: { type: "Identifier", name: "shared" },
path: [],
escapes: true,
});
expect([...state.getterResourceLimits]).toEqual(["getter-expansion"]);
});