fix(javascript): integrate Reflect.set receiver mutation coverage

Retain concurrent explicit receiver and opaque argument coverage while
preserving corrected call/apply receiver controls and the public CLI/MCP
fixture. Keep the complete invocation follow-up history without rewriting it.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <[email protected]>
Co-authored-by: morluto <[email protected]>
This commit is contained in:
N0zoM1z0
2026-10-11 17:06:34 +00:00
co-authored by factory-droid[bot] Kaoru0822 morluto
4 changed files with 48 additions and 20 deletions
+4 -3
View File
@@ -350,9 +350,10 @@ creation. Arguments follow each actual callable invocation rather than property
chain depth; rebinding retains the original bound receiver.
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 an intrinsic. Variable redeclarations retain a
single unconditional initializer
earlier identity. Unexamined 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,
undecorated class or RegExp syntax keep those targets separate from intrinsic
@@ -609,6 +609,14 @@ describe("Electron module interop bound mutators", () => {
'const change = Object.defineProperty.call; change.call(Object.defineProperty,null,Object,"create",{value:customCreate});',
'const {apply:change} = Object.defineProperty; change.call(Object.defineProperty,null,[Object,"create",{value:customCreate}]);',
'const change = Reflect.apply.call; change.call(Reflect.apply,null,Object.defineProperty,null,[Object,"create",{value: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);',
])("rejects intrinsic writes through a bound mutator: %s", (write) => {
expect(
analyzeJavaScriptStaticSource(
@@ -633,6 +641,8 @@ describe("Electron module interop bound mutators", () => {
'Object.defineProperty.apply(null,[metadata,"x",{value:1}]);',
'const change = Object.defineProperty.call; change.call(Object.defineProperty,null,metadata,"x",{value:1});',
'const {apply:change} = Object.defineProperty; change.call(Object.defineProperty,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;
const writePoisonedModules = async (root: string): Promise<void> => {
@@ -109,6 +110,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);
@@ -135,7 +137,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",
);