fix(javascript): retain Reflect set receiver effects

This commit is contained in:
morluto committed 2026-10-12 01:01:18 +08:00
1 parent ad27b8b4ff
commit c98d326514
4 files changed
+46 -18

No files matched your search

+2 -1
View File
@@ -349,7 +349,8 @@ argument prefixes and the original bound receiver through `call`, `apply`, and
creation. Visibly replaced invocation intrinsics produce unknown effects instead
of using their former call semantics; captured function aliases retain their
earlier identity. Unexamined
targets remain unresolved. Creating an unused binding does not itself mutate
targets remain unresolved. `Reflect.set` examines its explicit receiver as well
as its target, retaining unknown receiver effects. Creating an unused binding does not itself mutate
an intrinsic. Variable redeclarations retain a single unconditional initializer
when they introduce no assignment or second initializer.
Stable identifier chains to fresh object, array, function,
@@ -560,6 +560,14 @@ describe("Electron module interop bound mutators", () => {
'Reflect.apply = Object.defineProperty; Reflect.apply(Object,"create",{value:customCreate});',
'const apply = Reflect.apply; Reflect.apply = Object.defineProperty; Reflect.apply(Object,"create",{value:customCreate});',
'const change = Reflect.set.bind(null,Object,"create"); change(customCreate);',
'Reflect.set({},"create",customCreate,Object);',
'const metadata = {}; Reflect.set(metadata,"create",customCreate,Object);',
'const change = Reflect.set.bind(null,{},"create",customCreate); change(Object);',
'Reflect.apply(Reflect.set,null,[{},"create",customCreate,Object]);',
'Reflect.set({},"create",customCreate,...receivers);',
'Reflect.set({},"create",...valuesAndReceivers);',
"Reflect.set({},...keysValuesAndReceivers);",
'Reflect.set({},"create",customCreate,receiver);',
'const change = Object.defineProperty.call; change(null,Object,"create",{value:customCreate});',
'const {apply:change} = Object.defineProperty; change(null,[Object,"create",{value:customCreate}]);',
'const change = Reflect.apply.call; change(null,Object.defineProperty,null,[Object,"create",{value:customCreate}]);',
@@ -587,6 +595,8 @@ describe("Electron module interop bound mutators", () => {
'Object.defineProperty.apply(null,[metadata,"x",{value:1}]);',
'const change = Object.defineProperty.call; change(null,metadata,"x",{value:1});',
'const {apply:change} = Object.defineProperty; change(null,[metadata,"x",{value:1}]);',
'Reflect.set(metadata,"x",1,{});',
'const receiver = {}; Reflect.set({},"x",1,receiver);',
])("keeps a fresh bound target or unused binding separate: %s", (write) => {
expect(
analyzeJavaScriptStaticSource(
@@ -96,25 +96,40 @@ export function* collectJavaScriptIdentityWritesSteps(
(owner === "Object" && objectMutators.has(method ?? "")) ||
(owner === "Reflect" && reflectMutators.has(method ?? ""))
) {
const target = arguments_?.[0];
// A known mutator with an opaque argument list can overwrite any
// captured intrinsic. Do not turn an unexamined target into proof.
if (target === undefined || t.isSpreadElement(target)) {
const scope = origin(node, "opaque-call");
if (scope !== undefined) retain(node, scope);
}
if (t.isNode(target) && !t.isSpreadElement(target)) {
const { root, members } = electronMemberPath(target);
const exported = origin(root, "member");
if (exported !== undefined) {
const key = keyedMutators.has(method ?? "")
? [staticString(arguments_?.[1]) ?? null]
: [];
retain(node, [...exported, ...members, ...key]);
} else if (!freshObject(target)) {
// Reflect.set can write through its explicit receiver as well as
// dispatching through its target. Both identities need examination.
const targets =
owner === "Reflect" &&
method === "set" &&
arguments_ !== undefined &&
(arguments_.length >= 4 || arguments_.some(t.isSpreadElement))
? [
arguments_[0],
arguments_.some(t.isSpreadElement)
? undefined
: arguments_[3],
]
: [arguments_?.[0]];
for (const target of targets) {
// A known mutator with an opaque argument list can overwrite any
// captured intrinsic. Do not turn an unexamined target into proof.
if (target === undefined || t.isSpreadElement(target)) {
const scope = origin(node, "opaque-call");
if (scope !== undefined) retain(node, scope);
}
if (t.isNode(target) && !t.isSpreadElement(target)) {
const { root, members } = electronMemberPath(target);
const exported = origin(root, "member");
if (exported !== undefined) {
const key = keyedMutators.has(method ?? "")
? [staticString(arguments_?.[1]) ?? null]
: [];
retain(node, [...exported, ...members, ...key]);
} else if (!freshObject(target)) {
const scope = origin(node, "opaque-call");
if (scope !== undefined) retain(node, scope);
}
}
}
}
}
@@ -36,6 +36,7 @@ const INTRINSIC_MUTATIONS = [
"bound.cjs",
'const change = Object.defineProperty.bind(null,Object,"create"); change({value:customCreate});',
],
["receiver.cjs", 'Reflect.set({},"create",customCreate,Object);'],
] as const;
cliTest.for(["CLI", "stdio MCP"] as const)(
@@ -103,6 +104,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
require("./uncurried.cjs");
require("./generic-bind.cjs");
require("./replaced-apply.cjs");
require("./receiver.cjs");
throw new Error("must not execute main");
`;
await writeFile(join(root, "main.cjs"), source);
@@ -129,7 +131,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
preload_entrypoints: 1,
});
expect(result.statistics.parse_failures).toBe(0);
expect(result.statistics.parsed_javascript_files).toBe(9);
expect(result.statistics.parsed_javascript_files).toBe(10);
const windows = result.graph.nodes.filter(
({ kind }) => kind === "browser-window",
);