fix(javascript): retain static paths through TypeScript wrappers (#3998)

Normalize erased TypeScript wrappers through existing expression/string owners for static paths, call targets, collection/configuration receivers and global aliases. Preserve binding/mutation guards and explicit unknown targets, with CLI and application-boundary regressions.

Original contribution by @ShuaixinHuang.

Co-authored-by: ShuaixinHuang <1836656514@qq.com>
This commit is contained in:
ShuaixinHuang
2026-10-16 01:04:24 +08:00
committed by GitHub
parent d67d895c26
commit 633e4ff434
6 changed files with 547 additions and 46 deletions
@@ -300,25 +300,21 @@ function* classifyDefinitionReceiversSteps(
if (client) roots.add(root);
};
const classifyOpenReceiver = (node: t.Node): void => {
if (
(!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) ||
(!t.isMemberExpression(node.callee) &&
!t.isOptionalMemberExpression(node.callee))
)
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) return;
const callee = unwrapJavaScriptExpression(node.callee).node;
if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee))
return;
const method = semanticStaticPropertyKey(
node.callee.property,
node.callee.computed,
);
const object = unwrapJavaScriptExpression(callee.object).node;
const method = semanticStaticPropertyKey(callee.property, callee.computed);
if (method === "get") {
if (isConfigurationReceiver(state, node.callee.object))
if (isConfigurationReceiver(state, object))
facts.set(node.start ?? -1, "configuration");
else if (isExtensionStateMember(node.callee.object))
else if (isExtensionStateMember(object))
extensionStateReads.push(node.start ?? -1);
return;
}
if (method !== "open" || !t.isIdentifier(node.callee.object)) return;
const receiver = node.callee.object.name;
if (method !== "open" || !t.isIdentifier(object)) return;
const receiver = object.name;
const binding = resolveSemanticBindingState(state, node, receiver);
if (binding === undefined) {
if (semanticResolutionBlocked(state, node, receiver)) return;
@@ -338,7 +334,9 @@ function* classifyDefinitionReceiversSteps(
return;
}
if (binding.mutable || binding.initializers.length !== 1) return;
const initializer = binding.initializers[0]?.node;
const initializerSource = binding.initializers[0]?.node;
if (initializerSource === undefined) return;
const initializer = unwrapJavaScriptExpression(initializerSource).node;
if (!t.isIdentifier(initializer)) return;
const initializerBinding = resolveSemanticBindingState(
state,
@@ -357,7 +355,9 @@ function* classifyDefinitionReceiversSteps(
initializerBinding.initializers.length !== 1
)
return;
const nested = initializerBinding.initializers[0]?.node;
const nestedSource = initializerBinding.initializers[0]?.node;
if (nestedSource === undefined) return;
const nested = unwrapJavaScriptExpression(nestedSource).node;
if (t.isIdentifier(nested)) {
const nestedFact = resolveGlobalAliasFact(
state,
@@ -420,16 +420,17 @@ const isConfigurationAccessorCall = (
state: JavaScriptSemanticAnalysisState,
node: t.Node,
): boolean => {
node = unwrapJavaScriptExpression(node).node;
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node))
return false;
const callee = node.callee;
const callee = unwrapJavaScriptExpression(node.callee).node;
if (
(!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee)) ||
semanticStaticPropertyKey(callee.property, callee.computed) !==
"getConfiguration"
)
return false;
const host = callee.object;
const host = unwrapJavaScriptExpression(callee.object).node;
const matchesSpelling = t.isIdentifier(host)
? host.name === "workspace"
: (t.isMemberExpression(host) || t.isOptionalMemberExpression(host)) &&
@@ -438,7 +439,7 @@ const isConfigurationAccessorCall = (
const root = t.isIdentifier(host)
? host
: t.isMemberExpression(host) || t.isOptionalMemberExpression(host)
? host.object
? unwrapJavaScriptExpression(host.object).node
: undefined;
if (!t.isIdentifier(root)) return false;
const binding = resolveSemanticBindingState(state, root, root.name);
@@ -2,6 +2,173 @@ import { describe, expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
const expressionWrappers = [
["plain", (value: string) => value],
["asserted", (value: string) => `(${value} as any)`],
["non-null", (value: string) => `(${value}!)`],
["satisfies", (value: string) => `(${value} satisfies any)`],
] as const;
it("shares erased call-target names with endpoint consumers without inventing dynamic keys", () => {
const analysis = analyzeJavaScriptStaticSource(`
(client.get as typeof client.get)("/api/items");
(navigator as typeof navigator).sendBeacon("/api/telemetry", payload);
(client[method] as typeof client.get)("/api/unknown");
((choose ? client.get : client.post) as typeof client.get)("/api/conditional");
`);
expect(analysis.parse_error_count).toBe(0);
expect(
analysis.endpoints.map(({ value, mechanism }) => [value, mechanism]),
).toEqual([
["/api/items", "call:client.get"],
["/api/telemetry", "call:navigator.sendBeacon"],
]);
});
it.each(expressionWrappers)(
"excludes %s collection and configuration reads while retaining request candidates",
(_label, wrap) => {
const call = (receiver: string, method: string, value: string) =>
`${wrap(`${wrap(receiver)}.${method}`)}(${JSON.stringify(value)});`;
const workspace = `${wrap("vscode")}.workspace`;
const configuration = `${wrap(`${wrap(workspace)}.getConfiguration`)}("editor")`;
const analysis = analyzeJavaScriptStaticSource(`
import * as vscode from "vscode";
const settings = ${wrap(configuration)};
let changed = ${wrap(configuration)};
changed = client;
const uncertain = flag ? ${wrap(configuration)} : client;
${call(`new ${wrap("Headers")}()`, "get", "content-type")}
${call(`new ${wrap("URLSearchParams")}()`, "delete", "q")}
${call(`new ${wrap("Map")}()`, "get", "setting")}
${call(`${wrap("response")}.headers`, "get", "authorization")}
${call(`${wrap("url")}.searchParams`, "delete", "token")}
${call(configuration, "get", "tabSize")}
${call("settings", "get", "wordWrap")}
${call(`${wrap("context")}.globalState`, "get", "lastSession")}
${call(`${wrap("context")}.workspaceState`, "get", "lastFile")}
${call("client", "get", "setting")}
${call(`new ${wrap("Headers")}()`, "get", "/api/header-shaped")}
${call("settings", "get", "https://api.example.test/config-shaped")}
${call("client", "get", "/api/items")}
${call("changed", "get", "changed-setting")}
${call("uncertain", "get", "conditional-setting")}
`);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.parse_status).toBe("complete");
expect(
analysis.endpoints.map(({ value, interpretation }) => [
value,
interpretation ?? null,
]),
).toEqual([
["setting", "candidate"],
["/api/header-shaped", null],
["https://api.example.test/config-shaped", null],
["/api/items", null],
["changed-setting", "candidate"],
["conditional-setting", "candidate"],
]);
},
);
it("retains uncertainty and module evidence for wrapped get receivers", () => {
const analysis = analyzeJavaScriptStaticSource(`
const vscode = otherLibrary;
const settings = vscode.workspace.getConfiguration("editor");
(settings.get as any)("unproven-config");
(context.globalState.get as any)("unproven-state");
((choose ? new Headers() : client).get as any)("uncertain-receiver");
(client[method] as any)("unknown-method");
((choose ? client.get : client.post) as any)("unknown-target");
`);
expect(analysis.parse_error_count).toBe(0);
expect(
analysis.endpoints.map(({ value, interpretation }) => [
value,
interpretation,
]),
).toEqual([
["unproven-config", "candidate"],
["unproven-state", "candidate"],
["uncertain-receiver", "candidate"],
]);
});
it("retains lexical browser-open facts through erased call and receiver wrappers", () => {
const analysis = analyzeJavaScriptStaticSource(`
((window as any).open as any)(method, "browser-context");
function local(window) {
((window as any).open as any)(method, "local-request");
}
`);
expect(analysis.parse_error_count).toBe(0);
expect(
analysis.endpoints.map(({ value, interpretation }) => [
value,
interpretation,
]),
).toEqual([["local-request", "candidate"]]);
});
it.each(expressionWrappers)(
"retains global and local identity through %s alias initializers",
(_label, wrap) => {
const analysis = analyzeJavaScriptStaticSource(`
const browser = ${wrap("window")};
(browser.open as any)(method, "browser-context");
const database = ${wrap("indexedDB")};
(database.open as any)("fixture-db", "2");
const cache = ${wrap("caches")};
(cache.open as any)("fixture-cache");
{
const indexedDB = ${wrap("caches")};
const nested = ${wrap("indexedDB")};
(nested.open as any)("nested-cache");
}
{
const caches = ${wrap("indexedDB")};
const nested = ${wrap("caches")};
(nested.open as any)("nested-db", "2");
}
function local(window, indexedDB, caches) {
const browser = ${wrap("window")};
(browser.open as any)(method, "local-window");
const database = ${wrap("indexedDB")};
(database.open as any)("GET", "/local-db");
const cache = ${wrap("caches")};
(cache.open as any)("GET", "/local-cache");
}
let replaced = ${wrap("window")};
replaced = client;
(replaced.open as any)(method, "changed-window");
const uncertain = ${wrap("flag ? window : client")};
(uncertain.open as any)(method, "uncertain-window");
`);
expect(analysis.parse_error_count).toBe(0);
expect
.soft(
analysis.endpoints.map(({ value, interpretation }) => [
value,
interpretation ?? null,
]),
)
.toEqual([
["local-window", "candidate"],
["/local-db", null],
["/local-cache", null],
["changed-window", "candidate"],
["uncertain-window", "candidate"],
]);
expect(analysis.storage.map(({ kind, name }) => [kind, name])).toEqual([
["indexed-db", "fixture-db"],
["cache-storage", "fixture-cache"],
["cache-storage", "nested-cache"],
["indexed-db", "nested-db"],
]);
},
);
it("retains endpoint observations for exact template and TypeScript member keys", () => {
for (const [get, beacon] of [
['"get"', '"sendBeacon"'],
@@ -180,9 +180,9 @@ export const staticPathResolutionContext = (
/** Resolve a path composed only from inert literal syntax. */
export const staticPath = (node: t.Node): string | undefined => {
if (t.isStringLiteral(node)) return node.value;
if (t.isTemplateLiteral(node) && node.expressions.length === 0)
return stringValue(node);
node = unwrapJavaScriptExpression(node).node;
const value = stringValue(node);
if (value !== undefined) return value;
if (t.isBinaryExpression(node, { operator: "+" }))
return staticConcatenation(node);
if (t.isCallExpression(node) || t.isNewExpression(node))
@@ -200,7 +200,7 @@ const staticConcatenation = (node: t.BinaryExpression): string | undefined => {
let current: t.Node = node;
while (t.isBinaryExpression(current, { operator: "+" })) {
operands.push(current.right);
current = current.left;
current = unwrapJavaScriptExpression(current.left).node;
}
const anchored = isDirectoryPath(current);
if (!anchored) operands.push(current);
@@ -249,8 +249,9 @@ const staticFileUrlPath = (
node: t.CallExpression | t.NewExpression,
): string | undefined => {
if (node.arguments.length !== 1) return undefined;
const argument = argumentNode(node.arguments[0]);
if (argument === undefined) return undefined;
const rawArgument = argumentNode(node.arguments[0]);
if (rawArgument === undefined) return undefined;
const argument = unwrapJavaScriptExpression(rawArgument).node;
if (isSourceFileUrl(argument)) return sourceRelativeFileUrlPath(argument);
const value = staticPath(argument);
const rawPath = value?.split(/[?#]/u, 1)[0] ?? "";
@@ -277,8 +278,9 @@ const staticFileUrlPath = (
};
/** Project a known source-relative URL through the actual file URL decoder. */
const sourceRelativeFileUrlPath = (node: t.Node): string | undefined => {
if (!isSourceFileUrl(node)) return undefined;
const sourceRelativeFileUrlPath = (
node: t.CallExpression | t.NewExpression,
): string | undefined => {
const value = stringValue(node.arguments[0]);
if (
value === undefined ||
@@ -347,7 +349,9 @@ const staticResolvedPath = (
};
const isFilesystemPathExpression = (node: t.Node): boolean => {
if (t.isStringLiteral(node)) return node.value.startsWith("/");
node = unwrapJavaScriptExpression(node).node;
const value = stringValue(node);
if (value !== undefined) return value.startsWith("/");
if (isFileIdentity(node) || isDirectoryIdentity(node)) return true;
if (t.isBinaryExpression(node, { operator: "+" }))
return (
@@ -377,6 +381,7 @@ const isFilesystemPathExpression = (node: t.Node): boolean => {
};
const isDirectoryIdentity = (node: t.Node | undefined): boolean => {
if (node !== undefined) node = unwrapJavaScriptExpression(node).node;
if (isDirectoryPath(node)) return true;
if (!t.isCallExpression(node) && !t.isNewExpression(node)) return false;
const name = calleeName(node.callee);
@@ -388,6 +393,7 @@ const isDirectoryIdentity = (node: t.Node | undefined): boolean => {
/** `__dirname` or the dirname of the source file, as a string. */
const isDirectoryPath = (node: t.Node | undefined): boolean => {
if (node !== undefined) node = unwrapJavaScriptExpression(node).node;
if (t.isIdentifier(node, { name: "__dirname" })) return true;
if (!t.isCallExpression(node)) return false;
const name = calleeName(node.callee);
@@ -413,6 +419,7 @@ export const isImportMetaUrl = (node: t.Node | undefined): boolean => {
};
const isFileIdentity = (node: t.Node | undefined): boolean => {
if (node !== undefined) node = unwrapJavaScriptExpression(node).node;
if (t.isIdentifier(node, { name: "__filename" }) || isImportMetaUrl(node))
return true;
if (!t.isCallExpression(node)) return false;
@@ -560,14 +567,17 @@ const KEYED_COLLECTION_CONSTRUCTORS = new Set([
* binding, so callers keep URL and path literals as requests.
*/
const isKeyedCollectionReceiver = (callee: t.Node): boolean => {
callee = unwrapJavaScriptExpression(callee).node;
if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee))
return false;
const receiver = callee.object;
if (t.isNewExpression(receiver))
const receiver = unwrapJavaScriptExpression(callee.object).node;
if (t.isNewExpression(receiver)) {
const constructor = unwrapJavaScriptExpression(receiver.callee).node;
return (
t.isIdentifier(receiver.callee) &&
KEYED_COLLECTION_CONSTRUCTORS.has(receiver.callee.name)
t.isIdentifier(constructor) &&
KEYED_COLLECTION_CONSTRUCTORS.has(constructor.name)
);
}
if (
!t.isMemberExpression(receiver) &&
!t.isOptionalMemberExpression(receiver)
@@ -669,14 +679,15 @@ export const storageKind = (
return undefined;
};
/** Produce a dotted callee name from member syntax. */
/** Produce a dotted callee name through type-erased expression wrappers. */
export const calleeName = (node: t.Node): string => {
node = unwrapJavaScriptExpression(node).node;
const segments: string[] = [];
while (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
// Dynamic keys retain their syntax identity instead of inventing a name.
segments.push(memberPropertyName(node));
if (!t.isNode(node.object)) break;
node = node.object;
node = unwrapJavaScriptExpression(node.object).node;
}
const base = t.isIdentifier(node)
? node.name
@@ -5,6 +5,7 @@ import { parseExpression } from "@babel/parser";
import { describe, expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { parserPluginsForPath } from "./javascriptSourceParser.js";
import {
staticPath,
staticPathResolutionContext,
@@ -46,8 +47,13 @@ const anchoredExpressions = [
] as const;
const unknownExpressions = [
"runtimePath as string",
"path.join(__dirname as string, runtimeFile as string) as string",
'path[method](__dirname, "preload.js")',
'path[`join${suffix}`](__dirname, "preload.js")',
'(path[method] as typeof path.join)(__dirname, "preload.js")',
'((useJoin ? path.join : path.resolve) as typeof path.join)(__dirname, "preload.js")',
'fileURLToPath(new (runtimeURL as typeof URL)("./preload.js", import.meta.url))',
"path.resolve()",
"path.posix.resolve()",
'path.resolve("sub", "../preload.js")',
@@ -69,7 +75,101 @@ const unknownExpressions = [
'path.dirname(__filename) + "preload.js"',
];
const pathValueExpressions = [
['"./preload.js" as const', "./preload.js", "module-specifier"],
[
'path.join(__dirname, "preload.js") as string',
"preload.js",
"filesystem-expression",
],
[
'path.join(__dirname as string, "preload.js" as const)',
"preload.js",
"filesystem-expression",
],
[
'path.resolve(__dirname as string, "preload.js" as const)',
"preload.js",
"filesystem-expression",
],
[
'fileURLToPath(new URL("./preload.js", import.meta.url as string) as URL) as string',
"./preload.js",
"filesystem-expression",
],
[
'path.join(path.dirname(__filename as string), "preload.js")',
"preload.js",
"filesystem-expression",
],
[
'path.join(path.dirname(__filename) as string, "preload.js")',
"preload.js",
"filesystem-expression",
],
[
'((__dirname + "/lib") as string) + "/preload.js"',
"lib/preload.js",
"filesystem-expression",
],
['"/app/preload.js"', "/app/preload.js", "filesystem-expression"],
["`/app/preload.js`", "/app/preload.js", "filesystem-expression"],
[
'path.join(`/app`, "preload.js")',
"/app/preload.js",
"filesystem-expression",
],
["`./preload.js`", "./preload.js", "module-specifier"],
] as const;
const pathCallTargetExpressions = [
['(path.join as typeof path.join)(__dirname, "preload.js")', "preload.js"],
['path.join!(__dirname, "preload.js")', "preload.js"],
[
'(path.resolve satisfies typeof path.resolve)(__dirname, "preload.js")',
"preload.js",
],
[
'((path as typeof path).posix as typeof path.posix).resolve("/app", "preload.js")',
"/app/preload.js",
],
[
'(fileURLToPath as typeof fileURLToPath)(new URL("./preload.js", import.meta.url))',
"./preload.js",
],
[
'fileURLToPath(new (URL as typeof URL)("./preload.js", import.meta.url))',
"./preload.js",
],
[
'fileURLToPath(new (url.URL as typeof URL)("./preload.js", import.meta.url))',
"./preload.js",
],
[
'path.join((path.dirname as typeof path.dirname)(__filename), "preload.js")',
"preload.js",
],
[
'path.join(dirname((fileURLToPath as typeof fileURLToPath)(import.meta.url)), "preload.js")',
"preload.js",
],
] as const;
describe("inert JavaScript path resolution", () => {
it.each(pathCallTargetExpressions)(
"preserves exact path values and context through call target %s",
(expression, path) => {
assertPathProjection(expression, path, "filesystem-expression");
},
);
it.each(pathValueExpressions)(
"preserves exact path values and context through %s",
(expression, path, context) => {
assertPathProjection(expression, path, context);
},
);
it.each(absoluteParts)(
"matches the POSIX resolve oracle for %j",
(...parts) => {
@@ -141,9 +241,7 @@ describe("inert JavaScript path resolution", () => {
});
it.each(unknownExpressions)("keeps unsupported %s unknown", (expression) => {
expect(
staticPath(parseExpression(expression, { sourceType: "module" })),
).toBeUndefined();
expect(staticPath(parsePathExpression(expression))).toBeUndefined();
const { analysis, windowExpression, utilityExpression } =
analyzePathConsumers(expression);
expect(analysis.parse_status).toBe("partial");
@@ -210,9 +308,7 @@ describe("inert JavaScript path resolution", () => {
'path.join("/app", runtimeFile)',
"path.join(...parts)",
])("preserves unresolved join syntax in %s", (expression) =>
expect(
staticPath(parseExpression(expression, { sourceType: "module" })),
).toBeUndefined(),
expect(staticPath(parsePathExpression(expression))).toBeUndefined(),
);
});
@@ -237,11 +333,15 @@ it.each([
'fileURLToPath("file:///app/preload.js", options)',
"fileURLToPath(selectedUrl)",
])("leaves platform-dependent or invalid %s unknown", (expression) =>
expect(
staticPath(parseExpression(expression, { sourceType: "module" })),
).toBeUndefined(),
expect(staticPath(parsePathExpression(expression))).toBeUndefined(),
);
const parsePathExpression = (expression: string) =>
parseExpression(expression, {
sourceType: "module",
plugins: parserPluginsForPath("preload.ts"),
});
const analyzePathConsumers = (expression: string) => {
const property = `preload: ${expression}`;
const genericPrefix = "const options = { ";
@@ -265,7 +365,7 @@ const assertPathProjection = (
path: string,
context: JavaScriptStaticPathContext,
) => {
const node = parseExpression(expression, { sourceType: "module" });
const node = parsePathExpression(expression);
expect(staticPath(node)).toBe(path);
expect(staticPathResolutionContext(node)).toBe(context);
const {
@@ -13,8 +13,25 @@ const execute = promisify(execFile);
it("reports a candidate endpoint through the worker boundary in the public CLI", async () => {
const root = await createTestTempDirectory("rea-endpoint-candidate-cli-");
// A method-like call whose first argument is neither a URL nor a path.
await writeFile(join(root, "main.js"), 'client.post("session");\n');
// Retain ambiguous requests while excluding type-erased collection/config reads.
await writeFile(
join(root, "main.ts"),
`
import * as vscode from "vscode";
client.post("session");
(client.get as any)("/api/items");
(new Headers().get as any)("content-type");
(new URLSearchParams() as any).delete("q");
((vscode.workspace.getConfiguration("editor") as any).get as any)("tabSize");
(context.globalState.get as any)("lastSession");
const browser = window as Window;
(browser.open as any)(method, "browser-context");
const database = indexedDB as IDBFactory;
(database.open as any)("fixture-db", "2");
const cache = caches as CacheStorage;
(cache.open as any)("fixture-cache");
`,
);
const { stdout } = await execute(
process.execPath,
["scripts/rea.mjs", "analyze-javascript-application", root, "--json"],
@@ -23,7 +40,32 @@ it("reports a candidate endpoint through the worker boundary in the public CLI",
const result = javascriptApplicationAnalysisResultSchema.parse(
parseEvidence(JSON.parse(stdout)).normalized_result,
);
const endpoint = result.graph.nodes.find(({ kind }) => kind === "endpoint");
expect
.soft(
result.graph.nodes
.filter(({ kind }) => kind === "endpoint")
.flatMap(({ observations }) =>
observations.map(({ properties }) => properties.value),
)
.sort(),
)
.toEqual(["/api/items", "session"]);
expect(
result.graph.nodes
.filter(({ kind }) => kind === "storage")
.flatMap(({ observations }) =>
observations.map(
({ properties }) =>
`${String(properties.storage_kind)}:${String(properties.name)}`,
),
)
.sort(),
).toEqual(["cache-storage:fixture-cache", "indexed-db:fixture-db"]);
const endpoint = result.graph.nodes.find(
({ kind, observations }) =>
kind === "endpoint" &&
observations.some(({ properties }) => properties.value === "session"),
);
expect(endpoint?.observations[0]).toMatchObject({
properties: {
endpoint_kind: "network",
@@ -125,6 +125,186 @@ it("resolves erased wrappers through the composed application graph", async () =
expect(parseJavaScriptSource(source, "app.mts")?.errors).toEqual([]);
});
it.each([
{
name: "path-value wrappers",
plain: ['"./preload.js"', '"./worker.js"', '"kept"'],
wrapped: [
'("./preload.js" as const)',
'("./worker.js" as const)',
'("kept" as const)',
],
preloadPath: "./preload.js",
workerPath: "./worker.js",
context: "module-specifier",
},
{
name: "path-call target wrappers",
plain: [
'path.join(__dirname, "preload.js")',
'fileURLToPath(new URL("./worker.js", import.meta.url))',
'"kept"',
],
wrapped: [
'(path.join as typeof path.join)(__dirname, "preload.js")',
'(fileURLToPath as typeof fileURLToPath)(new (URL as typeof URL)("./worker.js", import.meta.url))',
'("kept" as const)',
],
preloadPath: "preload.js",
workerPath: "./worker.js",
context: "filesystem-expression",
},
])(
"retains Electron entrypoint facts through erased TypeScript $name",
async ({
plain: plainExpressions,
wrapped: wrappedExpressions,
preloadPath,
workerPath,
context,
}) => {
const applications: ApplicationAnalysis[] = [];
for (const [preload, worker, serviceName] of [
plainExpressions,
wrappedExpressions,
]) {
applications.push(
await analyzeFiles(
new Map([
["package.json", '{"main":"input.ts","type":"commonjs"}'],
[
"input.ts",
`const { BrowserWindow, utilityProcess } = require("electron");
const path = require("node:path");
const { fileURLToPath } = require("node:url");
new BrowserWindow({ webPreferences: { preload: ${preload} } });
utilityProcess.fork(${worker}, [], { serviceName: ${serviceName} });`,
],
["preload.js", "void 'preload';\n"],
["worker.js", "void 'worker';\n"],
]),
),
);
}
const entrypointFacts = (analysis: ApplicationAnalysis) => {
const nodes = new Map(
analysis.graph.nodes.map((node) => [node.node_id, node]),
);
const windows = analysis.graph.nodes.filter(
({ kind }) => kind === "browser-window",
);
const utilities = analysis.graph.nodes.filter(
({ kind }) => kind === "electron-utility",
);
const windowIds = new Set(windows.map(({ node_id }) => node_id));
const preloadLoads = analysis.graph.edges.filter(
({ source_node_id, relation }) =>
windowIds.has(source_node_id) && relation === "loads",
);
const entrypointIds = new Set([
...preloadLoads.map(({ target_node_id }) => target_node_id),
...utilities.map(({ node_id }) => node_id),
]);
// Several provenance edges can identify the same entrypoint asset.
const assetMappings = new Map(
analysis.graph.edges
.filter(
({ source_node_id, relation }) =>
entrypointIds.has(source_node_id) && relation === "maps_to",
)
.map(({ source_node_id, target_node_id, properties }) => {
const identity = [
nodes.get(source_node_id)?.kind,
properties.resolved_path,
properties.resolution_status,
nodes
.get(target_node_id)
?.observations.find(
({ properties: target }) => typeof target.path === "string",
)?.properties.path,
];
return [
canonicalJson(identity, "Electron entrypoint asset identity"),
identity,
] as const;
}),
);
return {
summary: analysis.summary,
windows: windows.flatMap(({ observations }) =>
observations.map(({ properties }) => properties),
),
utilities: utilities.flatMap(({ observations }) =>
observations
.filter(
({ evidence }) =>
evidence.extractor.operation === "observe-utility-process-fork",
)
.map(({ properties }) => properties),
),
preloadLoads: preloadLoads.map(({ properties }) => properties),
assetMappings: [...assetMappings]
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
.map(([, identity]) => identity),
};
};
const [plain, wrapped] = applications.map(entrypointFacts);
expect(plain).toMatchObject({
summary: {
browser_windows: 1,
explicit_web_preferences: 1,
preload_entrypoints: 1,
utility_processes: 1,
resolved_utility_entrypoints: 1,
},
windows: [
{
source_path: "input.ts",
options_status: "object-literal",
web_preferences_status: "object-literal",
web_preferences: [
{
name: "preload",
value: {
status: "literal",
value: preloadPath,
expression: null,
},
},
],
preload_path: preloadPath,
preload_resolution_context: context,
absence_means_default: false,
},
],
utilities: [
{
module_path: workerPath,
module_resolution_context: context,
module_expression: null,
service_name: "kept",
},
],
preloadLoads: [
{
declared_path: preloadPath,
resolution_context: context,
resolved_path: "preload.js",
resolution_status: "resolved",
limitations: [],
},
],
assetMappings: [
["electron-preload", "preload.js", "resolved", "preload.js"],
["electron-utility", "worker.js", "resolved", "worker.js"],
],
});
expect(wrapped).toEqual(plain);
},
);
it("applies TypeScript .mts ambiguity rules while retaining unambiguous generics", () => {
expect(
parseJavaScriptSource(