fix(javascript): honor effective symbolic descriptor values (#7222)

Resolve ordered descriptor values at the shared JavaScript fact owner and preserve uncertain intrinsic writes instead of stale exports. Normalize ordinary method values and reuse canonical primitive, prototype, ambient identity and ordering facts across consumers.

384 selected tests, format/static/docs checks, all final CI jobs and reviews pass. Actual Node controls cover 35 bundles; one possibly repeated factory explicitly stays unknown. Shipped Obsidian selected-source complete Evidence matches base/fixed CLI and MCP, with coverage and resource measurements in the PR.

Co-authored-by: gensokyo-dev <[email protected]>
This commit is contained in:
SII-YDDandgensokyo-dev authored and GitHub committed 2026-10-27 03:08:00 +08:00
1 parent d54f5b07b4
commit d4c511ae6e
12 files changed
+1117 -102

No files matched your search

@@ -63,7 +63,7 @@ const needsConstantOptions = (node: t.NewExpression): boolean => {
const lookup = staticObjectProperty(options, "webPreferences");
return (
lookup.status === "explicit" &&
t.isIdentifier(unwrapJavaScriptExpression(lookup.property.value).node)
t.isIdentifier(unwrapJavaScriptExpression(lookup.value).node)
);
};
@@ -83,7 +83,7 @@ export function* finishElectronBrowserWindowsSteps(
const lookup = staticObjectProperty(options, "webPreferences");
return (
lookup.status === "explicit" &&
hasElectronOptionObjectCandidate(lookup.property.value, index)
hasElectronOptionObjectCandidate(lookup.value, index)
);
})
)
@@ -117,7 +117,7 @@ const inspectBrowserWindow = (
const lookup = staticObjectProperty(resolvedOptions, "webPreferences");
const declaredPreferences =
lookup.status === "explicit"
? unwrapJavaScriptExpression(lookup.property.value).node
? unwrapJavaScriptExpression(lookup.value).node
: undefined;
const resolvedPreferences =
(index && resolveElectronOptionObject(declaredPreferences, node, index)) ??
@@ -215,14 +215,14 @@ const collectWebPreferences = (
const effectivePreload = staticObjectProperty(object, "preload");
const preloadPath =
effectivePreload.status === "explicit"
? staticPath(effectivePreload.property.value, pathIntrinsics)
? staticPath(effectivePreload.value, pathIntrinsics)
: undefined;
const preload: ElectronBrowserWindowPreload =
effectivePreload.status === "explicit" && preloadPath !== undefined
? {
preload_path: preloadPath,
preload_resolution_context: staticPathResolutionContext(
effectivePreload.property.value,
effectivePreload.value,
pathIntrinsics,
),
}
@@ -344,8 +344,8 @@ const inspectUtilityProcess = (
// A nullish serviceName selects Electron's default display name.
const declaredName =
serviceNameProperty.status === "explicit" &&
!shapes.isNullish(serviceNameProperty.property.value)
? serviceNameProperty.property.value
!shapes.isNullish(serviceNameProperty.value)
? serviceNameProperty.value
: undefined;
const serviceName =
declaredName === undefined
@@ -5,11 +5,95 @@ import {
semanticMemberChain,
semanticStaticPropertyKey,
staticObjectProperty,
staticObjectPropertyReader,
staticObjectPropertyNames,
} from "./javascriptAstValues.js";
import { SEMANTIC_EXPRESSION_DEPTH_LIMIT } from "./javascriptSemanticResourceLimits.js";
it("copies ordinary members without proving requested methods or getter effects", () => {
it("keeps string spread indices separate from named descriptor fields", () => {
for (const text of ["", "x", "😀", "x".repeat(1_000_000)]) {
const original = t.objectProperty(
t.identifier("value"),
t.identifier("sender"),
);
const object = t.objectExpression([
original,
t.spreadElement(t.stringLiteral(text)),
]);
const read = staticObjectPropertyReader(object);
expect(read("value")).toEqual({
status: "explicit",
property: original,
value: original.value,
});
expect(read("get")).toEqual({ status: "missing" });
expect(read("set")).toEqual({ status: "missing" });
expect(read("0").status).toBe(text.length > 0 ? "dynamic" : "missing");
expect(read("1").status).toBe(text.length > 1 ? "dynamic" : "missing");
for (const key of ["00", "-0", "1.0", "length"])
expect(read(key)).toEqual({ status: "missing" });
// Nonempty strings keep their indexed inventory partial without expanding
// it into character-sized AST nodes or a potentially huge key-name array.
expect(staticObjectPropertyNames(object)).toEqual({
status: text.length > 0 ? "partial" : "complete",
names: ["value"],
});
}
});
it("keeps numeric overwrite order and constant template string spread keys", () => {
const property = t.objectProperty(
t.stringLiteral("0"),
t.identifier("original"),
);
const spread = t.spreadElement(
t.templateLiteral([t.templateElement({ raw: "x", cooked: "x" }, true)], []),
);
expect(
staticObjectProperty(t.objectExpression([property, spread]), "0").status,
).toBe("dynamic");
expect(
staticObjectProperty(t.objectExpression([spread, property]), "0"),
).toEqual({ status: "explicit", property, value: property.value });
});
it("keeps literal array indices, holes and unknown iterable suffixes separate from names", () => {
const value = t.objectProperty(t.identifier("value"), t.identifier("sender"));
for (const elements of [
[],
[null, null],
[t.numericLiteral(1)],
[null, t.numericLiteral(1)],
[null, t.spreadElement(t.identifier("input"))],
]) {
const object = t.objectExpression([
value,
t.spreadElement(t.arrayExpression(elements)),
]);
const read = staticObjectPropertyReader(object);
expect(read("value")).toEqual({
status: "explicit",
property: value,
value: value.value,
});
expect(read("get")).toEqual({ status: "missing" });
expect(read("set")).toEqual({ status: "missing" });
expect(read("0").status).toBe(elements[0] == null ? "missing" : "dynamic");
expect(read("1").status).toBe(elements[1] == null ? "missing" : "dynamic");
expect(read("100").status).toBe(
elements.some((entry) => t.isSpreadElement(entry))
? "dynamic"
: "missing",
);
expect(read("length")).toEqual({ status: "missing" });
expect(staticObjectPropertyNames(object)).toEqual({
status: elements.some((entry) => entry != null) ? "partial" : "complete",
names: ["value"],
});
}
});
it("preserves ordinary method data values without executing getter effects", () => {
const method = (name: string, computed = false) =>
t.objectMethod(
"method",
@@ -33,7 +117,7 @@ it("copies ordinary members without proving requested methods or getter effects"
for (const [member, status] of [
[method("helper"), "explicit"],
[setter, "explicit"],
[method("stdio"), "dynamic"],
[method("stdio"), "explicit"],
[getter, "dynamic"],
[method("key", true), "dynamic"],
] as const) {
@@ -281,3 +365,62 @@ it("shares static member paths and stops before unadmitted path expansion", () =
expect(semanticMemberChain(selected, () => ++admitted < 2)).toBeUndefined();
expect(admitted).toBe(2);
});
it("retains exact ordinary method data nodes while keeping accessors unknown", () => {
const method = t.objectMethod(
"method",
t.identifier("value"),
[],
t.blockStatement([]),
);
const getter = t.objectMethod(
"get",
t.identifier("value"),
[],
t.blockStatement([]),
);
expect(staticObjectProperty(t.objectExpression([method]), "value")).toEqual({
status: "explicit",
property: method,
value: method,
});
expect(staticObjectProperty(t.objectExpression([getter]), "value")).toEqual({
status: "dynamic",
});
});
it("keeps primitive unary results propertyless and typeof results index-only", () => {
const value = t.objectProperty(t.identifier("value"), t.identifier("sender"));
const index = t.objectProperty(
t.stringLiteral("0"),
t.stringLiteral("prior"),
);
for (const operator of ["+", "-", "~", "!", "void", "delete"] as const) {
const object = t.objectExpression([
value,
index,
t.spreadElement(t.unaryExpression(operator, t.numericLiteral(1))),
]);
expect(staticObjectProperty(object, "value")).toEqual({
status: "explicit",
property: value,
value: value.value,
});
expect(staticObjectProperty(object, "0")).toEqual({
status: "explicit",
property: index,
value: index.value,
});
}
const object = t.objectExpression([
value,
index,
t.spreadElement(t.unaryExpression("typeof", t.identifier("input"))),
]);
expect(staticObjectProperty(object, "value")).toEqual({
status: "explicit",
property: value,
value: value.value,
});
expect(staticObjectProperty(object, "0")).toEqual({ status: "dynamic" });
});
+219 -38
View File
@@ -1,6 +1,7 @@
import * as t from "@babel/types";
import type { JavaScriptSemanticPrimitive } from "./javascriptSemanticValueTypes.js";
import { SEMANTIC_EXPRESSION_DEPTH_LIMIT } from "./javascriptSemanticResourceLimits.js";
import { semanticArrayIndex } from "./javascriptSemanticPropertyPaths.js";
/** Type-only class fields neither initialize own slots nor shadow inherited members. */
export const isRuntimeJavaScriptClassProperty = (
@@ -160,25 +161,109 @@ export const isNullishJavaScriptSyntax = (node: t.Node): boolean => {
);
};
export type ObjectDataPropertyValue =
| t.ObjectProperty["value"]
| t.ObjectMethod;
/** Preserve original data-value nodes; accessor reads remain unresolved. */
export const objectDataPropertyValue = (
property: t.ObjectProperty | t.ObjectMethod,
): ObjectDataPropertyValue | undefined =>
t.isObjectProperty(property)
? property.value
: property.kind === "method"
? property
: undefined;
const PROPERTYLESS_UNARY = new Set(["+", "-", "~", "!", "void", "delete"]);
const PROPERTYLESS_BINARY = new Set([
"-",
"*",
"/",
"%",
"**",
"<<",
">>",
">>>",
"|",
"^",
"&",
"==",
"!=",
"===",
"!==",
"<",
"<=",
">",
">=",
"in",
"instanceof",
]);
/** Operator results are primitive even when their value is not statically known. */
const primitiveOwnKeys = (
node: t.Node,
isNullish?: (node: t.Node) => boolean,
): "indices" | "none" | null => {
if (
t.isStringLiteral(node) ||
t.isTemplateLiteral(node) ||
(t.isUnaryExpression(node) && node.operator === "typeof") ||
(t.isBinaryExpression(node) && node.operator === "+")
)
return "indices";
return t.isNumericLiteral(node) ||
t.isBooleanLiteral(node) ||
t.isBigIntLiteral(node) ||
t.isNullLiteral(node) ||
(t.isUnaryExpression(node) && PROPERTYLESS_UNARY.has(node.operator)) ||
t.isUpdateExpression(node) ||
(t.isBinaryExpression(node) && PROPERTYLESS_BINARY.has(node.operator)) ||
isNullish?.(node) === true
? "none"
: null;
};
type StaticObjectProperty =
| { readonly status: "missing" | "dynamic" }
| { readonly status: "explicit"; readonly property: t.ObjectProperty };
| {
readonly status: "explicit";
readonly property: t.ObjectProperty | t.ObjectMethod;
readonly value: ObjectDataPropertyValue;
};
type StaticObjectMember =
| { readonly kind: "unknown" }
| { readonly kind: "prototype-setter" }
| {
readonly kind: "prototype-setter";
readonly prototype: "null" | "ignored" | "unknown";
}
| { readonly kind: "primitive-indices"; readonly length: number | null }
| {
readonly kind: "array-indices";
readonly elements: t.ArrayExpression["elements"];
readonly firstSpread: number;
}
| {
readonly kind: "property";
readonly property: t.ObjectProperty | t.ObjectMethod;
readonly key: string | null;
};
const UNKNOWN_STATIC_OBJECT_MEMBER: StaticObjectMember = { kind: "unknown" };
/** Object-literal prototype setters are never ordinary own-property writes. */
export const isObjectPrototypeSetter = (property: t.ObjectProperty): boolean =>
export const isObjectPrototypeSetter = (
property: t.ObjectProperty | t.ObjectMethod,
key: string | null = semanticStaticPropertyKey(
property.key,
property.computed,
),
): boolean =>
t.isObjectProperty(property) &&
!property.computed &&
!property.shorthand &&
semanticStaticPropertyKey(property.key, false) === "__proto__";
key === "__proto__";
/** Expand only bounded literal spreads, preserving unknown writes in order. */
const appendStaticObjectMembers = (
@@ -189,13 +274,28 @@ const appendStaticObjectMembers = (
): void => {
for (const property of object.properties) {
if (!t.isSpreadElement(property)) {
if (t.isObjectProperty(property) && isObjectPrototypeSetter(property)) {
const key = semanticStaticPropertyKey(property.key, property.computed);
if (
t.isObjectProperty(property) &&
isObjectPrototypeSetter(property, key)
) {
// A top-level setter changes lookup inheritance but is never an own
// key; one copied through object spread has no effect on the target.
if (depth === 0) members.push({ kind: "prototype-setter" });
if (depth === 0) {
const prototype = unwrapJavaScriptExpression(property.value).node;
const primitive = primitiveOwnKeys(prototype, isNullish);
members.push({
kind: "prototype-setter",
prototype: t.isNullLiteral(prototype)
? "null"
: primitive !== null
? "ignored"
: "unknown",
});
}
continue;
}
members.push({ kind: "property", property });
members.push({ kind: "property", property, key });
continue;
}
const { node: spread, depth: wrappers } = unwrapJavaScriptExpression(
@@ -216,12 +316,44 @@ const appendStaticObjectMembers = (
appendStaticObjectMembers(members, spread, nextDepth, isNullish);
continue;
}
// These primitive boxes have no own keys; strings can own index keys.
if (t.isArrayExpression(spread)) {
let firstSpread = -1;
let hasElements = false;
for (let index = 0; index < spread.elements.length; index++) {
const element = spread.elements[index];
if (element == null) continue;
hasElements = true;
if (t.isSpreadElement(element)) {
firstSpread = index;
break;
}
}
// Literal arrays own only present indices. A spread can shift later
// indices, but never adds named keys to the array under construction.
if (hasElements)
members.push({
kind: "array-indices",
elements: spread.elements,
firstSpread,
});
continue;
}
const primitive = primitiveOwnKeys(spread, isNullish);
if (primitive === "indices") {
const literal = readExactJavaScriptLiteral(spread);
const length =
literal.found && typeof literal.value === "string"
? literal.value.length
: null;
// Retain indexed writes without expanding strings or synthesizing properties.
if (length !== 0) members.push({ kind: "primitive-indices", length });
continue;
}
if (
t.isBooleanLiteral(spread) ||
t.isNumericLiteral(spread) ||
t.isBigIntLiteral(spread) ||
isNullish?.(property.argument) === true
primitive === "none" ||
t.isFunctionExpression(spread) ||
t.isArrowFunctionExpression(spread) ||
t.isRegExpLiteral(spread)
)
continue;
members.push(UNKNOWN_STATIC_OBJECT_MEMBER);
@@ -238,31 +370,77 @@ const staticObjectMembers = (
return members;
};
type StaticObjectPropertySource =
| readonly StaticObjectMember[]
| "missing"
| "dynamic";
const staticObjectPropertySource = (
object: t.Node | undefined,
isNullish?: (node: t.Node) => boolean,
): StaticObjectPropertySource =>
object === undefined
? "missing"
: t.isObjectExpression(object)
? staticObjectMembers(object, 0, isNullish)
: "dynamic";
const readStaticObjectProperty = (
source: StaticObjectPropertySource,
name: string,
): StaticObjectProperty => {
if (typeof source === "string") return { status: source };
let prototypeMaySupply = false;
for (let index = source.length - 1; index >= 0; index--) {
const member = source[index]!;
if (member.kind === "prototype-setter") {
prototypeMaySupply ||= member.prototype === "unknown";
continue;
}
if (member.kind === "unknown") return { status: "dynamic" };
if (member.kind === "primitive-indices") {
const index = semanticArrayIndex(name);
if (index !== null && (member.length === null || index < member.length))
return { status: "dynamic" };
continue;
}
if (member.kind === "array-indices") {
const index = semanticArrayIndex(name);
if (
index !== null &&
((member.firstSpread >= 0 && index >= member.firstSpread) ||
(index < member.elements.length && member.elements[index] != null))
)
return { status: "dynamic" };
continue;
}
const { property, key } = member;
if (key === null) return { status: "dynamic" };
if (key !== name) continue;
const value = objectDataPropertyValue(property);
return value === undefined
? { status: "dynamic" }
: { status: "explicit", property, value };
}
return prototypeMaySupply ? { status: "dynamic" } : { status: "missing" };
};
/** Parse ordered members once for related property queries within one boundary. */
export const staticObjectPropertyReader = (
object: t.Node | undefined,
isNullish?: (node: t.Node) => boolean,
): ((name: string) => StaticObjectProperty) => {
const source = staticObjectPropertySource(object, isNullish);
return (name) => readStaticObjectProperty(source, name);
};
/** Find the effective explicit property, preserving unresolved overrides. */
export const staticObjectProperty = (
object: t.Node | undefined,
name: string,
isNullish?: (node: t.Node) => boolean,
): StaticObjectProperty => {
if (object === undefined) return { status: "missing" };
if (!t.isObjectExpression(object)) return { status: "dynamic" };
let prototypeMaySupply = false;
for (const member of staticObjectMembers(object, 0, isNullish).toReversed()) {
if (member.kind === "prototype-setter") {
prototypeMaySupply = true;
continue;
}
if (member.kind === "unknown") return { status: "dynamic" };
const { property } = member;
const key = semanticStaticPropertyKey(property.key, property.computed);
if (key === null) return { status: "dynamic" };
if (key !== name) continue;
return t.isObjectProperty(property)
? { status: "explicit", property }
: { status: "dynamic" };
}
return prototypeMaySupply ? { status: "dynamic" } : { status: "missing" };
};
): StaticObjectProperty =>
readStaticObjectProperty(staticObjectPropertySource(object, isNullish), name);
export type StaticObjectPropertyNames =
| { readonly status: "missing" | "dynamic" }
@@ -280,14 +458,17 @@ export const staticObjectPropertyNames = (
let complete = true;
for (const member of staticObjectMembers(object, 0, isNullish)) {
if (member.kind === "prototype-setter") continue;
if (member.kind === "unknown") {
if (
member.kind === "unknown" ||
member.kind === "primitive-indices" ||
member.kind === "array-indices"
) {
// Indexed spread keys remain unenumerated; never claim a complete name
// inventory or expand the input into a potentially much larger key set.
complete = false;
continue;
}
const key = semanticStaticPropertyKey(
member.property.key,
member.property.computed,
);
const { key } = member;
if (key === null) {
complete = false;
continue;
@@ -305,7 +486,7 @@ export const semanticFinalObjectProperty = (
key: string,
): t.Node | undefined => {
const property = staticObjectProperty(node, key);
return property.status === "explicit" ? property.property.value : undefined;
return property.status === "explicit" ? property.value : undefined;
};
export const semanticFinalObjectStringProperty = (
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { runInNewContext } from "node:vm";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
@@ -787,14 +788,32 @@ it("keeps named spreads unknown without proving nullish aliases", () => {
).toEqual([]);
});
it("skips propertyless literal spreads while retaining string and unknown spreads", () => {
expect(
rpcEndpoints(`
it("preserves named fields across primitive and string spreads while retaining unknown overrides", () => {
const source = `
httpLink({url:"/primitives",...false,...42,...1n,[void effect()]:0});
httpLink({url:"/string",..."ab"});
httpLink({url:"/array",...[1]});
httpLink({url:"/unknown",...options});
`),
).toEqual([
`;
const observed: string[] = [];
runInNewContext(
source,
{
httpLink: (options: { url: string }) => observed.push(options.url),
effect: () => {},
options: { url: "/overwritten" },
},
{ timeout: 1_000 },
);
expect(observed).toEqual([
"/primitives",
"/string",
"/array",
"/overwritten",
]);
expect(rpcEndpoints(source)).toEqual([
{ kind: "network", value: "/primitives", mechanism: "trpc:link:httpLink" },
{ kind: "network", value: "/string", mechanism: "trpc:link:httpLink" },
{ kind: "network", value: "/array", mechanism: "trpc:link:httpLink" },
]);
});
@@ -6,7 +6,11 @@ import {
indexJavaScriptBindingsSteps,
indexJavaScriptBindingsWithReceiversSteps,
} from "./javascriptSemanticAnalysis.js";
import { completeSemanticSteps } from "./javascriptSemanticTraversal.js";
import {
completeSemanticSteps,
traverseJavaScriptAst,
} from "./javascriptSemanticTraversal.js";
import { createGlobalReferenceReader } from "./javascriptSemanticAmbientWrites.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
it("keeps wrapper module aliases distinct from global-object module properties", () => {
@@ -240,3 +244,96 @@ it("indexes unknown-key stores of fresh objects in linear stored-value work", ()
// earlier value from every later store grows at least quadratically.
expect(work(64)).toBeLessThan(3 * work(32));
});
it.each([
[
"before and after a direct replacement",
"inspect(Object.defineProperty);Object.defineProperty=()=>{};inspect(Object.defineProperty);",
["unmodified", "replacement"],
],
[
"an unrelated property",
"Object.assign=()=>{};inspect(Object.defineProperty);",
["unmodified"],
],
[
"a conditional replacement",
"if(flag)Object.defineProperty=()=>{};inspect(Object.defineProperty);",
["unknown"],
],
[
"a replaced parent",
"Object=input;inspect(Object.defineProperty);",
["unknown"],
],
[
"a lexical shadow",
"const Object={defineProperty(){}};inspect(Object.defineProperty);",
["unrelated"],
],
[
"an unproven alias write",
"const copy=Object;copy.defineProperty=()=>{};inspect(Object.defineProperty);",
["unknown"],
],
[
"a later write in a possibly repeated activation",
"function run(){inspect(Object.defineProperty);Object.defineProperty=()=>{};}",
["unknown"],
],
[
"competing unconditional replacements before a read",
"function run(){Object.defineProperty=()=>{};Object.defineProperty=()=>{};inspect(Object.defineProperty);}",
["replacement"],
],
[
"a repeated activation with a later write",
"function run(){inspect(Object.defineProperty);Object.defineProperty=()=>{};}run();run();",
["unknown"],
],
[
"a replaced global namespace",
"globalThis={Object:{}};globalThis.Object.defineProperty=()=>{};inspect(Object.defineProperty);",
["unknown"],
],
[
"a namespaced replacement read directly",
"globalThis.Object.defineProperty=()=>{};inspect(Object.defineProperty);",
["replacement"],
],
[
"a direct replacement read through the namespace",
"Object.defineProperty=()=>{};inspect(globalThis.Object.defineProperty);",
["replacement"],
],
[
"a namespaced global",
"inspect(globalThis.Object.defineProperty);globalThis.Object.defineProperty=()=>{};inspect(globalThis.Object.defineProperty);",
["unmodified", "replacement"],
],
])(
"retains ordered global-reference facts for %s",
(_label, source, expected) => {
const parsed = parseJavaScriptSource(source, "app.cjs");
if (parsed === null) throw new Error("Expected global-reference fixture");
const state = completeSemanticSteps(
indexJavaScriptBindingsSteps(parsed.program),
);
const read = createGlobalReferenceReader(state, [
"Object",
"defineProperty",
]);
const statuses: string[] = [];
traverseJavaScriptAst(parsed.program, {
enter(node) {
if (
t.isCallExpression(node) &&
t.isIdentifier(node.callee, { name: "inspect" }) &&
t.isNode(node.arguments[0])
)
statuses.push(read(node.arguments[0]).status);
},
});
expect(statuses).toEqual(expected);
},
);
@@ -16,12 +16,19 @@ import {
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import { isImportMetaUrl } from "./javascriptStaticAnalysisHelpers.js";
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
import {
semanticBindingAtRead,
semanticEffectTiming,
semanticDefiniteEffectPrecedesReference,
} from "./javascriptSemanticMutationInitializers.js";
import {
semanticIterationSources,
type SemanticIterationSource,
} from "./javascriptSemanticIterationSources.js";
import { joinSemanticPropertyPaths } from "./javascriptSemanticPropertyPaths.js";
import {
joinSemanticPropertyPaths,
semanticPropertyPathKeyMatches,
} from "./javascriptSemanticPropertyPaths.js";
import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js";
import {
semanticCopiedReferencePath,
@@ -789,6 +796,22 @@ export function* indexJavaScriptAmbientWritesSteps(
state.ambientEscapes.retain(["global", null]);
}
/** Match lexical global identity; null denotes a direct global root. */
const globalReferenceNamespace = (
callee: t.Node,
state: JavaScriptSemanticAnalysisState,
path: readonly [string, ...string[]],
): string | null | undefined => {
const { root, members } = electronMemberPath(callee);
const matches = (expected: readonly string[]) =>
members.length === expected.length &&
members.every((key, index) => key === expected[index]);
if (isUnshadowedGlobal(root, state, path[0]) && matches(path.slice(1)))
return null;
const namespace = globalObjectName(root, state);
return namespace !== undefined && matches(path) ? namespace : undefined;
};
/**
* Recognize an ambient property path, optionally through a global object, without a visible
* source-local overwrite. This is a syntax-local identity fact, not proof that
@@ -802,20 +825,118 @@ export const isUnmodifiedGlobalReference = (
const path: readonly [string, ...string[]] =
typeof name === "string" ? [name] : name;
if (state.ambientWrites.covers(["global", ...path])) return false;
const { root, members } = electronMemberPath(callee);
const matches = (expected: readonly string[]) =>
members.length === expected.length &&
members.every((key, index) => key === expected[index]);
if (isUnshadowedGlobal(root, state, path[0]) && matches(path.slice(1)))
return true;
const namespace = globalObjectName(root, state);
const namespace = globalReferenceNamespace(callee, state, path);
return (
namespace !== undefined &&
matches(path) &&
!state.ambientWrites.covers(["global", namespace])
(namespace === null || !state.ambientWrites.covers(["global", namespace]))
);
};
export type GlobalReferenceRead =
| { readonly status: "unmodified" | "unrelated" | "unknown" }
| { readonly status: "replacement"; readonly value: t.Expression };
/** Reuse retained write provenance and activation ordering at a selected read.
* The reader retains only matching effects, refreshing when their owner grows.
* Missing timing, possibly repeated activations, conditional replacements and
* parent writes stay unknown. */
export const createGlobalReferenceReader = (
state: JavaScriptSemanticAnalysisState,
path: readonly [string, ...string[]],
): ((callee: t.Node) => GlobalReferenceRead) => {
const selected = ["global", ...path];
let generation = -1;
const byNamespace = new Map<
string | null,
typeof state.ambientWriteEffects
>();
const matches = (
effect: (typeof state.ambientWriteEffects)[number],
target: readonly string[],
) =>
effect.path.length <= target.length &&
effect.path.every((key, index) =>
semanticPropertyPathKeyMatches(key, target[index]!),
);
return (callee) => {
const namespace = globalReferenceNamespace(callee, state, path);
if (namespace === undefined) return { status: "unrelated" };
if (
!state.ambientWrites.covers(selected) &&
(namespace === null || !state.ambientWrites.covers(["global", namespace]))
)
return { status: "unmodified" };
if (generation !== state.ambientWriteEffects.length) {
generation = state.ambientWriteEffects.length;
byNamespace.clear();
}
let effects = byNamespace.get(namespace);
if (effects === undefined) {
effects = state.ambientWriteEffects.filter(
(effect) =>
matches(effect, selected) ||
(namespace !== null && matches(effect, ["global", namespace])),
);
byNamespace.set(namespace, effects);
}
if (effects.length === 0) return { status: "unknown" };
const active = effects.filter(
(effect) => semanticEffectTiming(callee, effect.node, state) !== "after",
);
if (active.length === 0) return { status: "unmodified" };
if (
active.some(
(effect) =>
effect.path.length !== selected.length ||
!effect.path.every((key, index) => key === selected[index]),
)
)
return { status: "unknown" };
let replacement: t.AssignmentExpression | undefined;
for (const effect of active) {
const write = effect.node;
const writeNamespace = t.isAssignmentExpression(write)
? globalReferenceNamespace(write.left, state, path)
: undefined;
if (
t.isAssignmentExpression(write, { operator: "=" }) &&
writeNamespace !== undefined &&
(writeNamespace === null ||
!state.ambientWrites.covers(["global", writeNamespace])) &&
semanticDefiniteEffectPrecedesReference(
write,
callee,
state.parentsByNode,
) &&
(replacement === undefined ||
semanticDefiniteEffectPrecedesReference(
replacement,
write,
state.parentsByNode,
))
)
replacement = write;
}
if (replacement === undefined) return { status: "unknown" };
const confirmed = replacement;
if (
active.some(
(effect) =>
effect.node !== confirmed &&
semanticEffectTiming(confirmed, effect.node, state) !== "before" &&
(effect.node === undefined ||
!semanticDefiniteEffectPrecedesReference(
effect.node,
confirmed,
state.parentsByNode,
)),
)
)
return { status: "unknown" };
return { status: "replacement", value: confirmed.right };
};
};
/** A module URL base is known only while its property has no visible write. */
export const isUnmodifiedImportMetaUrl = (
node: t.Node | undefined,
@@ -425,7 +425,7 @@ const stdioMode = (
if (lookup.status === "missing")
return unsetStdioMode(expression, method, context);
if (lookup.status !== "explicit") return "dynamic";
const value = unwrapJavaScriptExpression(lookup.property.value).node;
const value = unwrapJavaScriptExpression(lookup.value).node;
if (context.isNullish(value))
return unsetStdioMode(expression, method, context);
if (t.isStringLiteral(value)) return value.value;
@@ -449,7 +449,7 @@ const unsetStdioMode = (
const silent = staticObjectProperty(options, "silent", context.isNullish);
if (silent.status === "missing") return "default";
if (silent.status !== "explicit") return "dynamic";
const value = unwrapJavaScriptExpression(silent.property.value).node;
const value = unwrapJavaScriptExpression(silent.value).node;
if (context.isNullish(value)) return "default";
const truthy = staticJavaScriptTruthiness(value);
if (truthy === undefined) return "dynamic";
@@ -757,3 +757,181 @@ it.each([
expect(resolver.unresolved).toBe(true);
},
);
it.each([
["unknown spread", `{value:require("electron"),...input}`, "unknown"],
["unknown descriptor", `input`, "unknown"],
["negative number spread", `{value:require("electron"),...-1}`, "external"],
["negative bigint spread", `{value:require("electron"),...-1n}`, "external"],
[
"ordinary method value",
`{value(){return require("electron")}}`,
"function",
],
["null prototype", `{__proto__:null,value:require("electron")}`, "external"],
[
"ignored primitive prototype",
`{__proto__:0,value:require("electron")}`,
"external",
],
[
"function spread",
`{value:require("electron"),...function(){}}`,
"external",
],
["regexp spread", `{value:require("electron"),.../x/}`, "external"],
[
"interpolated string spread",
'{value:require("electron"),...`x${input}`}',
"external",
],
["empty array spread", `{value:require("electron"),...[]}`, "external"],
["array spread", `{value:require("electron"),...[1]}`, "external"],
["sparse array spread", `{value:require("electron"),...[,1]}`, "external"],
[
"unknown iterable array spread",
`{value:require("electron"),...[...input]}`,
"external",
],
[
"patched defineProperty",
`{get:()=>replacement}`,
"external",
"Object.defineProperty=()=>{};",
],
[
"literal-return no-op replacement",
`{enumerable:true}`,
"external",
"Object.defineProperty=(_target,_key)=>0;",
],
[
"successive no-op replacements",
`{enumerable:true}`,
"external",
"Object.defineProperty=()=>{};Object.defineProperty=()=>{};",
],
[
"literal stub with effectful parameter binding",
`{enumerable:true}`,
"unknown",
"Object.defineProperty=(target=mutate())=>0;",
],
[
"mixed getter and spread value",
`{get:()=>0,...{value:require("electron")}}`,
"unknown",
],
[
"mixed setter and value",
`{value:require("electron"),set:()=>{}}`,
"unknown",
],
[
"erased descriptor assertion",
`({value:require("electron")} as const)`,
"external",
],
["string spread", `{value:require("electron"),..."x"}`, "external"],
["accessor getter", `{get(){return replacement}}`, "unknown"],
["accessor setter", `{set(value){replacement=value}}`, "unknown"],
["absent accessor", `{get:undefined}`, "unknown"],
["unknown key", `{value:require("electron"),[key]:replacement}`, "unknown"],
[
"getter",
`{value:require("electron"),get value(){return replacement}}`,
"unknown",
],
[
"unknown spread may supply accessors",
`{...input,value:require("electron")}`,
"unknown",
],
[
"known spread before value",
`{...{enumerable:true},value:require("electron")}`,
"external",
],
["null spread", `{value:require("electron"),...null}`, "external"],
["undefined spread", `{value:require("electron"),...undefined}`, "external"],
["void spread", `{value:require("electron"),...void 0}`, "external"],
[
"shadowed undefined",
`{value:require("electron"),...undefined}`,
"unknown",
"const undefined=input;",
],
["unchanged value", `{enumerable:true}`, "external"],
])(
"preserves effective descriptor value uncertainty for %s",
(_name, descriptor, kind, setup = "") => {
const value = inspectOne(`(() => {
const modules = {
1(module,exports) {
${setup}
exports.api = require("electron");
Object.defineProperty(exports,"api",${descriptor});
},
2(module,exports,load) { inspect(load(1).api); }
};
function load(id) {const m={exports:{}};modules[id](m,m.exports,load);return m.exports;}
load(2);
})();`);
expect(value.kind).toBe(kind);
if (kind === "external")
expect(value).toEqual({ kind: "external", path: ["electron"] });
},
);
it("reads a literal prototype only from its actual prototype setter", () => {
expect(
inspectOne('inspect(({ordinary:require("electron")}).__proto__);').kind,
).toBe("unknown");
expect(
inspectOne(
'inspect(({__proto__:require("electron"),ordinary:1}).__proto__);',
),
).toEqual({ kind: "external", path: ["electron"] });
});
it.each(["local", "outer"])(
"does not resurrect an export when the intrinsic changes %s after replacement",
(position) => {
const value = inspectOne(`(() => {
const modules = {
1(module,exports) {
exports.api = require("electron");
Object.defineProperty(exports,"api",{value:null});
${position === "local" ? "Object.defineProperty=()=>{};" : ""}
},
2(module,exports,load) {inspect(load(1).api);}
};
function load(id){const m={exports:{}};modules[id](m,m.exports,load);return m.exports;}
load(2);
${position === "outer" ? "Object.defineProperty=()=>{};" : ""}
})();`);
expect(value.kind).not.toBe("external");
},
);
it("does not preserve a sender through a possibly mutating intrinsic replacement", () => {
const value = inspectOne(`(() => {
const modules={
1(module,exports){exports.api=require("electron");Object.defineProperty=(target,key)=>{target[key]=null;};Object.defineProperty(exports,"api",{value:require("electron")});},
2(module,exports,load){inspect(load(1).api);}
};
function load(id){const m={exports:{}};modules[id](m,m.exports,load);return m.exports;}load(2);
})();`);
expect(value.kind).toBe("unknown");
});
it("keeps a possibly repeated factory's native descriptor value unknown", () => {
const value = inspectOne(`(() => {
const modules={
1(module,exports){exports.api=null;Object.defineProperty(exports,"api",{value:require("electron")});Object.defineProperty=()=>{};},
2(module,exports,load){inspect(load(1).api);}
};
function load(id){const m={exports:{}};modules[id](m,m.exports,load);return m.exports;}load(2);
})();`);
expect(value.kind).toBe("unknown");
});
@@ -1,11 +1,17 @@
import * as t from "@babel/types";
import {
staticObjectPropertyReader,
readExactJavaScriptLiteral,
isObjectPrototypeSetter,
objectDataPropertyValue,
semanticStaticPropertyKey,
semanticWriteSiteTarget,
semanticWriteTargets,
runtimeFunctionParameters,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import { createSemanticExpressionShapeResolver } from "./javascriptSemanticExpressionShapes.js";
import { createGlobalReferenceReader } from "./javascriptSemanticAmbientWrites.js";
import { javascriptBootstrapAt } from "./javascriptBootstrap.js";
import { ELECTRON_MODULE } from "./javascriptElectronMemberWrites.js";
import {
@@ -60,14 +66,6 @@ interface SymbolicReadBoundary {
type SymbolicNamespace = Extract<SymbolicValue, { kind: "namespace" }>;
const emptyEnvironment: SymbolicEnvironment = { values: new Map() };
const isObjectPrototypeSetter = (
property: t.ObjectMethod | t.ObjectProperty,
name: string,
): boolean =>
t.isObjectProperty(property) &&
!property.computed &&
!property.shorthand &&
name === "__proto__";
const containsOwnedReturn = (root: t.Node): boolean => {
let found = false;
@@ -131,6 +129,11 @@ export class JavaScriptSymbolicBindings {
),
) {
this.#electronIdentity = electronIdentity;
const shapes = createSemanticExpressionShapeResolver(state);
const defineProperty = createGlobalReferenceReader(state, [
"Object",
"defineProperty",
]);
this.#indexBindingAliases();
traverseJavaScriptAst(program, {
enter: (node) => {
@@ -141,29 +144,49 @@ export class JavaScriptSymbolicBindings {
enter: (node) => {
const target = semanticWriteSiteTarget(node);
if (target !== null) this.#indexWriteSite(target, node);
if (
t.isCallExpression(node) &&
t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.object, { name: "Object" }) &&
!resolveSemanticBindingState(state, node.callee.object, "Object") &&
semanticStaticPropertyKey(
node.callee.property,
node.callee.computed,
) === "defineProperty"
) {
if (t.isCallExpression(node) && t.isMemberExpression(node.callee)) {
const intrinsic = defineProperty(node.callee);
if (intrinsic.status === "unrelated") return;
const [target, key, descriptor] = node.arguments;
if (
t.isIdentifier(target) &&
t.isStringLiteral(key) &&
t.isObjectExpression(descriptor)
descriptor !== undefined
) {
const value = descriptor.properties.find(
(p) =>
t.isObjectProperty(p) &&
semanticStaticPropertyKey(p.key, p.computed) === "value",
if (intrinsic.status !== "unmodified") {
const replacement =
intrinsic.status === "replacement"
? unwrapJavaScriptExpression(intrinsic.value).node
: undefined;
const unchanged =
(t.isFunctionExpression(replacement) ||
t.isArrowFunctionExpression(replacement)) &&
(t.isBlockStatement(replacement.body)
? replacement.body.body.length === 0
: readExactJavaScriptLiteral(replacement.body).found) &&
runtimeFunctionParameters(replacement).every(
(parameter) =>
t.isIdentifier(parameter) ||
(t.isRestElement(parameter) &&
t.isIdentifier(parameter.argument)),
);
if (!unchanged) this.#addWrite(target, [key.value], node, node);
return;
}
const properties = staticObjectPropertyReader(
unwrapJavaScriptExpression(descriptor).node,
shapes.isNullish,
);
if (t.isObjectProperty(value))
const value = properties("value");
const hasAccessor =
properties("get").status !== "missing" ||
properties("set").status !== "missing";
if (value.status === "explicit" && !hasAccessor)
this.#addWrite(target, [key.value], value.value, node);
else if (value.status !== "missing" || hasAccessor)
// Unknown values, accessor replacements and invalid mixed
// descriptors cannot preserve an earlier data export.
this.#addWrite(target, [key.value], node, node);
}
}
},
@@ -387,7 +410,7 @@ export class JavaScriptSymbolicBindings {
(candidate): candidate is t.ObjectMethod | t.ObjectProperty =>
!t.isSpreadElement(candidate),
)
.find((candidate) => isObjectPrototypeSetter(candidate, key));
.find((candidate) => isObjectPrototypeSetter(candidate));
if (prototypeSetter !== undefined)
return this.#objectPropertyValue(
prototypeSetter,
@@ -431,12 +454,10 @@ export class JavaScriptSymbolicBindings {
property: t.ObjectMethod | t.ObjectProperty,
environment: SymbolicEnvironment,
): SymbolicValue {
return t.isObjectMethod(property) && property.kind !== "method"
const value = objectDataPropertyValue(property);
return value === undefined
? symbolicUnknown("accessor property")
: this.evaluate(
t.isObjectProperty(property) ? property.value : property,
environment,
);
: this.evaluate(value, environment);
}
members(
+3 -2
View File
@@ -6,15 +6,16 @@ import {
isNullishJavaScriptSyntax,
readExactJavaScriptLiteral,
staticObjectProperty,
type ObjectDataPropertyValue,
} from "./javascript/javascriptAstValues.js";
/** Read a statically known own property value from an object expression. */
export const objectValue = (
object: t.ObjectExpression,
name: string,
): t.ObjectProperty["value"] | undefined => {
): ObjectDataPropertyValue | undefined => {
const result = staticObjectProperty(object, name, isNullishJavaScriptSyntax);
return result.status === "explicit" ? result.property.value : undefined;
return result.status === "explicit" ? result.value : undefined;
};
export const objectString = (
+3 -3
View File
@@ -347,7 +347,7 @@ const addWebMcpDeclaration = (
const schemaProperty =
camelSchema.status === "missing" ||
(camelSchema.status === "explicit" &&
isNullishJavaScriptSyntax(camelSchema.property.value))
isNullishJavaScriptSyntax(camelSchema.value))
? staticObjectProperty(
declaration,
"input_schema",
@@ -361,7 +361,7 @@ const addWebMcpDeclaration = (
if (schemaProperty.status === "missing") {
schemaPropertyNamesCoverage = "not_present";
} else if (schemaProperty.status === "explicit") {
const schema = schemaProperty.property.value;
const schema = schemaProperty.value;
if (t.isObjectExpression(schema)) {
const propertiesProperty = staticObjectProperty(
schema,
@@ -371,7 +371,7 @@ const addWebMcpDeclaration = (
if (propertiesProperty.status === "missing") {
schemaPropertyNamesCoverage = "not_present";
} else if (propertiesProperty.status === "explicit") {
const properties = propertiesProperty.property.value;
const properties = propertiesProperty.value;
if (t.isObjectExpression(properties)) {
const projection = staticObjectPropertyNames(
properties,
@@ -1,6 +1,8 @@
import { expect } from "vitest";
import { writeFile } from "node:fs/promises";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { runInNewContext } from "node:vm";
import { stripTypeScriptTypes } from "node:module";
import { z } from "zod";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
@@ -9,6 +11,10 @@ import { cliTest, type TestCli } from "../../support/cli/cliFixture.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { applicationFeatureTraceResultSchema } from "../../../src/domain/javascript/javascriptFeatureTraceSchemas.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
import {
analysisViewResultSchema,
javascriptApplicationSummaryViewResultSchema,
} from "../../../src/domain/analysisView/analysisView.js";
const sourcePositionSchema = z.object({ line: z.number(), column: z.number() });
const callPathConditionSchema = z.object({
@@ -869,3 +875,251 @@ cliTest(
}
},
);
const descriptorSender = `function(channel) { return (...args) => send(channel, ...args); }`;
const descriptorNoop = `function() { return () => {}; }`;
const descriptorCases: readonly (readonly [
string,
string,
boolean,
boolean?,
boolean?,
])[] = [
["control", `{value:${descriptorSender}}`, true],
["accessor-noop", `{get:function(){return ${descriptorNoop}}}`, false],
["mixed-getter", `{get:()=>0,...{value:${descriptorSender}}}`, false, true],
["mixed-setter", `{value:${descriptorSender},...{set:()=>{}}}`, false, true],
["string-spread", `{value:${descriptorSender},..."x"}`, true],
["negative-number", `{value:${descriptorSender},...-1}`, true],
["negative-bigint", `{value:${descriptorSender},...-1n}`, true],
[
"method-value",
"{value" + descriptorSender.slice("function".length) + "}",
true,
],
["patch-after-sender", `{value:${descriptorSender}}`, true, false, false],
["negative-prototype", `{__proto__:-1,value:${descriptorSender}}`, true],
["bigint-prototype", `{__proto__:-1n,value:${descriptorSender}}`, true],
["successive-noops", `{enumerable:true}`, true],
["literal-noop", `{enumerable:true}`, true],
["replaced-namespace", `{value:${descriptorNoop}}`, false],
["namespaced-noop", `{value:${descriptorNoop}}`, true],
["patch-after", `{value:${descriptorNoop}}`, false],
["outer-patch-after", `{value:${descriptorNoop}}`, false],
["mutating-intrinsic", `{value:${descriptorSender}}`, false],
["null-prototype", `{__proto__:null,value:${descriptorSender}}`, true],
["function-spread", `{value:${descriptorSender},...function(){}}`, true],
["regexp-spread", `{value:${descriptorSender},.../x/}`, true],
["template-spread", "{value:" + descriptorSender + ",...`x${1}`}", true],
["empty-array", `{value:${descriptorSender},...[]}`, true],
["array", `{value:${descriptorSender},...[1]}`, true],
["patched-intrinsic", `{get:function(){return ${descriptorNoop}}}`, true],
["typescript-assertion", `({value:${descriptorSender}} as const)`, true],
[
"duplicate-noop",
`{value:${descriptorSender},value:${descriptorNoop}}`,
false,
],
[
"duplicate-sender",
`{value:${descriptorNoop},value:${descriptorSender}}`,
true,
],
[
"spread-sender",
`{value:${descriptorNoop},...{value:${descriptorSender}}}`,
true,
],
[
"spread-noop",
`{value:${descriptorSender},...{value:${descriptorNoop}}}`,
false,
],
[
"computed-sender",
`{value:${descriptorNoop},["value"]:${descriptorSender}}`,
true,
],
["null-spread", `{value:${descriptorSender},...null}`, true],
["undefined-spread", `{value:${descriptorSender},...undefined}`, true],
["void-spread", `{value:${descriptorSender},...void 0}`, true],
["nested-null-spread", `{value:${descriptorSender},...{...null}}`, true],
] as const;
const analyzeDescriptorFacts = async ({
cli,
root,
connection,
}: {
cli: TestCli;
root: string;
connection: Awaited<ReturnType<typeof connectLocalToolsMcp>> | undefined;
}) => {
type ApplicationResult = z.infer<
typeof javascriptApplicationAnalysisResultSchema
>;
type Observation = Pick<
ApplicationResult["graph"]["nodes"][number]["observations"][number],
"properties" | "evidence"
>;
let statistics: ApplicationResult["statistics"];
const observations: Observation[] = [];
if (connection === undefined) {
const application = await analyzeJavaScriptDirectory("CLI", cli, root);
statistics = application.statistics;
for (const node of application.graph.nodes)
observations.push(...node.observations);
} else {
const response = await connection.call("analyze_javascript_application", {
input_path: root,
format: "directory",
detail: "summary",
});
expect(response.isError).not.toBe(true);
const summary = z
.object({
normalized_result: javascriptApplicationSummaryViewResultSchema,
})
.parse(response.structuredContent).normalized_result;
statistics =
javascriptApplicationAnalysisResultSchema.shape.statistics.parse(
summary.summary.statistics,
);
let offset: number | null = 0;
while (offset !== null) {
const response = await connection.call("inspect_analysis_view", {
source: {
kind: "retained-evidence",
evidence_id: summary.parent_evidence_id,
},
view: {
kind: "javascript-facts",
where: { kinds: ["javascript-expression"] },
property_keys: ["step_kind", "terminal", "channel", "resolution"],
offset,
limit: 64,
},
});
expect(response.isError).not.toBe(true);
const page = z
.object({ normalized_result: analysisViewResultSchema })
.parse(response.structuredContent).normalized_result;
if (page.kind !== "javascript-facts")
throw new Error("Expected retained JavaScript facts");
expect(page.parent_evidence_id).toBe(summary.parent_evidence_id);
observations.push(...page.rows);
offset = page.coverage.next_offset;
if (offset === null) {
expect(page.coverage.exhausted).toBe(true);
expect(observations).toHaveLength(page.coverage.total);
}
}
await connection.client.ping();
}
return { statistics, observations };
};
cliTest.for(["CLI", "stdio MCP"] as const)(
"uses effective defineProperty descriptor values for bundled IPC paths through %s",
async (surface, { cli }) => {
const workspace = await createTestTempDirectory("rea-descriptor-values-");
const root = join(workspace, "路径-'values");
const files = Object.fromEntries(
descriptorCases.map(([name, descriptor, sends, throws]) => {
const source = `(() => {
const modules = {
2(module) { module.exports = require("electron"); },
5(module, exports, load) {
const electron = load(2);
${name === "successive-noops" ? "Object.defineProperty=()=>{};Object.defineProperty=()=>{};" : name === "literal-noop" ? "Object.defineProperty=(_target,_key)=>0;" : name === "replaced-namespace" ? "globalThis={Object:{}};globalThis.Object.defineProperty=()=>{};" : name === "namespaced-noop" ? "globalThis.Object.defineProperty=()=>{};" : name === "patched-intrinsic" ? "Object.defineProperty=()=>{};" : name === "mutating-intrinsic" ? "Object.defineProperty=(target,key)=>{target[key]=" + descriptorNoop + ";};" : ""}
exports.sender = ${["accessor-noop", "successive-noops", "literal-noop", "replaced-namespace", "namespaced-noop", "patched-intrinsic", "patch-after", "outer-patch-after", "mutating-intrinsic"].includes(name) ? descriptorSender : descriptorNoop};
Object.defineProperty(exports, "sender", ${descriptor});
${name.startsWith("patch-after") ? "Object.defineProperty=()=>{};" : ""}
function send(channel, ...args) { electron.ipcRenderer.send(channel, ...args); }
},
9(module, exports, load) {
const helper = load(5);
load(2).contextBridge.exposeInMainWorld("${name}", {copy: helper.sender("${name}:copy")});
}
};
function load(id) { const m = {exports:{}}; modules[id].call(m.exports,m,m.exports,load); return m.exports; }
load(9);
${name === "outer-patch-after" ? "Object.defineProperty=()=>{};" : ""}
})();`;
// Node executes only this test-owned fixture with a recording Electron
// substitute: an independent descriptor-semantics oracle, not provider proof.
const channels: string[] = [];
const run = () =>
runInNewContext(
name === "typescript-assertion"
? stripTypeScriptTypes(source)
: source,
{
require: () => ({
ipcRenderer: {
send: (channel: string) => channels.push(channel),
},
contextBridge: {
exposeInMainWorld: (_key: string, api: { copy(): void }) =>
api.copy(),
},
}),
},
{ timeout: 1_000 },
);
if (throws) expect(run).toThrow(/Invalid property descriptor/u);
else run();
expect(channels, name).toEqual(sends ? [`${name}:copy`] : []);
return [
`${name}.${name === "typescript-assertion" ? "cts" : "cjs"}`,
source,
];
}),
);
await writeFixtureFiles(root, files);
const connection =
surface === "stdio MCP" ? await connectLocalToolsMcp() : undefined;
if (connection !== undefined) {
const failure = await connection.call("analyze_javascript_application", {
input_path: join(root, "absent"),
format: "directory",
});
expect(failure.isError).toBe(true);
await connection.client.ping();
}
const { statistics, observations } = await analyzeDescriptorFacts({
cli,
root,
connection,
});
for (const [name, source] of Object.entries(files))
expect(await readFile(join(root, name), "utf8")).toBe(source);
expect(statistics).toMatchObject({
parsed_javascript_files: descriptorCases.length,
parse_failures: 0,
});
const terminals = observations.filter(
({ properties }) =>
properties.step_kind === "terminal" &&
properties.terminal === "electron.ipcRenderer.send",
);
expect(
terminals.map(({ properties }) => properties.channel).sort(),
).toEqual(
descriptorCases
// A factory with a later global patch can run repeatedly: its native
// intrinsic is unknown even though this test invokes it only once.
.filter(([, , sends, , admitted = sends]) => admitted)
.map(([name]) => `${name}:copy`)
.sort(),
);
for (const terminal of terminals)
expect(terminal).toMatchObject({
properties: { resolution: "candidate" },
evidence: {
state: "inferred",
authority: "static-relationship-inference",
},
});
},
);