fix(javascript): preserve loader authority for exact suffixes (#9477)

* fix(javascript): require exact loader suffix case

* fix(javascript): share exact loader mode across import resolution

* test(process): align symlink fixture with filesystem checkpoints

* test(javascript): normalize the module resolution oracle path separators

* test(frida): assert the CLI spawn Evidence contract

* test(mcp): use an unsupported analysis-view parent

* test(browser): separate source-location line bounds from parity

* fix(javascript): preserve uncertain loader provenance

* test(javascript): cover uncertain require resolution

* fix(javascript): retain conditional Electron evidence

* fix(javascript): preserve timed semantic provenance

* fix(javascript): retain conditional builtin effects

* test(javascript): distinguish bundled host authority

* fix(javascript): preserve nested loader invocation identity

* test(javascript): qualify Electron fixture authority

* fix(javascript): preserve loader invocation authority

* fix(javascript): respect source order for resolver escapes

* fix(javascript): check nested loader callable mutations

* fix(javascript): retain conditional constructor requests

* fix(reference): retain constructor loader relationships

* fix(reference): follow nested loader callable paths

* refactor(javascript): share callable mutation interpretation

* fix(javascript): require exact package loader suffixes

* refactor(javascript): unify conditional relation evidence

* refactor(javascript): unify conditional node evidence

* fix(javascript): retain unknown authority for mixed case sources

* fix(javascript): preserve loader uncertainty for dotfiles

* fix(reference): classify javascript suffix dotfiles

* test(reference): cover suffix dotfiles through cli

* fix(javascript): retain equivalent conditional built-in origins

* fix(javascript): preserve wrapped resolver operation

* fix(javascript): retain nested conditional built-in receivers

* test(javascript): declare CommonJS in native addon fixtures

* fix(javascript): retain wrapped resolver candidates

* style: format merged native command schema

* fix(javascript): preserve uncertain loader authority

* test(javascript): require candidate preload evidence

* test(javascript): cover reference loader member writes

---------

Co-authored-by: ShuaixinHuang <1836656514@qq.com>
This commit is contained in:
morlutoandShuaixinHuang authored and GitHub committed 2026-11-09 13:21:24 +08:00
1 parent 78a84d75c0
commit b46f4fcebd
112 files changed
+5156 -797

No files matched your search

@@ -1,7 +1,8 @@
import { describe, expect, it } from "vitest";
import { runInNewContext } from "node:vm";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { analyzeJavaScriptStaticSource as analyzeUnknownHostSource } from "./javascriptStaticAnalysis.js";
import { analyzeCommonJsStaticFixture as analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
// Webpack 5 standalone bootstrap with an Electron external, as emitted for
// `target: "electron-main"`.
@@ -31,6 +32,17 @@ const expectUnresolved = (source: string) => {
).toEqual([]);
};
it("does not claim Electron host identity from an unbound require in unknown context", () => {
const analysis = analyzeUnknownHostSource(bootstrap(EXTERNAL, CONSUMER));
expect(analysis.electron.browser_windows).toContainEqual(
expect.objectContaining({
preload_path: "./preload.js",
loader_authority: "candidate",
}),
);
expect(analysis.analysis_coverage?.applicability.electron).toBe("unknown");
});
describe("Electron identity through Webpack externals", () => {
it.each([
["const key=45840;n.m[key]=local;", 0],
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { analyzeJavaScriptStaticSource as analyzeUnknownHostSource } from "./javascriptStaticAnalysis.js";
import { analyzeCommonJsStaticFixture as analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
import { ROLLDOWN_MODULE_INTEROP } from "./javascriptModuleInterop.fixture.js";
const WINDOW =
@@ -20,6 +21,13 @@ const BLOCK_MODULE_INTEROP = ROLLDOWN_MODULE_INTEROP.replace(
.replace(" });", " });\n }")
.replace(" return to;", " }\n return to;");
it("does not claim Electron interop identity when wrapper authority is unknown", () => {
expect(
analyzeUnknownHostSource(`${ROLLDOWN_MODULE_INTEROP}
${IMPORT} ${WRAP} ${WINDOW}`).electron.browser_windows,
).toEqual([]);
});
describe("Electron module interop identity", () => {
it("preserves named APIs through the released Rolldown namespace conversion", () => {
const analysis = analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}
@@ -64,6 +64,32 @@ describe("native addon type-only re-exports", () => {
});
});
it("does not claim a native re-export from a candidate wrapper with a proven factory loader", () => {
const analysis = analyzeJavaScriptStaticSource(`
globalThis.webpackChunkApp.push([[1], {
"factory": function (m, e, load) {
const known = load("./known.node");
module.exports = load("./addon.node");
}
}]);
`);
expect(analysis.electron.native_addon_bindings).toContainEqual(
expect.objectContaining({
binding_kind: "require",
specifier: "./known.node",
}),
);
expect(analysis.electron.native_addon_bindings).not.toContainEqual(
expect.objectContaining({
binding_kind: "re-export",
specifier: "./addon.node",
}),
);
expect(
analysis.analysis_coverage?.semantics.unknown_findings,
).toBeGreaterThan(0);
});
describe("native addon loader provenance", () => {
it("does not give ESM fixtures an ambient CommonJS loader", () => {
const analysis = analyzeJavaScriptStaticSource(`
@@ -126,3 +152,28 @@ describe("native addon loader provenance", () => {
},
);
});
it.each(["call", "apply"])(
"does not prove native bindings through a replaced require.%s",
(method) => {
const argument =
method === "call" ? 'null,"./addon.node"' : 'null,["./addon.node"]';
const replaced = analyzeCommonJsStaticFixture(
`require.${method}=custom;const addon=require.${method}(${argument});`,
);
expect(replaced.electron.native_addon_bindings).toEqual([]);
const intact = analyzeCommonJsStaticFixture(
`require.other=custom;const addon=require.${method}(${argument});`,
);
expect(intact.electron.native_addon_bindings).toMatchObject([
{ specifier: "./addon.node", binding_kind: "require" },
]);
},
);
it("does not prove native bindings through a replaced aliased loader member", () => {
const analysis = analyzeCommonJsStaticFixture(
'const load=require;load.call=custom;const addon=load.call(null,"./addon.node");',
);
expect(analysis.electron.native_addon_bindings).toEqual([]);
});
@@ -5,7 +5,10 @@ import { inspectElectronIpcNode } from "./electronStaticAnalysisIpc.js";
import { inspectElectronNativeNode } from "./electronStaticAnalysisNative.js";
import { inspectElectronProtocolNode } from "./electronStaticAnalysisProtocol.js";
import type { JavaScriptFindingContext } from "./javascriptStaticAnalysisState.js";
import { electronCalleePath } from "./electronStaticAnalysisValues.js";
import {
electronCandidateAuthority,
electronCalleePath,
} from "./electronStaticAnalysisValues.js";
import { addLocatedFinding } from "./javascriptStaticAnalysisFindings.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
@@ -22,6 +25,11 @@ export const inspectElectronStaticNode = (
? (electronCalleePath(node.callee, context) ??
context.electronInvocationPath?.(node))
: undefined;
const provenAppPathChange =
context.electronAppPathChangePossible?.(node) === true;
const candidateAppPathChange =
!provenAppPathChange &&
context.electronCandidateAppPathChangePossible?.(node) === true;
if (
(path !== undefined &&
path[0] === "app" &&
@@ -29,7 +37,8 @@ export const inspectElectronStaticNode = (
path
.slice(2)
.every((member) => ["call", "apply", "bind"].includes(member))) ||
context.electronAppPathChangePossible?.(node) === true
provenAppPathChange ||
candidateAppPathChange
)
// A bound or exported setter may escape this source before it changes the
// shared app path. Keep that possibility without claiming an invocation.
@@ -37,7 +46,16 @@ export const inspectElectronStaticNode = (
collection: context.accumulator.appPathChanges,
key: "electron:app-path-change",
node,
value: { module_key: null, location: range(node) },
value: {
...(candidateAppPathChange
? { loader_authority: "candidate" as const }
: {}),
...(t.isCallExpression(node) || t.isOptionalCallExpression(node)
? electronCandidateAuthority(node.callee, context)
: {}),
module_key: null,
location: range(node),
},
});
inspectElectronProtocolNode(node, context);
};
@@ -13,6 +13,7 @@ import { contextBridgeExposureArguments } from "./javascriptContextBridgeExposur
import {
boundedExpression,
collectContextBridgeMembers,
electronCandidateAuthority,
electronCalleePath,
electronStaticValue,
} from "./electronStaticAnalysisValues.js";
@@ -139,6 +140,7 @@ const inspectBrowserWindow = (
);
context.accumulator.unknownFindings += collected.unknown;
const finding: ElectronBrowserWindowFinding = {
...electronCandidateAuthority(node.callee, context),
options_status:
resolvedOptions === undefined
? "missing"
@@ -155,6 +157,7 @@ const inspectBrowserWindow = (
location: range(node),
};
if (
finding.loader_authority !== "candidate" &&
finding.preload_path !== null &&
finding.preload_resolution_context !== null
)
@@ -305,6 +308,7 @@ const inspectContextBridge = (
? { api_key: key.value, api_key_expression: null }
: { api_key: null, api_key_expression: null };
const finding: ElectronContextBridgeFinding = {
...electronCandidateAuthority(node.callee, context),
...apiKey,
world: exposure.world,
world_id: worldId,
@@ -373,6 +377,7 @@ const inspectUtilityProcess = (
module_expression: null,
};
const finding: ElectronUtilityProcessFinding = {
...electronCandidateAuthority(node.callee, context),
...moduleReference,
service_name: serviceName ?? null,
module_key: null,
@@ -0,0 +1,141 @@
import { describe, expect, it } from "vitest";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import { analyzeParsedJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
const analyze = (source: string, path: string) => {
const parsed = parseJavaScriptSource(source, path);
if (parsed === null) throw new Error("fixture did not parse");
return analyzeParsedJavaScriptStaticSource(source, parsed);
};
describe("conditional Electron identities", () => {
const source = `
const electron = require("electron");
const { BrowserWindow, protocol } = electron;
new BrowserWindow({ webPreferences: { sandbox: false } });
protocol.handle("app", handler);
`;
it("retains alias-derived findings without proving an unknown loader", () => {
const result = analyze(source, "main.MJS");
expect(result.analysis_coverage?.applicability.electron).toBe("unknown");
expect(result.electron.browser_windows).toMatchObject([
{
loader_authority: "candidate",
web_preferences: [
{ name: "sandbox", value: { status: "literal", value: false } },
],
},
]);
expect(result.electron.protocols).toMatchObject([
{
loader_authority: "candidate",
api: "handle",
scheme: { status: "literal", value: "app" },
},
]);
});
it("retains established CommonJS and ES module observations as proven", () => {
const commonjs = analyze(source, "main.cjs");
expect(commonjs.analysis_coverage?.applicability.electron).toBe("observed");
expect(
commonjs.electron.browser_windows[0]?.loader_authority,
).toBeUndefined();
const esm = analyze(
'import { BrowserWindow } from "electron"; new BrowserWindow({});',
"main.mjs",
);
expect(esm.analysis_coverage?.applicability.electron).toBe("observed");
expect(esm.electron.browser_windows[0]?.loader_authority).toBeUndefined();
});
it("retains optional loader findings with the host's authority", () => {
const optional =
'const {app, BrowserWindow} = require?.("electron"); app.setAppPath("/tmp"); new BrowserWindow();';
const candidate = analyze(optional, "main.MJS");
expect(candidate.analysis_coverage?.applicability.electron).toBe("unknown");
expect(candidate.electron.browser_windows).toMatchObject([
{ loader_authority: "candidate" },
]);
expect(candidate.electron.app_path_changes).toMatchObject([
{ loader_authority: "candidate" },
]);
const commonjs = analyze(optional, "main.cjs");
expect(commonjs.electron.browser_windows).toHaveLength(1);
expect(
commonjs.electron.browser_windows[0]?.loader_authority,
).toBeUndefined();
expect(commonjs.electron.app_path_changes).toHaveLength(1);
expect(
commonjs.electron.app_path_changes?.[0]?.loader_authority,
).toBeUndefined();
});
it("does not claim conditional identity when initialization is skipped or loader is shadowed", () => {
const skipped = analyze(
'while (false) { var electron = require("electron"); } new electron.BrowserWindow({});',
"main.MJS",
);
expect(skipped.electron.browser_windows).toEqual([]);
const shadowed = analyze(
'const require = () => ({ BrowserWindow: Local }); const { BrowserWindow } = require("electron"); new BrowserWindow({});',
"main.MJS",
);
expect(shadowed.electron.browser_windows).toEqual([]);
});
it("does not use a later candidate initializer at an earlier read", () => {
const early = analyze(
'new electron.BrowserWindow({}); var electron = require("electron");',
"main.MJS",
);
expect(early.electron.browser_windows).toEqual([]);
const later = analyze(
'var electron = require("electron"); new electron.BrowserWindow({});',
"main.MJS",
);
expect(later.electron.browser_windows).toMatchObject([
{ loader_authority: "candidate" },
]);
const aliasCapturedEarly = analyze(
'var BrowserWindow = electron.BrowserWindow; var electron = require("electron"); new BrowserWindow({});',
"main.MJS",
);
expect(aliasCapturedEarly.electron.browser_windows).toEqual([]);
const aliasCapturedLater = analyze(
'var electron = require("electron"); var BrowserWindow = electron.BrowserWindow; new BrowserWindow({});',
"main.MJS",
);
expect(aliasCapturedLater.electron.browser_windows).toMatchObject([
{ loader_authority: "candidate" },
]);
});
it("does not use a later proven CommonJS initializer at an earlier read", () => {
const early = analyze(
'new electron.BrowserWindow({}); var electron = require("electron");',
"main.cjs",
);
expect(early.electron.browser_windows).toEqual([]);
const later = analyze(
'var electron = require("electron"); new electron.BrowserWindow({});',
"main.cjs",
);
expect(later.electron.browser_windows).toHaveLength(1);
expect(later.electron.browser_windows[0]?.loader_authority).toBeUndefined();
const aliasEarly = analyze(
'var BrowserWindow = electron.BrowserWindow; var electron = require("electron"); new BrowserWindow({});',
"main.cjs",
);
expect(aliasEarly.electron.browser_windows).toEqual([]);
const aliasLater = analyze(
'var electron = require("electron"); var BrowserWindow = electron.BrowserWindow; new BrowserWindow({});',
"main.cjs",
);
expect(aliasLater.electron.browser_windows).toHaveLength(1);
expect(
aliasLater.electron.browser_windows[0]?.loader_authority,
).toBeUndefined();
});
});
@@ -14,8 +14,13 @@ describe("computed Electron web preference keys", () => {
} });
`);
expect(analysis.parse_status).toBe("complete");
expect(analysis.parse_status).toBe("partial");
expect(analysis.analysis_coverage?.syntax).toEqual({
status: "complete",
error_count: 0,
});
expect(analysis.electron.browser_windows[0]).toMatchObject({
loader_authority: "candidate",
preload_path: "./preload.js",
web_preferences: [
{ name: "", value: { status: "literal", value: false } },
@@ -1,6 +1,7 @@
import { expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
it.each([
"module.require = replacement;",
@@ -20,7 +21,11 @@ it.each([
);
it.each([
['const { app } = require("electron"); app.setAppPath("./other");', true],
[
'const { app } = require("electron"); app.setAppPath("./other");',
true,
true,
],
[
'import { app as runtime } from "electron/main"; runtime.setAppPath(path);',
true,
@@ -32,14 +37,17 @@ it.each([
[
'const { app } = require("electron"); const runtime = app; runtime.setAppPath("./other");',
true,
true,
],
[
'const { app } = require("electron"); const copy = { ...{ app } }; copy.app.setAppPath("./other");',
true,
true,
],
[
'const { app } = require("electron"); const change = app.setAppPath; change("./other");',
true,
true,
],
['const app = { setAppPath() {} }; app.setAppPath("./other");', false],
[
@@ -52,8 +60,10 @@ it.each([
],
])(
"retains Electron application path changes by binding identity: %s",
(source, expected) => {
const analysis = analyzeJavaScriptStaticSource(source);
(source, expected, commonJs = false) => {
const analysis = commonJs
? analyzeCommonJsStaticFixture(source)
: analyzeJavaScriptStaticSource(source);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.electron.app_path_changes ?? []).toEqual(
expected
@@ -112,7 +122,7 @@ it.each([
])(
"retains indirect application path changes by Electron identity: %s",
(call, expected) => {
const analysis = analyzeJavaScriptStaticSource(`
const analysis = analyzeCommonJsStaticFixture(`
const { app } = require("electron");
${call}
`);
@@ -160,7 +170,7 @@ it("recovers APIs through an ES module default import of Electron", () => {
});
it("recovers APIs and sender checks written with optional chaining", () => {
const analysis = analyzeJavaScriptStaticSource(`
const analysis = analyzeCommonJsStaticFixture(`
const { contextBridge, ipcMain, utilityProcess } = require("electron");
contextBridge?.exposeInMainWorld("api", {});
utilityProcess?.fork("./worker.js");
@@ -202,7 +212,7 @@ it("recovers APIs and sender checks written with optional chaining", () => {
});
it("preserves empty and computed literal contextBridge member keys", () => {
const analysis = analyzeJavaScriptStaticSource(`
const analysis = analyzeCommonJsStaticFixture(`
const { contextBridge } = require("electron");
const dynamicKey = getKey();
contextBridge.exposeInMainWorld("api", {
@@ -232,7 +242,7 @@ it("preserves empty and computed literal contextBridge member keys", () => {
});
it("follows bundler namespace interop helpers without accepting other writes", () => {
const analysis = analyzeJavaScriptStaticSource(`
const analysis = analyzeCommonJsStaticFixture(`
var __toESM = (mod) => mod;
var __importStar = (mod) => mod;
let electron = require("electron");
@@ -279,7 +289,7 @@ it("does not trust unbound helper names or non-function helper values", () => {
});
it("follows stable declaration aliases but not an assigned default parameter", () => {
const analysis = analyzeJavaScriptStaticSource(`
const analysis = analyzeCommonJsStaticFixture(`
const direct = require("electron");
const constAlias = direct;
new constAlias.BrowserWindow({webPreferences:{preload:"./const.js"}});
@@ -302,7 +312,7 @@ it("follows stable declaration aliases but not an assigned default parameter", (
});
it("preserves one initializer across variable redeclarations without accepting writes", () => {
const analysis = analyzeJavaScriptStaticSource(`
const analysis = analyzeCommonJsStaticFixture(`
var electron;
var electron = require("electron");
var electron;
@@ -8,6 +8,7 @@ import type {
} from "./electronStaticAnalysisTypes.js";
import {
boundedExpression,
electronCandidateAuthority,
electronCalleePath,
electronStaticValue,
handlerKind,
@@ -91,6 +92,7 @@ const inspectIpcCall = (
if (findingState.handler_kind === "missing")
context.accumulator.unknownFindings += 1;
const finding: ElectronIpcFinding = {
...(scoped?.authority ?? electronCandidateAuthority(node.callee, context)),
...findingState,
...(scoped === undefined ? {} : { receiver: scoped.receiver }),
...channelReference,
@@ -112,6 +114,7 @@ const scopedIpcDescriptor = (
| {
readonly descriptor: ElectronIpcDescriptor;
readonly receiver: ElectronIpcReceiver;
readonly authority: { readonly loader_authority?: "candidate" };
}
| undefined => {
if (
@@ -143,6 +146,7 @@ const scopedIpcDescriptor = (
? undefined
: {
descriptor,
authority: receiver.authority,
receiver: { kind, owner_location: range(receiver.owner) },
};
};
@@ -86,8 +86,8 @@ export function* finishElectronNativeBindingsSteps(
: new Map<CommonJsExportSite, CommonJsExportReceiverEffect>();
for (const node of pending) {
if (t.isVariableDeclarator(node))
inspectRequireBinding(node, context, index.bindings);
else inspectReExport(node, context, index.bindings, receivers.get(node));
inspectRequireBinding(node, context, index);
else inspectReExport(node, context, index, receivers.get(node));
}
pending.length = 0;
}
@@ -184,9 +184,9 @@ const inspectExportAll = (
const inspectRequireBinding = (
node: t.VariableDeclarator,
context: JavaScriptFindingContext,
bindings: JavaScriptSemanticAnalysisState,
index: JavaScriptReceiverIndex,
): void => {
const required = nativeRequire(node.init, context, bindings);
const required = nativeRequire(node.init, context, index);
if (required === undefined) return;
addBinding({
context,
@@ -201,11 +201,15 @@ const inspectRequireBinding = (
const inspectReExport = (
node: t.AssignmentExpression,
context: JavaScriptFindingContext,
bindings: JavaScriptSemanticAnalysisState,
index: JavaScriptReceiverIndex,
receiver: CommonJsExportReceiverEffect | undefined,
): void => {
const exported = commonJsRuntimeExportValues(node, bindings)[0];
const exported = commonJsRuntimeExportValues(node, index.bindings)[0];
if (exported === undefined) return;
if (exported.loaderAuthority === "candidate") {
context.accumulator.unknownFindings += 1;
return;
}
const applicability = commonJsExportApplicability(
receiver,
exported.exportedName,
@@ -218,7 +222,7 @@ const inspectReExport = (
// The export owner leaves arithmetic/logical stores without a selected RHS.
const value = exported.value;
if (value == null) return;
const required = nativeRequire(value, context, bindings);
const required = nativeRequire(value, context, index);
if (required === undefined) return;
addBinding({
context,
@@ -274,7 +278,7 @@ const nativeCallReceiver = (
const nativeRequire = (
node: t.Node | null | undefined,
context: JavaScriptFindingContext,
bindings: JavaScriptSemanticAnalysisState,
index: JavaScriptReceiverIndex,
):
| {
readonly specifier: string;
@@ -298,8 +302,9 @@ const nativeRequire = (
return undefined;
const call = semanticRequireCallAt(
node,
bindings,
index.bindings,
context.accumulator.factoryRequireBindingIds,
index,
);
if (
call === undefined ||
@@ -200,8 +200,18 @@ describe("Electron option captures and initialization", () => {
expect(analysis.electron.browser_windows).toMatchObject([
{
options_status: "object-literal",
preload_path: "preload.js",
preload_resolution_context: "filesystem-expression",
preload_path: null,
preload_resolution_context: null,
loader_authority: "candidate",
web_preferences: expect.arrayContaining([
expect.objectContaining({
name: "preload",
value: expect.objectContaining({
status: "dynamic",
expression: expect.stringContaining('require("path").join'),
}),
}),
]),
},
]);
});
@@ -951,7 +961,11 @@ describe("Electron option evidence", () => {
utilityProcess.fork("./worker.js", [], { serviceName: "kept", cwd: "/app" });
`);
expect(analysis.parse_status).toBe("complete");
expect(analysis.parse_status).toBe("partial");
expect(analysis.analysis_coverage?.syntax).toEqual({
status: "complete",
error_count: 0,
});
expect(analysis.electron.browser_windows[0]).toMatchObject({
preload_path: "./kept.js",
preload_resolution_context: "module-specifier",
@@ -996,7 +1010,7 @@ describe("Electron option evidence", () => {
},
);
it("selects proven Electron preloads while ignoring media attributes and unrelated properties", () => {
it("retains conditional Electron preloads while ignoring unrelated properties", () => {
const analysis = analyzeJavaScriptStaticSource(`
jsx("audio", { controls: true, preload: "metadata", src });
(0, React.createElement)(\`video\`, { preload: "auto" });
@@ -1006,8 +1020,12 @@ describe("Electron option evidence", () => {
new BrowserWindow({webPreferences: {preload: "./preload.js"}});
`);
expect(analysis.role_paths.map(({ path }) => path)).toEqual([
"./preload.js",
expect(analysis.role_paths).toEqual([]);
expect(analysis.electron.browser_windows).toEqual([
expect.objectContaining({
preload_path: "./preload.js",
loader_authority: "candidate",
}),
]);
});
@@ -1071,7 +1089,11 @@ describe("Electron empty option keys", () => {
`);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.parse_status).toBe("complete");
expect(analysis.parse_status).toBe("partial");
expect(analysis.analysis_coverage?.syntax).toEqual({
status: "complete",
error_count: 0,
});
expect(analysis.electron.browser_windows).toMatchObject([
{
web_preferences_status: "object-literal",
@@ -1097,7 +1119,11 @@ describe("Electron empty option keys", () => {
`);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.parse_status).toBe("complete");
expect(analysis.parse_status).toBe("partial");
expect(analysis.analysis_coverage?.syntax).toEqual({
status: "complete",
error_count: 0,
});
expect(analysis.electron.browser_windows).toMatchObject([
{
web_preferences_status: "object-literal",
@@ -1124,7 +1150,11 @@ describe("Electron empty option keys", () => {
`);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.parse_status).toBe("complete");
expect(analysis.parse_status).toBe("partial");
expect(analysis.analysis_coverage?.syntax).toEqual({
status: "complete",
error_count: 0,
});
expect(analysis.electron.utility_processes).toMatchObject([
{ module_path: "./worker.js", service_name: "kept" },
]);
@@ -7,6 +7,7 @@ import type {
} from "./electronStaticAnalysisTypes.js";
import {
boundedExpression,
electronCandidateAuthority,
electronCalleePath,
electronStaticValue,
} from "./electronStaticAnalysisValues.js";
@@ -269,6 +270,7 @@ const addProtocol = (
const scheme = schemeValue(context, input.scheme, located);
if (scheme.status === "dynamic") context.accumulator.unknownFindings += 1;
const finding: ElectronProtocolFinding = {
...electronCandidateAuthority(input.node.callee, context),
api: input.api,
role: input.role,
scheme,
@@ -1,6 +1,30 @@
import { expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { analyzeJavaScriptStaticSource as analyzeUnknownHostSource } from "./javascriptStaticAnalysis.js";
import { analyzeCommonJsStaticFixture as analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
it("retains an unknown-host Electron request as a candidate without claiming IPC identity", () => {
const analysis = analyzeUnknownHostSource(
'const { BrowserWindow } = require("electron"); new BrowserWindow().webContents.ipc.handle("x", () => {});',
);
expect(analysis.electron.ipc).toEqual([
expect.objectContaining({
channel: "x",
operation: "handle",
loader_authority: "candidate",
}),
]);
expect(analysis.analysis_coverage?.applicability.electron).toBe("unknown");
expect(analysis.references).toContainEqual(
expect.objectContaining({
specifier: "electron",
loader_authority: "candidate",
}),
);
expect(
analysis.analysis_coverage?.semantics.unknown_findings,
).toBeGreaterThan(0);
});
it.each([
["webContents", "window.webContents.ipc"],
@@ -40,6 +40,8 @@ export type ElectronBrowserWindowFinding = ElectronBrowserWindowPreload & {
readonly web_preferences: readonly ElectronWebPreference[];
/** Object syntax supporting constant substitution, separate from construction. */
readonly resolution_sources?: readonly JavaScriptSourceRange[];
/** Electron identity is conditional on an unproved loader. */
readonly loader_authority?: "candidate";
readonly module_key: string | null;
/** Recovered factory runtime; absent in older or unbundled findings. */
readonly module_runtime?: string;
@@ -60,6 +62,8 @@ export type ElectronContextBridgeFinding = ElectronContextBridgeApiKey & {
readonly api_status: "object-literal" | "dynamic" | "missing";
readonly members: readonly string[];
readonly unknown_members: number;
/** Electron identity is conditional on an unproved loader. */
readonly loader_authority?: "candidate";
readonly module_key: string | null;
/** Recovered factory runtime; absent in older or unbundled findings. */
readonly module_runtime?: string;
@@ -124,6 +128,8 @@ interface ElectronIpcFindingContext {
readonly receiver?: ElectronIpcReceiver;
/** Absent on direct syntax observations; wrappers require inferred evidence. */
readonly resolution_basis?: "wrapper-substitution";
/** Electron identity is conditional on an unproved loader. */
readonly loader_authority?: "candidate";
readonly module_key: string | null;
/** Recovered factory runtime; absent in older or unbundled findings. */
readonly module_runtime?: string;
@@ -168,6 +174,8 @@ export interface ElectronSenderValidationFinding {
readonly mechanism: string;
readonly expected: ElectronStaticValue;
readonly enforcement: "unknown";
/** Electron identity is conditional on an unproved loader. */
readonly loader_authority?: "candidate";
readonly module_key: string | null;
/** Recovered factory runtime; absent in older or unbundled findings. */
readonly module_runtime?: string;
@@ -176,6 +184,8 @@ export interface ElectronSenderValidationFinding {
interface ElectronUtilityProcessFindingState {
readonly service_name: string | null;
/** Electron identity is conditional on an unproved loader. */
readonly loader_authority?: "candidate";
readonly module_key: string | null;
/** Recovered factory runtime; absent in older or unbundled findings. */
readonly module_runtime?: string;
@@ -207,6 +217,8 @@ export interface ElectronNativeAddonBindingFinding {
/** Omitted in legacy findings; omission means unknown, not false. */
readonly namespace_access?: boolean;
readonly dynamic_member_access?: boolean;
/** Electron identity is conditional on an unproved loader. */
readonly loader_authority?: "candidate";
readonly module_key: string | null;
/** Recovered factory runtime; absent in older or unbundled findings. */
readonly module_runtime?: string;
@@ -237,6 +249,8 @@ export interface ElectronProtocolFinding {
readonly session: "default-protocol" | "default-session";
/** Privilege keys with a literal `true`; null unless `role` is `privileged-scheme` with a literal object. */
readonly enabled_privileges: readonly string[] | null;
/** Electron identity is conditional on an unproved loader. */
readonly loader_authority?: "candidate";
readonly module_key: string | null;
/** Recovered factory runtime; absent in older or unbundled findings. */
readonly module_runtime?: string;
@@ -260,6 +274,8 @@ export interface ElectronStaticFindings {
/** An Electron application path setter invocation or bound delegate capture. */
export interface ElectronAppPathChangeFinding {
/** Electron identity is conditional on an unproved loader. */
readonly loader_authority?: "candidate";
readonly module_key: string | null;
readonly module_runtime?: string;
readonly location: JavaScriptSourceRange;
@@ -281,6 +297,8 @@ export interface ElectronCallPathStep {
/** A statically substituted candidate, never a runtime reachability claim. */
export interface ElectronCallPath {
/** One or more Electron identities depend on an unproved loader. */
readonly loader_authority?: "candidate";
readonly entry_kind: "bridge-member" | "ipc-handler";
readonly entry: string;
/** Exact bridge API and property segments; `entry` remains display text. */
@@ -18,10 +18,25 @@ export const electronCalleePath = (
const { root, members } = electronMemberPath(callee);
if (!members.every((member): member is string => member !== null))
return undefined;
const exported = context.electronBindings?.get(root.start ?? -1);
const exported =
context.electronBindings?.get(root.start ?? -1) ??
context.electronCandidateBindings?.get(root.start ?? -1);
return exported === undefined ? undefined : [...exported, ...members];
};
/** Keep the identity uncertainty attached to each derived finding. */
export const electronCandidateAuthority = (
callee: t.Node,
context: JavaScriptFindingContext,
): { readonly loader_authority?: "candidate" } => {
const { root } = electronMemberPath(callee);
const offset = root.start ?? -1;
return context.electronBindings?.has(offset) !== true &&
context.electronCandidateBindings?.has(offset) === true
? { loader_authority: "candidate" }
: {};
};
/** Preserve one literal value or the exact inert expression. */
export const electronStaticValue = (
source: string,
@@ -18,8 +18,8 @@ import {
type JavaScriptFindingContext,
} from "./javascriptStaticAnalysisState.js";
const inspect = (source: string) => {
const file = parseJavaScriptSource(source);
const inspect = (source: string, path?: string) => {
const file = parseJavaScriptSource(source, path);
if (file === null) throw new Error("Fixture must parse");
const index = complete(
indexJavaScriptBindingsWithReceiversSteps(file.program),
@@ -73,8 +73,11 @@ it.each([
});
it("still checks mutations for a source-proven scoped Electron receiver", () => {
const result = inspect(`const {BrowserWindow} = require("electron");
const window = new BrowserWindow(); window.webContents.ipc.handle("channel", () => {});`);
const result = inspect(
`const {BrowserWindow} = require("electron");
const window = new BrowserWindow(); window.webContents.ipc.handle("channel", () => {});`,
"fixture.cts",
);
expect(result.findings).toHaveLength(1);
expect(result.mutationIndexCalls).toBeGreaterThan(0);
});
+21 -10
View File
@@ -1,6 +1,9 @@
import * as t from "@babel/types";
import { electronCalleePath } from "./electronStaticAnalysisValues.js";
import {
electronCalleePath,
electronCandidateAuthority,
} from "./electronStaticAnalysisValues.js";
import {
semanticStaticPropertyKey,
unwrapJavaScriptExpression,
@@ -14,17 +17,17 @@ import { completeSemanticSteps } from "./javascriptSemanticTraversal.js";
import type { JavaScriptFindingContext } from "./javascriptStaticAnalysisState.js";
/** A renderer load belongs to an intact, source-proven BrowserWindow instance. */
export const isElectronRendererLoad = (
export const electronRendererLoadAuthority = (
callee: t.Node,
site: t.Node,
context: JavaScriptFindingContext,
index: JavaScriptBindingIndex,
): boolean => {
): { readonly loader_authority?: "candidate" } | undefined => {
callee = unwrapJavaScriptExpression(callee).node;
if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee))
return false;
return undefined;
const method = semanticStaticPropertyKey(callee.property, callee.computed);
if (method !== "loadFile" && method !== "loadURL") return false;
if (method !== "loadFile" && method !== "loadURL") return undefined;
const receiver = resolveElectronBrowserWindowReceiver(
callee.object,
site,
@@ -32,11 +35,11 @@ export const isElectronRendererLoad = (
context,
index,
);
return (
receiver !== undefined &&
return receiver !== undefined &&
(receiver.path.length === 1 ||
(receiver.path.length === 2 && receiver.path[0] === "webContents"))
);
? receiver.authority
: undefined;
};
/** Source allocation identity, not a claim about runtime window/frame identity. */
@@ -47,7 +50,11 @@ export const resolveElectronBrowserWindowReceiver = (
context: JavaScriptFindingContext,
index: JavaScriptBindingIndex,
):
| { readonly owner: t.NewExpression; readonly path: readonly string[] }
| {
readonly owner: t.NewExpression;
readonly path: readonly string[];
readonly authority: { readonly loader_authority?: "candidate" };
}
| undefined => {
let receiver = unwrapJavaScriptExpression(node).node;
const path = [...suffix];
@@ -105,7 +112,11 @@ export const resolveElectronBrowserWindowReceiver = (
for (const alias of aliases)
if (index.referenceMayHaveChangedAt(site, alias.receiver, alias.path))
return undefined;
return { owner: receiver, path };
return {
owner: receiver,
path,
authority: electronCandidateAuthority(receiver.callee, context),
};
};
const supportedReceiverPath = (path: readonly string[]): boolean => {
@@ -147,7 +147,11 @@ it("preserves every direct observation in a long flat sibling block", () => {
}`);
expect(result.electron.ipc.map(({ channel }) => channel)).toEqual(channels);
expect(result.electron.call_paths ?? []).toEqual([]);
expect(result.parse_status).toBe("complete");
expect(result.parse_status).toBe("partial");
expect(result.analysis_coverage?.syntax.status).toBe("complete");
expect(result.analysis_coverage?.semantics.unknown_findings).toBeGreaterThan(
0,
);
});
it("keeps sibling guards, definite exits and nested shadowing separate", () => {
@@ -370,7 +374,11 @@ it("preserves an observed empty contextBridge API object", () => {
expect(result.electron.context_bridge_apis?.[0]).not.toHaveProperty(
"resolution_basis",
);
expect(result.parse_status).toBe("complete");
expect(result.parse_status).toBe("partial");
expect(result.analysis_coverage?.syntax.status).toBe("complete");
expect(result.analysis_coverage?.semantics.unknown_findings).toBeGreaterThan(
0,
);
});
it.each([
@@ -767,7 +775,11 @@ it("resolves a site-determined constant channel in place of its dynamic observat
expect.objectContaining({ entry: "desktop.ping", channel: "ping" }),
);
// The syntax pass's unknown channel is resolved, not left as partial.
expect(result.parse_status).toBe("complete");
expect(result.parse_status).toBe("partial");
expect(result.analysis_coverage?.syntax.status).toBe("complete");
expect(result.analysis_coverage?.semantics.unknown_findings).toBeGreaterThan(
0,
);
});
it("resolves a site-determined registration whose handler is a named local", () => {
const result = analyzeJavaScriptStaticSource(`
@@ -784,7 +796,11 @@ it("resolves a site-determined registration whose handler is a named local", ()
handler_kind: "inline-function",
}),
]);
expect(result.parse_status).toBe("complete");
expect(result.parse_status).toBe("partial");
expect(result.analysis_coverage?.syntax.status).toBe("complete");
expect(result.analysis_coverage?.semantics.unknown_findings).toBeGreaterThan(
0,
);
});
it("keeps an argument-supplied channel unresolved beside its resolved candidates", () => {
const result = analyzeJavaScriptStaticSource(`
@@ -987,7 +1003,11 @@ it.each(["interface Shape { marker(): void }", "type Shape = typeof marker;"])(
resolution_basis ?? "direct",
]),
).toEqual([["ch", "wrapper-substitution"]]);
expect(result.parse_status).toBe("complete");
expect(result.parse_status).toBe("partial");
expect(result.analysis_coverage?.syntax.status).toBe("complete");
expect(
result.analysis_coverage?.semantics.unknown_findings,
).toBeGreaterThan(0);
},
);
it("resolves a handler a module-level factory call created once in place", () => {
@@ -1005,7 +1025,11 @@ it("resolves a handler a module-level factory call created once in place", () =>
resolution_basis ?? "direct",
]),
).toEqual([["ch", "wrapper-substitution"]]);
expect(result.parse_status).toBe("complete");
expect(result.parse_status).toBe("partial");
expect(result.analysis_coverage?.syntax.status).toBe("complete");
expect(result.analysis_coverage?.semantics.unknown_findings).toBeGreaterThan(
0,
);
});
it("keeps separate outer calls that pass one shared handler through a wrapper", () => {
const result = analyzeJavaScriptStaticSource(`
@@ -1063,7 +1087,11 @@ it("resolves a module-level handler in place when the entry binds an unused para
resolution_basis: "wrapper-substitution",
}),
]);
expect(result.parse_status).toBe("complete");
expect(result.parse_status).toBe("partial");
expect(result.analysis_coverage?.syntax.status).toBe("complete");
expect(result.analysis_coverage?.semantics.unknown_findings).toBeGreaterThan(
0,
);
});
it.each([
["a named local", 'const handler = () => tag; register("same", handler);'],
@@ -63,6 +63,7 @@ const ipcEndpointKey = (value: ElectronIpcFinding): string =>
value.side,
value.operation,
value.channel,
value.loader_authority ?? "proven",
javascriptSourceRangeKey(value.location),
]);
@@ -205,7 +206,12 @@ interface BridgeExposure {
}
const bridgeExposureKey = (value: ElectronContextBridgeFinding): string =>
JSON.stringify([value.world, value.api_key, value.api_key_expression]);
JSON.stringify([
value.world,
value.api_key,
value.api_key_expression,
value.loader_authority ?? "proven",
]);
const bridgeContributionKey = (value: ElectronContextBridgeFinding): string =>
JSON.stringify([value.world_id, value.members, value.unknown_members]);
@@ -234,6 +240,7 @@ const mergeBridgeExposures = (
});
interface PathContext {
readonly loader_authority?: "candidate";
readonly entryNode: t.Node;
/** Source ranges of the local calls this path made since its entry. */
readonly calls: string[];
@@ -423,11 +430,15 @@ export function* collectElectronWrappedCallPathsSteps(
accumulator.unknownFindings += 1;
return null;
}
const authorityContext =
value.kind === "external" && value.loaderAuthority === "candidate"
? { ...context, loader_authority: "candidate" as const }
: context;
return (
value.conditions?.reduce(
(current, branch) => condition(current, branch.node, branch.positive),
context,
) ?? context
authorityContext,
) ?? authorityContext
);
};
// Duplicate checks consult only facts at the same endpoint and call site.
@@ -873,6 +884,9 @@ export function* collectElectronWrappedCallPathsSteps(
): void => {
if (context.entryKind === null) return;
const path: ElectronCallPath = {
...(context.loader_authority === "candidate"
? { loader_authority: "candidate" as const }
: {}),
entry_kind: context.entryKind,
entry: context.entry,
...(context.entryKind !== "bridge-member" ||
@@ -958,6 +972,9 @@ export function* collectElectronWrappedCallPathsSteps(
? undefined
: range(context.entryNode);
const findingContext = {
...(context.loader_authority === "candidate"
? { loader_authority: "candidate" as const }
: {}),
module_key: null,
location,
...(invocationLocation === undefined
@@ -1223,6 +1240,9 @@ export function* collectElectronWrappedCallPathsSteps(
// call contributes a separate API per key it is invoked with.
const inventory = bridgeMemberInventory(object);
const finding: ElectronContextBridgeFinding = {
...(context.loader_authority === "candidate"
? { loader_authority: "candidate" as const }
: {}),
world: exposure.world,
world_id: worldId,
api_key: key.value,
@@ -14,9 +14,10 @@ import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.
import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js";
import { createSemanticModuleInterop } from "./javascriptModuleInterop.js";
import {
semanticRequireOrigin,
semanticRequireOriginCandidate,
type JavaScriptRequireOrigin,
} from "./javascriptSemanticRequireOrigin.js";
import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js";
import {
resolveSemanticBindingState,
semanticResolutionBlocked,
@@ -42,7 +43,7 @@ interface BootstrapExternals {
/** Loader bindings with the canonical n.n getter installed before dispatch. */
readonly interopGetterBindingIds: ReadonlySet<string>;
/** Table keys whose factory only re-exports one unshadowed literal require; null when a key repeats. */
readonly externals: ReadonlyMap<string, string | null>;
readonly externals: ReadonlyMap<string, JavaScriptModuleOrigin | null>;
}
const proofs = new WeakMap<
@@ -62,6 +63,15 @@ const proofs = new WeakMap<
export const semanticBundledRequireOrigin = (
node: t.Node | null | undefined,
state: JavaScriptSemanticAnalysisState,
): JavaScriptRequireOrigin | undefined => {
const result = semanticBundledRequireOriginCandidate(node, state);
return result?.origin.loaderAuthority === "candidate" ? undefined : result;
};
/** Retain conditional externals without treating the bootstrap as host proof. */
export const semanticBundledRequireOriginCandidate = (
node: t.Node | null | undefined,
state: JavaScriptSemanticAnalysisState,
): JavaScriptRequireOrigin | undefined => {
const chain = semanticMemberChain(node);
const current = chain?.root;
@@ -80,7 +90,10 @@ export const semanticBundledRequireOrigin = (
// CommonJS external, invoking that getter yields the exact exports object;
// bind its identity to the proven registry result while retaining the
// nested loader call as the source of provenance.
const nested = semanticBundledRequireOrigin(current.arguments[0], state);
const nested = semanticBundledRequireOriginCandidate(
current.arguments[0],
state,
);
const loaderBinding = resolveSemanticBindingState(
state,
current.callee.object,
@@ -136,11 +149,11 @@ export const semanticBundledRequireOrigin = (
const proof = bootstrapExternals(ancestor, state);
if (proof === null || !proof.loaderBindingIds.has(binding.bindingId))
continue;
const specifier = proof.externals.get(key);
return typeof specifier === "string"
const origin = proof.externals.get(key);
return origin !== null && origin !== undefined
? {
origin: {
specifier,
...origin,
importedPath: chain.members,
resolutionMode: "require",
},
@@ -299,7 +312,7 @@ const isInstalledBeforeEntryDispatch = (
};
const bootstrapProofForLoaderCall = (
call: t.CallExpression,
call: t.CallExpression | t.OptionalCallExpression,
loaderBindingId: string,
state: JavaScriptSemanticAnalysisState,
): BootstrapExternals | undefined => {
@@ -403,7 +416,7 @@ const proveBootstrapExternals = (
state,
);
if (integrity === undefined) return null;
const externals = new Map<string, string | null>();
const externals = new Map<string, JavaScriptModuleOrigin | null>();
for (const { property, factory } of factories) {
const key = modulePropertyName(property);
const specifier = integrity.replacedKeys.has(key)
@@ -430,7 +443,7 @@ const proveBootstrapExternals = (
const externalSpecifier = (
factory: t.FunctionExpression | t.ArrowFunctionExpression | t.ObjectMethod,
state: JavaScriptSemanticAnalysisState,
): string | undefined => {
): JavaScriptModuleOrigin | undefined => {
const moduleParameter = javascriptBundlerFactoryRoles(
factory,
BOOTSTRAP_RUNTIME,
@@ -474,9 +487,12 @@ const externalSpecifier = (
target?.bindingId !== moduleBinding.bindingId
)
return undefined;
const required = semanticRequireOrigin(expression.right, state)?.origin;
const required = semanticRequireOriginCandidate(
expression.right,
state,
)?.origin;
return required !== undefined && required.importedPath.length === 0
? required.specifier
? required
: undefined;
};
@@ -3,7 +3,10 @@ import { runInNewContext } from "node:vm";
import { expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
import {
analyzeCommonJsStaticFixture,
analyzeCommonJsSemanticFixture,
} from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
const cases = [
[
@@ -115,7 +118,7 @@ it.each(cases)(
module.exports,
);
expect(changed).toBe(possible ? 1 : 0);
const analysis = analyzeJavaScriptStaticSource(defining);
const analysis = analyzeCommonJsStaticFixture(defining);
expect(analysis.parse_error_count).toBe(0);
expect((analysis.electron.app_path_changes ?? []).length > 0).toBe(
possible,
@@ -140,9 +143,9 @@ it("retains native source uncertainty when an array iterator is overwritten", ()
// The local change function is harmless, but the published app still permits
// a caller to select the native setter. Unknown iteration retains that
// possibility without identifying a yielded value or an exact export link.
const analysis = analyzeJavaScriptStaticSource(defining);
const analysis = analyzeCommonJsStaticFixture(defining);
expect((analysis.electron.app_path_changes ?? []).length).toBeGreaterThan(0);
const ir = analyzeJavaScriptSemantics(defining);
const ir = analyzeCommonJsSemanticFixture(defining);
expect(
ir.moduleLinks.filter(({ kind }) => kind === "commonjs-export"),
).toEqual([
@@ -8,6 +8,7 @@ import {
} from "./javascriptAstValues.js";
import {
resolveSemanticBindingState,
semanticResolutionBlocked,
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js";
@@ -19,8 +20,10 @@ import type { JavaScriptSemanticPropertyPath } from "./javascriptSemanticPropert
import { semanticProjectedSourcesAtRead } from "./javascriptSemanticProjectedSources.js";
import {
isUnshadowedGlobal,
semanticBindingHasPriorValueWrite,
semanticEffectTiming,
} from "./javascriptSemanticMutationInitializers.js";
import { javaScriptRuntimeContext } from "./javascriptSourceParser.js";
import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js";
const exportInvocations = new WeakMap<
@@ -176,12 +179,37 @@ const copiedOwnFields = (
return { definite: common ?? new Set(), complete };
};
/** A possible wrapper export remains a candidate when its host is unknown. */
const isCommonJsExportReference = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
name: string,
): boolean => {
const context = javaScriptRuntimeContext(state.program);
if (context === "module") return false;
if (isUnshadowedGlobal(node, state, name)) return true;
if (
context !== "unknown" ||
!t.isIdentifier(node, { name }) ||
semanticResolutionBlocked(state, node, name)
)
return false;
const binding = resolveSemanticBindingState(state, node, name);
return (
binding !== undefined &&
state.scopesById.get(binding.scopeId)?.kind === "program" &&
binding.declarationKinds.size === 1 &&
binding.declarationKinds.has("var") &&
!semanticBindingHasPriorValueWrite(node, binding, state)
);
};
const isCommonJsModuleExports = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
): boolean =>
(t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) &&
isUnshadowedGlobal(node.object, state, "module") &&
isCommonJsExportReference(node.object, state, "module") &&
semanticStaticPropertyKey(node.property, node.computed) === "exports";
/** One syntax owner for CJS export names and their receiver identities. */
@@ -193,7 +221,7 @@ export const commonJsExportName = (
return undefined;
const key = semanticStaticPropertyKey(node.property, node.computed);
if (
isUnshadowedGlobal(node.object, state, "exports") ||
isCommonJsExportReference(node.object, state, "exports") ||
isCommonJsModuleExports(node.object, state)
)
return key === null ? "*" : key;
@@ -230,7 +258,7 @@ export const commonJsDefinedExport = (
return undefined;
const receiver = unwrapJavaScriptExpression(target).node;
if (
(!isUnshadowedGlobal(receiver, state, "exports") &&
(!isCommonJsExportReference(receiver, state, "exports") &&
!isCommonJsModuleExports(receiver, state)) ||
!isUnmodifiedGlobalReference(node.callee, state, [
"Object",
@@ -272,7 +300,7 @@ const commonJsAssignedExports = (
if (!t.isExpression(target)) return undefined;
const receiver = unwrapJavaScriptExpression(target).node;
if (
!isUnshadowedGlobal(receiver, state, "exports") &&
!isCommonJsExportReference(receiver, state, "exports") &&
!isCommonJsModuleExports(receiver, state)
)
return undefined;
@@ -289,11 +317,8 @@ const commonJsAssignedExports = (
};
};
/** Runtime values published by the CommonJS syntax owner, before receiver applicability. */
export const commonJsRuntimeExportValues = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
): readonly {
interface CommonJsRuntimeExportValue {
readonly loaderAuthority?: "candidate";
readonly exportedName: string;
readonly value: t.Node | null;
/** Retained input to unresolved iteration; never a selected yielded value. */
@@ -301,7 +326,13 @@ export const commonJsRuntimeExportValues = (
/** Own enumerable fields copied at this source, after later-source overwrites. */
readonly projection?: JavaScriptSemanticPropertyPath;
readonly ownEnumerable?: true;
}[] => {
}
/** Runtime values published by the CommonJS syntax owner, before receiver applicability. */
const commonJsExportValues = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
): readonly CommonJsRuntimeExportValue[] => {
if (t.isAssignmentExpression(node)) {
const exportedName = commonJsExportName(node.left, state);
return exportedName === undefined
@@ -310,8 +341,7 @@ export const commonJsRuntimeExportValues = (
}
const assigned = commonJsAssignedExports(node, state);
if (assigned !== undefined) {
const exported: ReturnType<typeof commonJsRuntimeExportValues>[number][] =
[];
const exported: CommonJsRuntimeExportValue[] = [];
const overwritten = new Set<string>();
for (let index = assigned.sources.length - 1; index >= 0; index--) {
const { value, iterationSource } = assigned.sources[index]!;
@@ -359,6 +389,20 @@ export const commonJsRuntimeExportValues = (
];
};
/** Preserve potential exports without asserting an unobserved CommonJS wrapper. */
export const commonJsRuntimeExportValues = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
): readonly CommonJsRuntimeExportValue[] => {
const values = commonJsExportValues(node, state);
return javaScriptRuntimeContext(state.program) === "unknown"
? values.map((value) => ({
...value,
loaderAuthority: "candidate" as const,
}))
: values;
};
type Identity = t.Node | null;
type Execution = "ordered" | "conditional" | "deferred";
@@ -423,12 +467,12 @@ export function* indexCommonJsExportReceiversSteps(
target = unwrapJavaScriptExpression(target).node;
const possiblePublicWrite =
(t.isMemberExpression(target) || t.isOptionalMemberExpression(target)) &&
isUnshadowedGlobal(target.object, state, "module") &&
isCommonJsExportReference(target.object, state, "module") &&
semanticStaticPropertyKey(target.property, target.computed) === null;
const publicWrite =
isCommonJsModuleExports(target, state) || possiblePublicWrite;
const exportsWrite = isUnshadowedGlobal(target, state, "exports");
const moduleWrite = isUnshadowedGlobal(target, state, "module");
const exportsWrite = isCommonJsExportReference(target, state, "exports");
const moduleWrite = isCommonJsExportReference(target, state, "module");
if (!publicWrite && !exportsWrite && !moduleWrite) return;
if (mode === "deferred") {
// A source offset cannot order an invocation against module completion.
@@ -489,14 +533,14 @@ export function* indexCommonJsExportReceiversSteps(
mode !== "ordered"
? null
: receiver !== null &&
isUnshadowedGlobal(receiver, state, "exports")
isCommonJsExportReference(receiver, state, "exports")
? exportsIdentity
: publicIdentity,
);
}
const descriptor = commonJsDefinedExport(node, state);
if (descriptor !== undefined) {
const receiver = isUnshadowedGlobal(
const receiver = isCommonJsExportReference(
descriptor.receiver,
state,
"exports",
@@ -513,7 +557,7 @@ export function* indexCommonJsExportReceiversSteps(
node,
mode !== "ordered"
? null
: isUnshadowedGlobal(defined.receiver, state, "exports")
: isCommonJsExportReference(defined.receiver, state, "exports")
? exportsIdentity
: publicIdentity,
);
@@ -559,7 +603,7 @@ export function* indexCommonJsExportReceiversSteps(
(t.isMemberExpression(node.argument) ||
t.isOptionalMemberExpression(node.argument))
) {
const receiver = isUnshadowedGlobal(
const receiver = isCommonJsExportReference(
node.argument.object,
state,
"exports",
@@ -575,7 +619,7 @@ export function* indexCommonJsExportReceiversSteps(
}
}
write(node.argument, null, mode);
} else if (isUnshadowedGlobal(node, state, "exports")) {
} else if (isCommonJsExportReference(node, state, "exports")) {
values.set(node, exportsIdentity);
} else if (isCommonJsModuleExports(node, state)) {
values.set(node, publicIdentity);
@@ -15,7 +15,7 @@ import {
applicationFeatureSeedSchema,
type ApplicationFeatureSeed,
} from "./javascriptFeatureTraceSchemas.js";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
import type { JsonValue } from "../jsonValue.js";
const memberFields = [
@@ -455,7 +455,7 @@ const propertyMatch = (node: ApplicationNode, field: string) => ({
});
const contextBridgeGraph = () => {
const analysis = analyzeJavaScriptStaticSource(
const analysis = analyzeCommonJsStaticFixture(
"const { contextBridge } = require('electron');\ncontextBridge.exposeInMainWorld('desktopApi', { openProject() {}, closeProject() {} });",
);
expect(analysis.parse_status).toBe("complete");
@@ -6,13 +6,39 @@ export const isJsonModuleTarget = (
path: string,
kind: JavaScriptArtifactFileKind | undefined,
): boolean =>
path.endsWith(".json") && (kind === "json" || kind === "package-json");
posix.extname(path) === ".json" &&
(kind === "json" || kind === "package-json");
/** Native-addon dispatch likewise needs the exact `.node` loader suffix. */
export const isNativeAddonModuleTarget = (
path: string,
kind: JavaScriptArtifactFileKind | undefined,
): boolean => path.endsWith(".node") && kind === "native-addon";
): boolean => posix.extname(path) === ".node" && kind === "native-addon";
/** Exact loader suffixes are separate from case-folded artifact grammar. */
export const explicitJavaScriptLoaderMode = (
path?: string,
): "module" | "commonjs" | undefined => {
switch (path === undefined ? undefined : posix.extname(path)) {
case ".mjs":
case ".mts":
return "module";
case ".cjs":
case ".cts":
return "commonjs";
default:
return undefined;
}
};
/** Case variants are artifact syntax hints, not recognized loader suffixes. */
export const hasCaseVariantJavaScriptLoaderSuffix = (
path?: string,
): boolean => {
if (path === undefined) return false;
const suffix = /\.(?:js|ts|mjs|cjs|mts|cts)$/iu.exec(path)?.[0];
return suffix !== undefined && suffix !== suffix.toLowerCase();
};
/** Loader evidence retained for a source-level module reference. */
export type JavaScriptModuleResolutionMode =
@@ -29,9 +55,10 @@ export const moduleResolutionModeForJavaScriptReference = (
if (explicitMode !== undefined) return explicitMode;
if (referenceKind === "require") return "require";
if (referenceKind === "dynamic-import") return "import";
const loaderMode = explicitJavaScriptLoaderMode(sourcePath);
if (loaderMode !== undefined)
return loaderMode === "commonjs" ? "require" : "import";
const extension = posix.extname(sourcePath).toLowerCase();
if (extension === ".cjs" || extension === ".cts") return "require";
if (extension === ".mts") return "import";
if (extension === ".ts" || extension === ".tsx") return "compiler-dependent";
return "import";
};
@@ -62,6 +62,15 @@ const canonicalAmbientPath = (
type AmbientPath = readonly (string | null)[];
const ambientEffectCovers = (
effect: JavaScriptSemanticAnalysisState["ambientWriteEffects"][number],
target: readonly string[],
): boolean =>
effect.path.length <= target.length &&
effect.path.every((key, index) =>
semanticPropertyPathKeyMatches(key, target[index]!),
);
const retainedAmbientEffects = new WeakMap<
JavaScriptSemanticAnalysisState,
Map<t.Node | undefined, Set<string>>
@@ -798,8 +807,8 @@ export function* indexJavaScriptAmbientWritesSteps(
}
}
// Settle other ambient effects first. A syntactic resolver call is safe only
// if no write or opaque escape can replace the native member anywhere in
// this analysis; otherwise retain its receiver escape conservatively.
// if no write or opaque escape can replace its native member before this
// invocation; ambiguous execution order remains conservative.
const resolverReceivers: typeof escapes = [];
for (const effect of escapes) {
const { node, target } = effect;
@@ -821,10 +830,32 @@ export function* indexJavaScriptAmbientWritesSteps(
}
for (const effect of resolverReceivers) {
const { node } = effect;
const selected = ["global", "require", "resolve"];
const mayAffectResolver = (
effects: typeof state.ambientWriteEffects,
covered: boolean,
): boolean => {
if (!covered) return false;
const matches = effects.filter((entry) =>
ambientEffectCovers(entry, selected),
);
return (
matches.length === 0 ||
matches.some(
(entry) => semanticEffectTiming(node, entry.node, state) !== "after",
)
);
};
if (
(t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
!state.ambientWrites.covers(["global", "require", "resolve"]) &&
!state.ambientEscapes.covers(["global", "require", "resolve"]) &&
!mayAffectResolver(
state.ambientWriteEffects,
state.ambientWrites.covers(selected),
) &&
!mayAffectResolver(
state.ambientEscapeEffects,
state.ambientEscapes.covers(selected),
) &&
readSemanticRequireLoader(node, state, new Set()) === "commonjs-loader"
)
continue;
@@ -877,14 +908,6 @@ export const createGlobalReferenceReader = (
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" };
@@ -901,8 +924,9 @@ export const createGlobalReferenceReader = (
if (effects === undefined) {
effects = state.ambientWriteEffects.filter(
(effect) =>
matches(effect, selected) ||
(namespace !== null && matches(effect, ["global", namespace])),
ambientEffectCovers(effect, selected) ||
(namespace !== null &&
ambientEffectCovers(effect, ["global", namespace])),
);
byNamespace.set(namespace, effects);
}
@@ -0,0 +1,200 @@
import { expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
import { analyzeCommonJsSemanticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
const source = `const {spawn} = require('child_process');
const timers = require('timers');
const child = spawn('tool', []); child.kill();
const timer = timers.setTimeout(() => {}, 1); timers.clearTimeout(timer);`;
it("retains conditional child and timer observations without proving a loader", () => {
const result = analyzeJavaScriptSemantics(source);
expect(result.childProcessSpawns).toMatchObject([
{ command: "tool", builtinAuthority: "candidate" },
]);
expect(result.childProcessInteractions).toMatchObject([
{ kind: "signal", builtinAuthority: "candidate", resolution: "partial" },
]);
expect(result.timerOperations).toMatchObject([
{ kind: "schedule", builtinAuthority: "candidate" },
{ kind: "cancel", builtinAuthority: "candidate" },
]);
const proven = analyzeCommonJsSemanticFixture(source);
expect(proven.childProcessSpawns[0]).not.toHaveProperty("builtinAuthority");
expect(proven.timerOperations[0]).not.toHaveProperty("builtinAuthority");
});
it("does not treat shadowed or overwritten bindings as candidate builtins", () => {
const result = analyzeJavaScriptSemantics(`
const cp = require('child_process'); cp.spawn = custom; cp.spawn('bad', []);
function local(require) { require('timers').setTimeout(() => {}, 1); }
`);
expect(result.childProcessSpawns).toEqual([]);
expect(result.timerOperations).toEqual([]);
});
it("does not promote a builtin binding read before initialization", () => {
const result = analyzeJavaScriptSemantics(`
spawn('early', []);
var { spawn } = require('child_process');
spawn('late', []);
`);
expect(result.childProcessSpawns).toMatchObject([
{ command: "late", builtinAuthority: "candidate" },
]);
expect(result.childProcessSpawns).toHaveLength(1);
});
it("tracks conditional builtin slot writes without discarding unrelated or previously captured methods", () => {
const result = analyzeJavaScriptSemantics(`
const saved = require('child_process').spawn;
require('child_process').exec = custom;
require('child_process').spawn('unrelated', []);
require('child_process').spawn = custom;
require('child_process').spawn('replaced', []);
saved('captured', []);
`);
expect(
result.childProcessSpawns.map(({ command, builtinAuthority }) => ({
command,
builtinAuthority,
})),
).toEqual([
{ command: "unrelated", builtinAuthority: "candidate" },
{ command: "captured", builtinAuthority: "candidate" },
]);
});
it("retains a global process read conditionally when a candidate module may have changed it", () => {
const result = analyzeJavaScriptSemantics(`
require('process').env.MODE = custom;
const mode = process.env.MODE;
`);
expect(result.configurationOperations).toMatchObject([
{ key: "MODE", builtinAuthority: "candidate" },
]);
});
it("does not turn a proven same-slot replacement into a conditional global process read", () => {
const result = analyzeJavaScriptSemantics(`
process.env.MODE = custom;
const mode = process.env.MODE;
`);
expect(result.configurationOperations).toEqual([]);
});
it("weakens default-import builtin effects after a conditional same-slot write", () => {
const result = analyzeJavaScriptSemantics(`
import fs from 'fs';
require('fs').readFileSync = custom;
fs.readFileSync('config.json');
`);
expect(result.configurationOperations).toMatchObject([
{ key: "config.json", builtinAuthority: "candidate" },
]);
});
it("keeps named builtin snapshots proven while weakening mutable default namespaces", () => {
const result = analyzeJavaScriptSemantics(`
import {readFileSync} from 'fs';
import http from 'http'; import timers from 'timers'; import cp from 'child_process';
require('fs').readFileSync = custom;
require('http').get = custom;
require('timers').setTimeout = custom;
require('child_process').spawn = custom;
readFileSync('named.json'); http.get('http://example.test');
timers.setTimeout(() => {}, 1); cp.spawn('tool', []);
`);
expect(result.configurationOperations).toMatchObject([{ key: "named.json" }]);
expect(result.configurationOperations[0]).not.toHaveProperty(
"builtinAuthority",
);
expect(result.requestOperations).toMatchObject([
{ builtinAuthority: "candidate" },
]);
expect(result.timerOperations).toMatchObject([
{ builtinAuthority: "candidate" },
]);
expect(result.childProcessSpawns).toMatchObject([
{ builtinAuthority: "candidate" },
]);
});
it("keeps namespace snapshot methods proven while marking the mutable default object conditional", () => {
const result = analyzeJavaScriptSemantics(`
import * as fs from 'fs';
require('fs').readFileSync = custom;
fs.readFileSync('snapshot.json');
fs.default.readFileSync('mutable.json');
`);
const snapshot = result.configurationOperations.find(
({ key }) => key === "snapshot.json",
);
const mutable = result.configurationOperations.find(
({ key }) => key === "mutable.json",
);
expect(snapshot).toBeDefined();
expect(snapshot).not.toHaveProperty("builtinAuthority");
expect(mutable).toMatchObject({ builtinAuthority: "candidate" });
});
it("preserves namespace process env snapshot across object replacement, but not nested mutation", () => {
const replaced = analyzeJavaScriptSemantics(`
import * as p from 'process'; require('process').env = custom; p.env.MODE;
`).configurationOperations.find(({ key }) => key === "MODE");
expect(replaced).toBeDefined();
expect(replaced).not.toHaveProperty("builtinAuthority");
const mutated = analyzeJavaScriptSemantics(`
import * as p from 'process'; require('process').env.MODE = custom; p.env.MODE;
`).configurationOperations.find(({ key }) => key === "MODE");
expect(mutated).toMatchObject({ builtinAuthority: "candidate" });
});
it("distinguishes proven namespace object replacement from proven slot mutation", () => {
const replaced = analyzeJavaScriptSemantics(`
import * as p from 'process'; process.env = custom; p.env.MODE;
`).configurationOperations.find(({ key }) => key === "MODE");
expect(replaced).toBeDefined();
expect(replaced).not.toHaveProperty("builtinAuthority");
const mutated = analyzeJavaScriptSemantics(`
import * as p from 'process'; process.env.MODE = custom; p.env.MODE;
`).configurationOperations.find(({ key }) => key === "MODE");
expect(mutated).toBeUndefined();
});
it("keeps function fingerprints partial when built-in effects depend on an unknown loader", () => {
const source = `const { spawn } = require('child_process'); function execute() { spawn('tool', []); } function unrelated() { return 1; }`;
const candidate = analyzeJavaScriptSemantics(source);
const proven = analyzeCommonJsSemanticFixture(source);
const fingerprint = (result: typeof candidate, name: string) => {
const callable = result.callables.find((item) => item.name === name);
return result.functionFingerprints.find(
(item) => item.callableId === callable?.callableId,
);
};
expect(fingerprint(candidate, "execute")).toMatchObject({
status: "partial",
components: { effects: ["child-process"] },
});
expect(fingerprint(candidate, "execute")?.limitations.length).toBeGreaterThan(
1,
);
expect(fingerprint(candidate, "unrelated")?.status).toBe("complete");
expect(fingerprint(proven, "execute")?.status).toBe("complete");
});
it("marks an ESM fingerprint partial only when a conditional write affects its builtin effect", () => {
const sample = (write: string) =>
analyzeJavaScriptSemantics(`
import http from 'http';
${write}
function request() { http.get('http://example.test'); }
`).functionFingerprints[0];
expect(sample("")?.status).toBe("complete");
const conditional = sample("require('http').get = custom;");
expect(conditional).toMatchObject({
status: "partial",
components: { effects: ["network"] },
});
expect(conditional?.limitations.length).toBeGreaterThan(1);
});
@@ -16,6 +16,7 @@ const ordinaryMemberChain = ".next".repeat(64);
it("preserves a 12,000-member native import through both analyzers", () => {
const parsed = parseJavaScriptSource(
`const value = require("./addon.node")${memberChain}.last; const ordinary = root.next.last;`,
"fixture.cjs",
);
if (parsed === null) throw new Error("Expected valid JavaScript");
@@ -277,14 +277,19 @@ describe("CommonJS dependency links", () => {
});
it("keeps bindings and re-exports without extra dependency links", () => {
const ir = analyzeJavaScriptSemantics(`
const ir = analyzeParsedJavaScriptSemantics(
parseJavaScriptSource(
`
const bound = (require("bound") as unknown);
const { left, right } = require("destructured");
module.exports = require("forwarded");
exports = module.exports;
exports.member = require("member").value;
exports.first = exports.second = require("chained");
`);
`,
"entry.cts",
)!,
);
expect(
ir.moduleLinks.map(({ kind, specifier, localName, exportedName }) => ({
kind,
@@ -342,31 +347,36 @@ describe("CommonJS dependency links", () => {
{ specifier: "bound", importedPath: [], resolutionMode: "require" },
]);
});
});
it.each([
'require("outer", require("inner"));',
'const bound = require("outer", require("inner"));',
'module.exports = require("outer", require("inner"));',
])("retains a distinct nested call in %s", (source) => {
const links = analyzeJavaScriptSemantics(source).moduleLinks;
expect(links.map(({ specifier }) => specifier)).toEqual(["outer", "inner"]);
expect(links[1]).toMatchObject({
kind: "require",
importedName: null,
localName: null,
exportedName: null,
});
it.each([
'require("outer", require("inner"));',
'const bound = require("outer", require("inner"));',
'module.exports = require("outer", require("inner"));',
])("retains a distinct nested call in %s", (source) => {
const links = analyzeJavaScriptSemantics(source).moduleLinks;
expect(links.map(({ specifier }) => specifier)).toEqual(["outer", "inner"]);
expect(links[1]).toMatchObject({
kind: "require",
importedName: null,
localName: null,
exportedName: null,
});
});
it("preserves unshadowed require origins through aliases and assignments", () => {
const ir = analyzeJavaScriptSemantics(`
const ir = analyzeParsedJavaScriptSemantics(
parseJavaScriptSource(
`
const { ipcMain: bus } = require("electron");
const forwarded = bus;
let assigned;
assigned = require("electron").ipcMain;
{ const require = localLoader; require("./local.js"); }
`);
`,
"entry.cts",
)!,
);
for (const name of ["bus", "forwarded"])
expect(origin(topLevelBinding(ir, name))).toEqual({
specifier: "electron",
@@ -227,13 +227,14 @@ it.each([
);
it("keeps established logical operands across a long chain", () => {
// Known-falsy operands select the right branch at every level; the value and
// provenance must still come from the one reachable operand.
const ir = analyzeJavaScriptSemantics(
`const tok = ${'false || null || 0 || "" || '.repeat(10)}require("electron").app;`,
);
expect(ir.coverage).toEqual({ status: "complete", omittedCount: 0 });
expect(onlyBinding(ir, "tok").provenance).toEqual({
// Known-falsy operands select the right branch at every level.
const source = `const tok = ${'false || null || 0 || "" || '.repeat(10)}require("electron").app;`;
const parsed = parseJavaScriptSource(source, "main.cjs");
if (parsed === null) throw new Error("fixture did not parse");
const proven = analyzeParsedJavaScriptSemantics(parsed);
const candidate = analyzeJavaScriptSemantics(source);
expect(proven.coverage).toEqual({ status: "complete", omittedCount: 0 });
expect(onlyBinding(proven, "tok").provenance).toEqual({
status: "module",
origins: [
{
@@ -244,4 +245,14 @@ it("keeps established logical operands across a long chain", () => {
],
reason: null,
});
expect(onlyBinding(candidate, "tok").provenance).toMatchObject({
status: "ambiguous",
origins: [
{
specifier: "electron",
importedPath: ["app"],
loaderAuthority: "candidate",
},
],
});
});
@@ -67,12 +67,17 @@ import {
} from "./javascriptSemanticTraversal.js";
import {
semanticRequireOrigin,
semanticRequireOriginCandidate,
observePossibleCommonJsDelegateWrite,
invalidateSemanticRequireDelegateWrites,
} from "./javascriptSemanticRequireOrigin.js";
import { semanticBundledRequireOrigin } from "./javascriptBundledRequireOrigin.js";
import {
semanticBundledRequireOrigin,
semanticBundledRequireOriginCandidate,
} from "./javascriptBundledRequireOrigin.js";
import { collectSemanticMemberMutationsSteps } from "./javascriptSemanticMemberMutations.js";
import {
semanticBindingAtRead,
semanticEffectTiming,
semanticEffectTimingWithinActivation,
} from "./javascriptSemanticMutationInitializers.js";
@@ -844,12 +849,16 @@ const resolveGlobalAliasFact = (
export interface JavaScriptElectronIdentity {
/** Proven export paths keyed by call/construction root offset; absence is unknown. */
readonly bindings: ReadonlyMap<number, readonly string[]>;
/** Conditional export paths whose require loader is not established. */
readonly candidateBindings: ReadonlyMap<number, readonly string[]>;
readonly intactExport: (path: readonly (string | null)[]) => boolean;
readonly exportPath: (root: t.Node) => readonly string[] | undefined;
readonly candidateExportPath: (root: t.Node) => readonly string[] | undefined;
readonly invocationPath: (
node: t.CallExpression | t.OptionalCallExpression,
) => readonly string[] | undefined;
readonly mayChangeAppPath: (node: t.Node) => boolean;
readonly candidateMayChangeAppPath: (node: t.Node) => boolean;
}
export function* indexJavaScriptElectronIdentitySteps(
@@ -857,15 +866,33 @@ export function* indexJavaScriptElectronIdentitySteps(
state: JavaScriptSemanticAnalysisState,
): Generator<void, JavaScriptElectronIdentity> {
const facts = new Map<number, readonly string[]>();
const candidateFacts = new Map<number, readonly string[]>();
let hasCandidateElectronSource = false;
const interop = createSemanticModuleInterop(state);
const writes = new SemanticPropertyPathCoverage();
let collectedWrites = false;
const exportsByBinding = new Map<string, readonly string[] | null>();
const candidatesByBinding = new Map<string, readonly string[] | null>();
const originAvailableAtRead = (
binding: JavaScriptSemanticBindingState,
read: t.Node,
): boolean => {
if (
binding.directOrigins.length > 0 ||
semanticStableInitializer(binding, state) === undefined
)
return true;
const access = semanticBindingAtRead(binding, read, state);
// An unresolved activation can still carry a possible identity, while an
// ordered read before its initializer cannot refer to that value.
return access.status !== "ordered" || access.hasDefiniteInitializer;
};
const rootOrigin = (root: t.Node): readonly string[] | undefined => {
if (t.isIdentifier(root)) {
if (semanticResolutionBlocked(state, root, root.name)) return undefined;
const binding = resolveSemanticBindingState(state, root, root.name);
if (binding === undefined) return undefined;
if (binding === undefined || !originAvailableAtRead(binding, root))
return undefined;
let exported = exportsByBinding.get(binding.bindingId);
if (exported === undefined) {
exported = electronExport(binding, state, interop);
@@ -900,13 +927,79 @@ export function* indexJavaScriptElectronIdentitySteps(
}
return rootOrigin(root);
};
const candidateRootExport = (root: t.Node): readonly string[] | undefined => {
if (t.isIdentifier(root)) {
if (semanticResolutionBlocked(state, root, root.name)) return undefined;
const binding = resolveSemanticBindingState(state, root, root.name);
if (
binding === undefined ||
(collectedWrites && interop.invalidated(binding))
)
return undefined;
if (binding.directOrigins.length === 0) {
if (semanticStableInitializer(binding, state) === undefined)
return undefined;
if (!originAvailableAtRead(binding, root)) return undefined;
}
if (
state.opaqueEffects.byScope.size > 0 &&
(semanticOpaqueBindingReadMayHaveChanged(binding, root, state) ||
interop.aliasMayHaveChanged(binding))
)
return undefined;
let candidate = candidatesByBinding.get(binding.bindingId);
if (candidate === undefined) {
const provenance = evaluateSemanticProvenance(binding, state);
const initializer = semanticStableInitializer(binding, state);
const bundled =
initializer === undefined
? undefined
: semanticBundledRequireOriginCandidate(initializer.node, state)
?.origin;
const origin =
bundled?.loaderAuthority === "candidate" &&
initializer?.projection.every(
(key): key is string => typeof key === "string",
)
? {
...bundled,
importedPath: [
...bundled.importedPath,
...initializer.projection,
],
}
: provenance.status === "ambiguous" &&
provenance.origins.length === 1
? provenance.origins[0]
: undefined;
candidate =
origin?.loaderAuthority === "candidate" &&
ELECTRON_MODULE.test(origin.specifier)
? electronExportPath(origin.importedPath)
: null;
candidatesByBinding.set(binding.bindingId, candidate);
}
return candidate ?? undefined;
}
const origin = (
semanticRequireOriginCandidate(root, state) ??
semanticBundledRequireOriginCandidate(root, state)
)?.origin;
return origin?.loaderAuthority === "candidate" &&
ELECTRON_MODULE.test(origin.specifier)
? electronExportPath(origin.importedPath)
: undefined;
};
yield* collectJavaScriptIdentityWritesSteps(
program,
state.erasedSyntaxNodes,
(node, kind) => {
if (kind === "opaque-call") return [interop.opaqueMutationPath(node)];
const { root, members } = electronMemberPath(node);
const exported = kind === "binding" ? undefined : rootOrigin(root);
const exported =
kind === "binding"
? undefined
: (rootOrigin(root) ?? candidateRootExport(root));
const path =
exported === undefined
? (interop.intrinsicPath(root) ??
@@ -936,6 +1029,17 @@ export function* indexJavaScriptElectronIdentitySteps(
!t.isNewExpression(node)
)
return;
if (
(t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
t.isIdentifier(node.callee, { name: "require" })
) {
const origin = semanticRequireOriginCandidate(node, state)?.origin;
if (
origin?.loaderAuthority === "candidate" &&
ELECTRON_MODULE.test(origin.specifier)
)
hasCandidateElectronSource = true;
}
const { root, members } = electronMemberPath(node.callee);
const exported = rootExport(root);
if (
@@ -943,6 +1047,14 @@ export function* indexJavaScriptElectronIdentitySteps(
intactExport(["electron", ...exported, ...members])
)
facts.set(root.start ?? -1, exported);
else {
const candidate = candidateRootExport(root);
if (
candidate !== undefined &&
intactExport(["electron", ...candidate, ...members])
)
candidateFacts.set(root.start ?? -1, candidate);
}
},
});
const invocationPath = (
@@ -965,14 +1077,23 @@ export function* indexJavaScriptElectronIdentitySteps(
};
return {
bindings: facts,
candidateBindings: candidateFacts,
intactExport,
exportPath: rootExport,
candidateExportPath: candidateRootExport,
invocationPath,
mayChangeAppPath: createSemanticElectronAppPathChangeQuery(
state,
rootOrigin,
interop,
),
candidateMayChangeAppPath: hasCandidateElectronSource
? createSemanticElectronAppPathChangeQuery(
state,
candidateRootExport,
interop,
)
: () => false,
};
}
@@ -258,7 +258,7 @@ if (globalThis.register) {
const conditionalAlias = conditional;
conditionalAlias.handle("conditional-alias:ping", () => "local");
`;
const parsed = parseJavaScriptSource(text, "aliases.js");
const parsed = parseJavaScriptSource(text, "aliases.cjs");
if (parsed === null) throw new Error("Expected parsed source");
const index = completeSemanticSteps(
@@ -267,6 +267,21 @@ conditionalAlias.handle("conditional-alias:ping", () => "local");
const electron = completeSemanticSteps(
indexJavaScriptElectronIdentitySteps(parsed.program, index.bindings),
);
const unknown = parseJavaScriptSource(text, "aliases.js");
if (unknown === null) throw new Error("Expected unknown-host source");
const unknownIndex = completeSemanticSteps(
indexJavaScriptBindingsWithReceiversSteps(unknown.program),
);
const candidates = completeSemanticSteps(
indexJavaScriptElectronIdentitySteps(
unknown.program,
unknownIndex.bindings,
),
);
expect(candidates.bindings.size).toBe(0);
expect(candidates.candidateBindings.get(text.indexOf("Win({"))).toEqual([
"BrowserWindow",
]);
expect([...electron.bindings]).toEqual([
[text.indexOf("Win({"), ["BrowserWindow"]],
[text.indexOf("bridge.exposeInMainWorld"), ["contextBridge"]],
@@ -24,7 +24,7 @@ import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.j
import { semanticOpaqueNameMayHaveChanged } from "./javascriptSemanticOpaqueEffects.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
import {
builtinDataEffectMethod,
builtinDataEffectIdentity,
dataEffectArgumentBindingId,
dataEffectLiteralString,
dataEffectMemberCallee,
@@ -92,6 +92,7 @@ interface AsyncEffectAnalysis {
}
interface TimerCandidate {
readonly builtinAuthority?: "candidate";
readonly node: t.CallExpression | t.OptionalCallExpression;
readonly method: JavaScriptSemanticTimerOperation["method"];
readonly kind: JavaScriptSemanticTimerOperation["kind"];
@@ -306,20 +307,25 @@ const emitterIdentity = (
};
/**
* A timer proven to come from the Node `timers` module rather than a bare
* global. Browser and Electron renderer sources reach this analysis without a
* Node/browser distinction, and there `setTimeout("refresh()", 0)` is a real
* A timer associated with the Node `timers` module, either proven or
* conditional, rather than a bare global. Browser and Electron renderer sources
* reach this analysis without a Node/browser distinction, and there `setTimeout("refresh()", 0)` is a real
* timer: the string is evaluated by the host. Node's callback rule therefore
* applies only when the timer is imported from `node:timers`, where that form
* cannot exist.
* applies when interpreting the call as `node:timers`, where that form
* cannot create a Node timer even if the module identity is conditional.
*/
const nodeBuiltinTimerMethod = (
callee: t.Node,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticTimerOperation["method"] | null =>
admittedTimerMethod(
builtinDataEffectMethod(callee, state, ["timers", "node:timers"]) ?? "",
): JavaScriptSemanticTimerOperation["method"] | null => {
const identity = builtinDataEffectIdentity(callee, state, [
"timers",
"node:timers",
]);
return admittedTimerMethod(
identity?.path.length === 1 ? (identity.path[0] ?? "") : "",
);
};
/**
* A schedule imported from Node's `timers` module requires a function in
@@ -370,8 +376,9 @@ const schedulesWithNonFunctionCallback = (
const collectTimer = (input: TimerCollectionInput): void => {
const { node, ownerCallableId, state, output, ancestors } = input;
const method = timerMethod(node.callee, state);
if (method === null) return;
const identity = timerMethod(node.callee, state);
if (identity === null) return;
const { method, builtinAuthority } = identity;
if (
TIMER_SCHEDULE_METHODS.some((candidate) => candidate === method) &&
nodeBuiltinTimerMethod(node.callee, state) !== null &&
@@ -421,6 +428,7 @@ const collectTimer = (input: TimerCollectionInput): void => {
output.push({
node,
method,
...(builtinAuthority === undefined ? {} : { builtinAuthority }),
kind,
ownerCallableId,
handleBindingId:
@@ -467,6 +475,10 @@ const resolveTimers = (
candidate.handleRead !== null &&
candidate.handleRead.origins.length !== linked.length;
return {
...(candidate.builtinAuthority === "candidate" ||
linked.some((item) => item.builtinAuthority === "candidate")
? { builtinAuthority: "candidate" as const }
: {}),
timerId: timerId(candidate),
kind: candidate.kind,
method: candidate.method,
@@ -499,7 +511,10 @@ const resolveTimers = (
const timerMethod = (
callee: t.Node,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticTimerOperation["method"] | null => {
): {
readonly method: JavaScriptSemanticTimerOperation["method"];
readonly builtinAuthority?: "candidate";
} | null => {
if (t.isIdentifier(callee)) {
const binding = resolveSemanticBindingState(state, callee, callee.name);
if (binding === undefined) {
@@ -508,12 +523,24 @@ const timerMethod = (
return semanticResolutionBlocked(state, callee, callee.name) ||
!isUnmodifiedGlobalReference(callee, state, callee.name)
? null
: method;
: { method };
}
}
return admittedTimerMethod(
builtinDataEffectMethod(callee, state, ["timers", "node:timers"]) ?? "",
const identity = builtinDataEffectIdentity(callee, state, [
"timers",
"node:timers",
]);
const method = admittedTimerMethod(
identity?.path.length === 1 ? (identity.path[0] ?? "") : "",
);
return method === null
? null
: {
method,
...(identity?.builtinAuthority === undefined
? {}
: { builtinAuthority: identity.builtinAuthority }),
};
};
const admittedTimerMethod = (
value: string,
@@ -0,0 +1,123 @@
import { expect, it } from "vitest";
import { analyzeCommonJsSemanticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
import {
analyzeJavaScriptSemantics,
analyzeParsedJavaScriptSemantics,
} from "./javascriptSemanticAnalysis.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
const source = `
const fs = require('fs');
const process = require('process');
const http = require('http');
fs.readFileSync('config.json');
process.env.MODE;
const stream = fs.createReadStream('data');
stream.close();
http.get('http://example.test');
`;
it("retains built-in effects as conditional when the loader is unknown", () => {
const candidate = analyzeJavaScriptSemantics(source);
const proven = analyzeCommonJsSemanticFixture(source);
for (const ir of [candidate, proven]) {
expect(ir.configurationOperations.map(({ key }) => key)).toEqual([
"config.json",
"MODE",
]);
expect(ir.resourceOperations.map(({ kind }) => kind)).toEqual([
"acquire",
"release",
]);
expect(ir.requestOperations.map(({ endpoint }) => endpoint)).toEqual([
"http://example.test",
]);
}
for (const operation of [
...candidate.configurationOperations,
...candidate.resourceOperations,
...candidate.requestOperations,
])
expect(operation.builtinAuthority).toBe("candidate");
for (const operation of [
...proven.configurationOperations,
...proven.resourceOperations,
...proven.requestOperations,
])
expect(operation.builtinAuthority).toBeUndefined();
});
it("does not infer a conditional built-in after the selected method is replaced or the loader is shadowed", () => {
for (const source of [
`const fs = require('fs'); fs.readFileSync = replacement; fs.readFileSync('config');`,
`function load(require) { const fs = require('fs'); fs.readFileSync('config'); }`,
])
expect(analyzeJavaScriptSemantics(source).configurationOperations).toEqual(
[],
);
});
it("retains conditional built-in use across imported and unknown-loader alternatives", () => {
for (const choice of ["builtin : alternate", "alternate : builtin"]) {
const source = `import * as builtin from "node:fs"; const alternate = require("fs"); const selected = globalThis.flag ? ${choice}; selected.readFileSync("config.json");`;
const parsed = parseJavaScriptSource(source, "entry.MJS");
if (parsed === null) throw new Error("fixture did not parse");
expect(
analyzeParsedJavaScriptSemantics(parsed).configurationOperations,
).toEqual(
expect.arrayContaining([
expect.objectContaining({
kind: "file",
key: "config.json",
builtinAuthority: "candidate",
}),
]),
);
}
});
it("does not equate mixed alternatives with different identities or replaced methods", () => {
for (const source of [
'import * as builtin from "node:fs"; const alternate = require("http"); const selected = globalThis.flag ? builtin : alternate; selected.readFileSync("config.json");',
'import * as builtin from "node:fs"; const alternate = require("fs"); alternate.readFileSync = globalThis.replacement; const selected = globalThis.flag ? builtin : alternate; selected.readFileSync("config.json");',
'import * as builtin from "node:fs"; const alternate = require("fs"); const selected = globalThis.flag ? builtin : alternate; selected.default.readFileSync("config.json");',
]) {
const parsed = parseJavaScriptSource(source, "entry.MJS");
if (parsed === null) throw new Error("fixture did not parse");
expect(
analyzeParsedJavaScriptSemantics(parsed).configurationOperations,
).toEqual([]);
}
});
it("retains a conditional built-in reached through a nested receiver", () => {
const source =
'import * as builtin from "node:fs"; const alternate = require("fs"); const selected = globalThis.flag ? builtin : alternate; selected.promises.readFile("config.json");';
const parsed = parseJavaScriptSource(source, "entry.MJS");
if (parsed === null) throw new Error("fixture did not parse");
expect(
analyzeParsedJavaScriptSemantics(parsed).configurationOperations,
).toContainEqual(
expect.objectContaining({
kind: "file",
key: "config.json",
builtinAuthority: "candidate",
}),
);
});
it("does not retain a nested built-in after replacing either the object or its method", () => {
for (const mutation of [
"alternate.promises = globalThis.replacement;",
"alternate.promises.readFile = globalThis.replacement;",
"selected.promises.readFile = globalThis.replacement;",
]) {
const source = `import * as builtin from "node:fs"; const alternate = require("fs"); const selected = globalThis.flag ? builtin : alternate; ${mutation} selected.promises.readFile("config.json");`;
const parsed = parseJavaScriptSource(source, "entry.MJS");
if (parsed === null) throw new Error("fixture did not parse");
expect(
analyzeParsedJavaScriptSemantics(parsed).configurationOperations,
).toEqual([]);
}
});
@@ -33,7 +33,7 @@ import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
import {
builtinDataEffectMethod,
builtinDataEffectIdentity,
dataEffectLiteralString,
dataEffectMemberCallee,
assignedSemanticResultBindings,
@@ -52,6 +52,7 @@ interface ChildProcessAnalysis {
type ChildSpawnContext = JavaScriptExpressionShapeResolver;
interface SpawnCandidate {
readonly builtinAuthority?: "candidate";
readonly node: t.CallExpression | t.OptionalCallExpression;
readonly method: JavaScriptSemanticChildProcessSpawn["method"];
readonly ownerCallableId: string | null;
@@ -106,8 +107,9 @@ const collectSpawns = (
enter: (node, parent) => {
callableStack.enter(node, parent);
if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
const method = childProcessMethod(node.callee, state);
if (method !== null) {
const identity = childProcessMethod(node.callee, state);
if (identity !== null) {
const { method, builtinAuthority } = identity;
const projectedResultBindings = assignedSemanticResultBindings(
node,
ancestors,
@@ -116,6 +118,7 @@ const collectSpawns = (
output.push({
node,
method,
...(builtinAuthority === undefined ? {} : { builtinAuthority }),
ownerCallableId: callableStack.callables.at(-1) ?? null,
resultBindingId: scalarAssignedResultBinding(
projectedResultBindings,
@@ -145,6 +148,9 @@ const immutableSpawn = (
const { options, argvCount } = spawnArguments(candidate, context);
const command = dataEffectLiteralString(candidate.node.arguments[0]);
return {
...(candidate.builtinAuthority === undefined
? {}
: { builtinAuthority: candidate.builtinAuthority }),
processId: spawnId(candidate),
method: candidate.method,
location: range(candidate.node),
@@ -225,6 +231,9 @@ const childInteraction = (
const { kind, interactionMethod, eventName } = classification;
const listener = node.arguments[1];
return {
...(linked.some((candidate) => candidate.builtinAuthority === "candidate")
? { builtinAuthority: "candidate" as const }
: {}),
interactionId: `child:${kind}:${String(node.start ?? -1)}:${String(node.end ?? -1)}`,
kind,
method: interactionMethod,
@@ -239,7 +248,11 @@ const childInteraction = (
kind === "signal" ? killSignalName(node.arguments, state) : null,
listenerLocation:
kind === "listener" && t.isNode(listener) ? range(listener) : null,
resolution: complete ? "complete" : "partial",
resolution:
complete &&
linked.every((candidate) => candidate.builtinAuthority !== "candidate")
? "complete"
: "partial",
};
};
@@ -288,14 +301,28 @@ const classifyInteraction = (
const childProcessMethod = (
callee: t.Node,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticChildProcessSpawn["method"] | null => {
const method = builtinDataEffectMethod(callee, state, [
): {
readonly method: JavaScriptSemanticChildProcessSpawn["method"];
readonly builtinAuthority?: "candidate";
} | null => {
const identity = builtinDataEffectIdentity(callee, state, [
"child_process",
"node:child_process",
]);
return (
CHILD_PROCESS_METHODS.find((candidate) => candidate === method) ?? null
);
const method =
identity?.path.length === 1
? CHILD_PROCESS_METHODS.find(
(candidate) => candidate === identity.path[0],
)
: undefined;
return method === undefined
? null
: {
method,
...(identity?.builtinAuthority === undefined
? {}
: { builtinAuthority: identity.builtinAuthority }),
};
};
const resolveChildCandidates = (
@@ -4,6 +4,7 @@ import { BUILTIN_RESOURCE_METHODS } from "./javascriptBuiltinResourceMethods.js"
import { semanticBuiltinNamespacePath } from "./javascriptSemanticProvenance.js";
import {
semanticRequireOrigin,
semanticRequireOriginCandidate,
semanticRequireExportsIdentity,
semanticGlobalProcessOrigin,
semanticMutableBuiltinOrigin,
@@ -22,10 +23,14 @@ import {
} from "./javascriptSemanticPropertyPaths.js";
import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js";
import { semanticTopLevelEffectFollowsCapture } from "./javascriptSemanticMutationInitializers.js";
import {
semanticBindingAtRead,
semanticTopLevelEffectFollowsCapture,
} from "./javascriptSemanticMutationInitializers.js";
import { createSemanticCallableIdStack } from "./javascriptSemanticCallableStack.js";
import {
resolveSemanticBindingState,
semanticStableInitializer,
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticBindingState,
type JavaScriptSemanticMemberEffect,
@@ -171,17 +176,70 @@ export const builtinDataEffectCallablePath = (
callee: t.Node,
state: JavaScriptSemanticAnalysisState,
specifiers: readonly string[],
): readonly string[] | null => {
return builtinDataEffectCallablePathWithAuthority(
callee,
state,
specifiers,
false,
);
};
/** Recover a proven built-in or a conditional built-in identity. */
export const builtinDataEffectIdentity = (
callee: t.Node,
state: JavaScriptSemanticAnalysisState,
specifiers: readonly string[],
): {
readonly path: readonly string[];
readonly builtinAuthority?: "candidate";
} | null => {
const proven = builtinDataEffectCallablePathWithAuthority(
callee,
state,
specifiers,
false,
);
if (proven !== null) return { path: proven };
const candidate = builtinDataEffectCallablePathWithAuthority(
callee,
state,
specifiers,
true,
);
return candidate === null
? null
: { path: candidate, builtinAuthority: "candidate" };
};
const builtinDataEffectCallablePathWithAuthority = (
callee: t.Node,
state: JavaScriptSemanticAnalysisState,
specifiers: readonly string[],
allowCandidate: boolean,
): readonly string[] | null => {
const expression = unwrapJavaScriptExpression(callee).node;
if (expression !== callee)
return builtinDataEffectCallablePath(expression, state, specifiers);
return builtinDataEffectCallablePathWithAuthority(
expression,
state,
specifiers,
allowCandidate,
);
if (t.isSequenceExpression(expression)) {
const final = expression.expressions.at(-1);
return final === undefined
? null
: builtinDataEffectCallablePath(final, state, specifiers);
: builtinDataEffectCallablePathWithAuthority(
final,
state,
specifiers,
allowCandidate,
);
}
const required = semanticRequireOrigin(callee, state);
const required = (
allowCandidate ? semanticRequireOriginCandidate : semanticRequireOrigin
)(callee, state);
if (required !== undefined) {
const origin = builtinNamespaceOrigin(required.origin);
return specifiers.some(
@@ -193,6 +251,9 @@ export const builtinDataEffectCallablePath = (
callee,
state,
true,
undefined,
allowCandidate,
allowCandidate && required.origin.loaderAuthority !== "candidate",
)
? origin.importedPath
: null;
@@ -204,6 +265,7 @@ export const builtinDataEffectCallablePath = (
state,
specifiers,
callee,
allowCandidate,
[],
true,
);
@@ -216,13 +278,14 @@ export const builtinDataEffectCallablePath = (
const receiver = semanticBoundMemberPath(callee.object, state);
if (method !== null && receiver !== undefined) {
const provenance = evaluateSemanticProvenance(receiver.binding, state);
if (provenance.status === "module") {
const sourceOrigin = builtinProvenanceOrigin(provenance, allowCandidate, [
...receiver.path,
method,
]);
if (sourceOrigin !== undefined) {
const origin = builtinNamespaceOrigin({
...provenance.origins[0],
importedPath: [
...provenance.origins[0].importedPath,
...receiver.path,
],
...sourceOrigin,
importedPath: [...sourceOrigin.importedPath, ...receiver.path],
});
if (
specifiers.some(
@@ -235,6 +298,7 @@ export const builtinDataEffectCallablePath = (
state,
specifiers,
callee,
allowCandidate,
[...receiver.path, method],
true,
) !== null
@@ -242,7 +306,12 @@ export const builtinDataEffectCallablePath = (
: null;
}
}
const source = semanticExpressionModuleOrigin(callee.object, state);
const source = semanticExpressionModuleOrigin(
callee.object,
state,
new Set(),
allowCandidate,
);
if (method === null || source === undefined) return null;
const origin = builtinNamespaceOrigin(source);
return specifiers.some(
@@ -255,6 +324,8 @@ export const builtinDataEffectCallablePath = (
state,
true,
callee.object,
allowCandidate,
allowCandidate && source.loaderAuthority !== "candidate",
)
? [...origin.importedPath, method]
: null;
@@ -271,6 +342,7 @@ export const builtinDataEffectCallablePath = (
state,
specifiers,
callee,
allowCandidate,
[method],
true,
);
@@ -285,8 +357,11 @@ export const builtinDataEffectCallablePath = (
return null;
}
const provenance = evaluateSemanticProvenance(binding, state);
if (provenance.status !== "module") return null;
const origin = builtinNamespaceOrigin(provenance.origins[0]);
const sourceOrigin = builtinProvenanceOrigin(provenance, allowCandidate, [
method,
]);
if (sourceOrigin === undefined) return null;
const origin = builtinNamespaceOrigin(sourceOrigin);
const selected = [...origin.importedPath, method];
return specifiers.some(
(specifier) => specifier.replace(/^node:/u, "") === origin.specifier,
@@ -296,16 +371,19 @@ export const builtinDataEffectCallablePath = (
state,
specifiers,
initializer.node,
allowCandidate,
[method],
true,
)?.length === 0 &&
!requiredModuleChanged(
provenance.origins[0].specifier,
sourceOrigin.specifier,
selected,
callee,
state,
true,
initializer.node,
allowCandidate,
allowCandidate && sourceOrigin.loaderAuthority !== "candidate",
)
? [method]
: null;
@@ -337,6 +415,7 @@ const builtinBindingPath = (
state: JavaScriptSemanticAnalysisState,
specifiers: readonly string[],
capture: t.Node,
allowCandidate: boolean,
selectedPath: readonly string[] = [],
callableValue = false,
visited: ReadonlySet<string> = new Set(),
@@ -366,6 +445,7 @@ const builtinBindingPath = (
state,
specifiers,
capture,
allowCandidate,
selectedPath,
callableValue,
visited,
@@ -382,6 +462,7 @@ const computeBuiltinBindingPath = (
state: JavaScriptSemanticAnalysisState,
specifiers: readonly string[],
capture: t.Node,
allowCandidate: boolean,
selectedPath: readonly string[] = [],
callableValue = false,
visited: ReadonlySet<string>,
@@ -390,6 +471,19 @@ const computeBuiltinBindingPath = (
savedObjectCapture?: t.Node,
): readonly string[] | null => {
if (binding === undefined || visited.has(binding.bindingId)) return null;
// A source-owned initializer is not available before its ordered execution.
// Hoisted imports and unresolved activation order retain their existing facts.
if (
binding.directOrigins.length === 0 &&
semanticStableInitializer(binding, state) !== undefined
) {
const read = semanticBindingAtRead(
binding,
provenanceCapture ?? capture,
state,
);
if (read.status === "ordered" && !read.hasDefiniteInitializer) return null;
}
// Provenance identifies the module, independently of whether a selected slot
// still refers to its original built-in after member mutation or escape.
if (
@@ -413,7 +507,7 @@ const computeBuiltinBindingPath = (
))
),
) ||
builtinEscapes(binding, state).some(
builtinEscapes(binding, state, allowCandidate).some(
({ path, node }) =>
affectsBuiltinPath(path, selectedPath) &&
// A function-value escape cannot replace its native [[Call]]. An
@@ -431,9 +525,14 @@ const computeBuiltinBindingPath = (
provenanceCapture === undefined
? evaluateSemanticProvenance(binding, state)
: evaluateSemanticProvenanceAtRead(binding, provenanceCapture, state);
if (provenance.status !== "module") return null;
const { importedPath } = provenance.origins[0];
const namespace = builtinNamespaceOrigin(provenance.origins[0]);
const sourceOrigin = builtinProvenanceOrigin(
provenance,
allowCandidate,
selectedPath,
);
if (sourceOrigin === undefined) return null;
const { importedPath } = sourceOrigin;
const namespace = builtinNamespaceOrigin(sourceOrigin);
if (
!specifiers.some((item) => {
const name = item.replace(/^node:/u, "");
@@ -457,6 +556,10 @@ const computeBuiltinBindingPath = (
capture,
state,
callableValue,
undefined,
allowCandidate,
allowCandidate && origin.loaderAuthority !== "candidate",
processSnapshotObjectRoot(origin, selectedPath),
)
)
return null;
@@ -491,6 +594,7 @@ const computeBuiltinBindingPath = (
state,
specifiers,
current.capture,
allowCandidate,
current.path,
callableValue,
ancestors,
@@ -535,7 +639,11 @@ const computeBuiltinBindingPath = (
{ ...current, node: node.right },
);
} else {
const required = semanticRequireOrigin(node, state);
const required = (
allowCandidate
? semanticRequireOriginCandidate
: semanticRequireOrigin
)(node, state);
if (
required !== undefined &&
requiredModuleChanged(
@@ -545,6 +653,8 @@ const computeBuiltinBindingPath = (
state,
callableValue,
current.savedObjectCapture,
allowCandidate,
allowCandidate && required.origin.loaderAuthority !== "candidate",
)
)
return null;
@@ -557,7 +667,7 @@ const computeBuiltinBindingPath = (
semanticBuiltinNamespacePath(importedPath)
? []
: builtinNamespaceOrigin({
...provenance.origins[0],
...sourceOrigin,
importedPath: resolvedPath,
}).importedPath;
};
@@ -580,6 +690,37 @@ const builtinNamespaceOrigin = (
: { ...origin, specifier, importedPath };
};
const builtinProvenanceOrigin = (
provenance: ReturnType<typeof evaluateSemanticProvenance>,
allowCandidate: boolean,
selectedPath: readonly string[],
): JavaScriptModuleOrigin | undefined => {
if (provenance.status === "module") return provenance.origins[0];
if (
!allowCandidate ||
provenance.status !== "ambiguous" ||
!provenance.origins.some((origin) => origin.loaderAuthority === "candidate")
)
return undefined;
const first = provenance.origins[0];
if (first === undefined) return undefined;
if (provenance.origins.length === 1) return first;
if (selectedPath[0] === "default") return undefined;
const normalized = builtinNamespaceOrigin(first);
return provenance.origins.every((origin) => {
const other = builtinNamespaceOrigin(origin);
return (
other.specifier === normalized.specifier &&
other.importedPath.length === normalized.importedPath.length &&
other.importedPath.every(
(part, index) => part === normalized.importedPath[index],
)
);
})
? first
: undefined;
};
const affectsBuiltinPath = (
path: JavaScriptSemanticPropertyPath,
selected: readonly string[],
@@ -639,6 +780,7 @@ interface BuiltinEffectIndex {
readonly JavaScriptSemanticMemberEffect[]
>;
readonly receivers: WeakMap<t.Node, boolean>;
readonly candidateReceivers: WeakMap<t.Node, boolean>;
}
// Derived passes run after mutation collection. These indexes share that state's
@@ -663,6 +805,7 @@ const builtinEffectIndex = (
moduleEffects: new Map(),
bindingEscapes: new Map(),
receivers: new WeakMap(),
candidateReceivers: new WeakMap(),
};
builtinEffectIndexes.set(state, index);
}
@@ -672,14 +815,16 @@ const builtinEffectIndex = (
const builtinEscapes = (
binding: JavaScriptSemanticBindingState,
state: JavaScriptSemanticAnalysisState,
allowCandidate = false,
): readonly JavaScriptSemanticMemberEffect[] => {
const cache = builtinEffectIndex(state).bindingEscapes;
let effects = cache.get(binding.bindingId);
const key = `${binding.bindingId}:${allowCandidate ? "candidate" : "proven"}`;
let effects = cache.get(key);
if (effects === undefined) {
effects = binding.escapedPaths.filter(
({ node }) => !originalBuiltinReceiver(node, state),
({ node }) => !originalBuiltinReceiver(node, state, allowCandidate),
);
cache.set(binding.bindingId, effects);
cache.set(key, effects);
}
return effects;
};
@@ -687,13 +832,16 @@ const builtinEscapes = (
const builtinModuleEffects = (
specifier: string,
state: JavaScriptSemanticAnalysisState,
allowCandidate = false,
): readonly JavaScriptSemanticRequiredModuleEffect[] => {
const cache = builtinEffectIndex(state).moduleEffects;
let effects = cache.get(specifier);
const key = `${specifier}:${allowCandidate ? "candidate" : "proven"}`;
let effects = cache.get(key);
if (effects === undefined) {
effects = (state.requiredModuleEffects.get(specifier) ?? []).filter(
({ kind, node }) =>
kind === "write" || !originalBuiltinReceiver(node, state),
kind === "write" ||
!originalBuiltinReceiver(node, state, allowCandidate),
);
if (specifier === "fs/promises") {
const sharedFsEscapes = (state.requiredModuleEffects.get("fs") ?? [])
@@ -701,7 +849,7 @@ const builtinModuleEffects = (
({ kind, path, node }) =>
kind === "escape" &&
(path.length === 0 || path[0] === "promises") &&
!originalBuiltinReceiver(node, state),
!originalBuiltinReceiver(node, state, allowCandidate),
)
.map((effect) => ({
...effect,
@@ -710,11 +858,29 @@ const builtinModuleEffects = (
}));
effects = [...effects, ...sharedFsEscapes];
}
cache.set(specifier, effects);
cache.set(key, effects);
}
return effects;
};
/** A named ESM process env/argv value snapshots the object, not later slot replacement. */
const processSnapshotObjectRoot = (
origin: JavaScriptModuleOrigin,
selected: readonly string[],
): "env" | "argv" | undefined => {
if (
origin.resolutionMode === "require" ||
origin.specifier.replace(/^node:/u, "") !== "process"
)
return undefined;
const [root, ...rest] = [...origin.importedPath, ...selected];
if ((root !== "env" && root !== "argv") || rest.length === 0)
return undefined;
// The default export is the mutable process object, not a snapshot slot.
if (origin.importedPath[0] === "default") return undefined;
return root;
};
/** Check shared exports at the use or method-capture point, not at every later call. */
const requiredModuleChanged = (
specifier: string,
@@ -723,10 +889,20 @@ const requiredModuleChanged = (
state: JavaScriptSemanticAnalysisState,
callableValue = false,
savedObjectCapture?: t.Node,
allowCandidate = false,
ignoreCandidateEffects = false,
snapshotObjectRoot?: "env" | "argv",
): boolean => {
const identity = semanticRequireExportsIdentity(specifier, selected);
return builtinModuleEffects(identity.specifier, state).some(
({ path, node, kind }) =>
return builtinModuleEffects(identity.specifier, state, allowCandidate).some(
({ path, node, kind, loaderAuthority }) =>
!(ignoreCandidateEffects && loaderAuthority === "candidate") &&
!(
snapshotObjectRoot !== undefined &&
kind === "write" &&
path.length === 1 &&
path[0] === snapshotObjectRoot
) &&
!(
savedObjectCapture !== undefined &&
path.length < identity.path.length &&
@@ -752,12 +928,14 @@ const requiredModuleChanged = (
const originalBuiltinReceiver = (
node: t.Node | undefined,
state: JavaScriptSemanticAnalysisState,
allowCandidate = false,
): boolean => {
if (node === undefined) return false;
const receivers = builtinEffectIndex(state).receivers;
const index = builtinEffectIndex(state);
const receivers = allowCandidate ? index.candidateReceivers : index.receivers;
const previous = receivers.get(node);
if (previous !== undefined) return previous;
const result = computeOriginalBuiltinReceiver(node, state);
const result = computeOriginalBuiltinReceiver(node, state, allowCandidate);
receivers.set(node, result);
return result;
};
@@ -765,25 +943,49 @@ const originalBuiltinReceiver = (
const computeOriginalBuiltinReceiver = (
node: t.Node | undefined,
state: JavaScriptSemanticAnalysisState,
allowCandidate: boolean,
): boolean => {
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node))
return false;
const callee = unwrapJavaScriptExpression(node.callee).node;
if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee))
return false;
const receiver = t.isIdentifier(callee.object)
? resolveSemanticBindingState(state, callee.object, callee.object.name)
: undefined;
const boundReceiver = semanticBoundMemberPath(callee.object, state);
const receiver = boundReceiver?.binding;
const receiverPath = boundReceiver?.path ?? [];
const provenance =
receiver === undefined
? undefined
: evaluateSemanticProvenance(receiver, state);
const receiverMethod = semanticStaticPropertyKey(
callee.property,
callee.computed,
);
const boundOrigin =
provenance === undefined || receiverMethod === null
? undefined
: builtinProvenanceOrigin(provenance, allowCandidate, [
...receiverPath,
receiverMethod,
]);
const sourceOrigin =
provenance?.status === "module"
? provenance.origins[0]
: (semanticExpressionModuleOrigin(callee.object, state) ??
semanticRequireOrigin(callee.object, state)?.origin ??
semanticGlobalProcessOrigin(callee.object, state));
(boundOrigin === undefined
? undefined
: {
...boundOrigin,
importedPath: [...boundOrigin.importedPath, ...receiverPath],
}) ??
semanticExpressionModuleOrigin(
callee.object,
state,
new Set(),
allowCandidate,
) ??
(allowCandidate
? semanticRequireOriginCandidate(callee.object, state)
: semanticRequireOrigin(callee.object, state)
)?.origin ??
semanticGlobalProcessOrigin(callee.object, state);
if (sourceOrigin === undefined) return false;
const origin = builtinNamespaceOrigin(sourceOrigin);
const posixReceiver =
@@ -792,10 +994,15 @@ const computeOriginalBuiltinReceiver = (
origin.importedPath[0] === "posix";
if (origin.importedPath.length !== 0 && !posixReceiver) return false;
const method = semanticStaticPropertyKey(callee.property, callee.computed);
const identity = semanticRequireExportsIdentity(origin.specifier, [
...origin.importedPath,
method ?? "",
]);
// ESM namespace/named exports are snapshots, whereas the default object and
// CommonJS exports share mutable slots with require().
const sources =
provenance === undefined || boundOrigin === undefined
? [sourceOrigin]
: provenance.origins.map((source) => ({
...source,
importedPath: [...source.importedPath, ...receiverPath],
}));
if (
method === null ||
!BUILTIN_RECEIVER_METHODS[
@@ -803,16 +1010,38 @@ const computeOriginalBuiltinReceiver = (
]?.has(method) ||
receiver?.memberWrites.some(
({ path, node: write }) =>
affectsBuiltinPath(path, [method]) &&
affectsBuiltinPath(path, [...receiverPath, method]) &&
!semanticTopLevelEffectFollowsCapture(node, write, state.parentsByNode),
) ||
(
builtinEffectIndex(state).writesByModule.get(identity.specifier) ?? []
).some(
({ path, node: write }) =>
affectsBuiltinPath(path, identity.path) &&
!semanticTopLevelEffectFollowsCapture(node, write, state.parentsByNode),
)
sources.some((source) => {
const shared =
method === null
? undefined
: semanticMutableBuiltinOrigin(source, [method]);
const identity =
shared === undefined
? undefined
: semanticRequireExportsIdentity(shared.specifier, shared.path);
return (
identity !== undefined &&
(
builtinEffectIndex(state).writesByModule.get(identity.specifier) ?? []
).some(
({ path, node: write, loaderAuthority }) =>
!(
allowCandidate &&
source.loaderAuthority !== "candidate" &&
loaderAuthority === "candidate"
) &&
affectsBuiltinPath(path, identity.path) &&
!semanticTopLevelEffectFollowsCapture(
node,
write,
state.parentsByNode,
),
)
);
})
)
return false;
return !node.arguments.some((argument) => {
@@ -821,7 +1050,11 @@ const computeOriginalBuiltinReceiver = (
if (passed === undefined) return false;
const passedOrigin = evaluateSemanticProvenance(passed, state);
return (
passedOrigin.status === "module" &&
(passedOrigin.status === "module" ||
(allowCandidate &&
passedOrigin.status === "ambiguous" &&
passedOrigin.origins.length === 1 &&
passedOrigin.origins[0]?.loaderAuthority === "candidate")) &&
builtinNamespaceOrigin(passedOrigin.origins[0]).specifier ===
origin.specifier &&
builtinNamespaceOrigin(passedOrigin.origins[0]).importedPath.length === 0
@@ -833,19 +1066,33 @@ const semanticExpressionModuleOrigin = (
expression: t.Node,
state: JavaScriptSemanticAnalysisState,
visited = new Set<string>(),
allowCandidate = false,
): JavaScriptModuleOrigin | undefined => {
const node = unwrapJavaScriptExpression(expression).node;
const required = semanticRequireOrigin(node, state);
const required = (
allowCandidate ? semanticRequireOriginCandidate : semanticRequireOrigin
)(node, state);
if (required !== undefined) return required.origin;
if (t.isIdentifier(node)) {
const binding = resolveSemanticBindingState(state, node, node.name);
if (binding === undefined || visited.has(binding.bindingId))
return undefined;
const provenance = evaluateSemanticProvenance(binding, state);
return provenance.status === "module" ? provenance.origins[0] : undefined;
return provenance.status === "module" ||
(allowCandidate &&
provenance.status === "ambiguous" &&
provenance.origins.length === 1 &&
provenance.origins[0]?.loaderAuthority === "candidate")
? provenance.origins[0]
: undefined;
}
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
const base = semanticExpressionModuleOrigin(node.object, state, visited);
const base = semanticExpressionModuleOrigin(
node.object,
state,
visited,
allowCandidate,
);
const key = semanticStaticPropertyKey(node.property, node.computed);
return base === undefined || key === null
? undefined
@@ -875,11 +1122,49 @@ export const isSemanticProcessObject = (
state,
["process", "node:process"],
node,
false,
selected,
)?.length === 0
);
};
/** Preserve conditional process identity across loader and mutation uncertainty. */
export const semanticProcessObjectIdentity = (
node: t.Expression | t.Super,
state: JavaScriptSemanticAnalysisState,
family: "env" | "argv",
key?: string | null,
): { readonly builtinAuthority?: "candidate" } | null => {
if (isSemanticProcessObject(node, state, family, key)) return {};
if (!t.isIdentifier(node)) return null;
const binding = resolveSemanticBindingState(state, node, node.name);
const selected = key === undefined || key === null ? [family] : [family, key];
if (binding === undefined)
return semanticGlobalProcessOrigin(node, state) !== undefined &&
!requiredModuleChanged(
"process",
selected,
node,
state,
false,
undefined,
false,
true,
)
? { builtinAuthority: "candidate" }
: null;
return builtinBindingPath(
binding,
state,
["process", "node:process"],
node,
true,
selected,
)?.length === 0
? { builtinAuthority: "candidate" }
: null;
};
/** Resolve the nearest direct variable/assignment owner before a callable. */
export const outerDataEffectBinding = (
node: t.Node,
@@ -27,11 +27,11 @@ import {
semanticEffectTiming,
} from "./javascriptSemanticMutationInitializers.js";
import {
builtinDataEffectMethod as builtinMethod,
builtinDataEffectIdentity,
dataEffectArgumentBindingId as argumentBindingId,
dataEffectLiteralString as literalString,
dataEffectMemberCallee as memberCallee,
isSemanticProcessObject as isProcessObject,
semanticProcessObjectIdentity,
outerDataEffectBinding as outerAssignedBinding,
assignedSemanticResultBindings,
semanticProjectedBindingFacts,
@@ -103,6 +103,9 @@ const collectConfigurations = (
key: details.key,
sourceConfigId: null,
value: null,
...(details.builtinAuthority === undefined
? {}
: { builtinAuthority: details.builtinAuthority }),
resolution: details.resolution,
};
output.push({ node, operation });
@@ -116,6 +119,7 @@ const configurationDetails = (
): {
readonly kind: "environment" | "argv" | "file";
readonly key: string | null;
readonly builtinAuthority?: "candidate";
readonly resolution: "complete" | "partial";
} | null => {
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
@@ -124,19 +128,32 @@ const configurationDetails = (
}
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node))
return null;
const fsMethod = builtinMethod(node.callee, context.state, ["fs", "node:fs"]);
const fs = builtinDataEffectIdentity(node.callee, context.state, [
"fs",
"node:fs",
]);
const fsMethod = fs?.path.length === 1 ? fs.path[0] : null;
const promises =
fsMethod === null
? builtinDataEffectIdentity(node.callee, context.state, [
"fs/promises",
"node:fs/promises",
])
: null;
const isFileRead =
fsMethod === "readFile" ||
fsMethod === "readFileSync" ||
(fsMethod === null &&
builtinMethod(node.callee, context.state, [
"fs/promises",
"node:fs/promises",
]) === "readFile");
promises?.path.length === 1 &&
promises.path[0] === "readFile");
return isFileRead
? {
kind: "file",
key: literalString(node.arguments[0]),
...((fsMethod === null ? promises : fs)?.builtinAuthority ===
"candidate"
? { builtinAuthority: "candidate" as const }
: {}),
resolution:
literalString(node.arguments[0]) === null ? "partial" : "complete",
}
@@ -149,6 +166,7 @@ const processConfigurationMember = (
): {
readonly kind: "environment" | "argv";
readonly key: string | null;
readonly builtinAuthority?: "candidate";
readonly resolution: "complete" | "partial";
} | null => {
const object = node.object;
@@ -159,10 +177,19 @@ const processConfigurationMember = (
// `process.env[""]` names an exact (empty) key; only a dynamic key is unknown.
const key = semanticStaticPropertyKey(node.property, node.computed);
if (family === "argv" && key !== null && !isArrayIndexKey(key)) return null;
if (!isProcessObject(object.object, state, family, key)) return null;
const processIdentity = semanticProcessObjectIdentity(
object.object,
state,
family,
key,
);
if (processIdentity === null) return null;
return {
kind: family === "env" ? "environment" : "argv",
key,
...(processIdentity.builtinAuthority === undefined
? {}
: { builtinAuthority: processIdentity.builtinAuthority }),
resolution: key === null ? "partial" : "complete",
};
};
@@ -203,6 +230,9 @@ const collectDefaults = (
ownerCallableId: context.callableStack.at(-1) ?? null,
resultBindingId: outerAssignedBinding(node, context),
key: candidate.operation.key,
...(candidate.operation.builtinAuthority === undefined
? {}
: { builtinAuthority: candidate.operation.builtinAuthority }),
sourceConfigId: candidate.operation.configId,
value: value.status === "literal" ? value.value : null,
...(value.status === "unknown" && value.resourceLimit !== undefined
@@ -226,8 +256,9 @@ const collectRequests = (
!t.isNewExpression(node)
)
return;
const method = requestMethod(node, context.state);
if (method === null) return;
const identity = requestMethod(node, context.state);
if (identity === null) return;
const method = identity.method;
const endpoint = requestEndpoint(node);
// `http.get`/`http.request` parse a string target with `new URL` before the
// request exists, so a literal that is not an absolute URL throws
@@ -246,6 +277,9 @@ const collectRequests = (
requestId: `request:${String(node.start ?? -1)}:${String(node.end ?? -1)}`,
kind: "request",
method,
...(identity.builtinAuthority === undefined
? {}
: { builtinAuthority: identity.builtinAuthority }),
location: range(node),
ownerCallableId: context.callableStack.at(-1) ?? null,
resultBindingId: outerAssignedBinding(node, context),
@@ -478,7 +512,10 @@ const boundaryDetails = (
const requestMethod = (
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticRequestOperation["method"] | null => {
): {
readonly method: JavaScriptSemanticRequestOperation["method"];
readonly builtinAuthority?: "candidate";
} | null => {
if (t.isIdentifier(node.callee)) {
const name = node.callee.name;
const unbound =
@@ -490,15 +527,23 @@ const requestMethod = (
name === global &&
isUnmodifiedGlobalReference(node.callee, state, name)
)
return global;
return { method: global };
}
const method = builtinMethod(node.callee, state, [
const identity = builtinDataEffectIdentity(node.callee, state, [
"http",
"https",
"node:http",
"node:https",
]);
return method === "request" || method === "get" ? method : null;
const method = identity?.path.length === 1 ? identity.path[0] : null;
return method === "request" || method === "get"
? {
method,
...(identity?.builtinAuthority === undefined
? {}
: { builtinAuthority: identity.builtinAuthority }),
}
: null;
};
const requestFields = (
@@ -83,10 +83,25 @@ export function* collectJavaScriptSemanticFingerprintsSteps(
const networkEffects = networkEffectsByOwner.get(callable.callableId) ?? [];
const resourceEffects =
resourceEffectsByOwner.get(callable.callableId) ?? [];
const conditionalBuiltinEffects =
timerEffects.some(
({ builtinAuthority }) => builtinAuthority === "candidate",
) ||
childProcessEffects.some(
({ builtinAuthority }) => builtinAuthority === "candidate",
) ||
networkEffects.some(
({ builtinAuthority }) => builtinAuthority === "candidate",
) ||
resourceEffects.some(
({ builtinAuthority }) => builtinAuthority === "candidate",
);
fingerprints.push({
callableId: callable.callableId,
status:
parserPartial || callable.returnCoverage.status !== "complete"
parserPartial ||
conditionalBuiltinEffects ||
callable.returnCoverage.status !== "complete"
? "partial"
: "complete",
components: {
@@ -121,6 +136,11 @@ export function* collectJavaScriptSemanticFingerprintsSteps(
...(parserPartial
? ["Incomplete semantic recovery makes this fingerprint partial."]
: []),
...(conditionalBuiltinEffects
? [
"A built-in effect has conditional loader or mutation identity, so its fingerprint contribution is partial.",
]
: []),
],
});
}
@@ -11,6 +11,8 @@ import type { JavaScriptModuleResolutionMode } from "./javascriptModuleResolutio
/** One exact module origin followed through imports, requires, or aliases. */
export interface JavaScriptModuleOrigin {
/** An unbound require whose CommonJS wrapper is not established. */
readonly loaderAuthority?: "candidate";
readonly resolutionMode?: JavaScriptModuleResolutionMode;
readonly specifier: string;
readonly importedPath: readonly string[];
@@ -264,6 +266,7 @@ export interface JavaScriptSemanticEventOperation {
/** One timer scheduling or cancellation candidate. */
export interface JavaScriptSemanticTimerOperation {
readonly builtinAuthority?: "candidate";
readonly timerId: string;
readonly kind: "schedule" | "cancel";
readonly method:
@@ -284,6 +287,7 @@ export interface JavaScriptSemanticTimerOperation {
/** One explicit asynchronous node:child_process creation candidate. */
export interface JavaScriptSemanticChildProcessSpawn {
readonly builtinAuthority?: "candidate";
readonly processId: string;
readonly method: "spawn" | "exec" | "execFile" | "fork";
readonly location: JavaScriptSourceRange;
@@ -299,6 +303,7 @@ export interface JavaScriptSemanticChildProcessSpawn {
/** One exit/error listener or signal operation linked to child candidates. */
export interface JavaScriptSemanticChildProcessInteraction {
readonly builtinAuthority?: "candidate";
readonly interactionId: string;
readonly kind: "listener" | "signal";
readonly method: "on" | "once" | "addListener" | "kill";
@@ -314,6 +319,7 @@ export interface JavaScriptSemanticChildProcessInteraction {
/** One environment, argv, file, or default configuration source. */
export interface JavaScriptSemanticConfigurationOperation {
readonly builtinAuthority?: "candidate";
readonly configId: string;
readonly kind: "environment" | "argv" | "file" | "default";
readonly location: JavaScriptSourceRange;
@@ -328,6 +334,7 @@ export interface JavaScriptSemanticConfigurationOperation {
/** One request construction or response-consumer candidate. */
export interface JavaScriptSemanticRequestOperation {
readonly builtinAuthority?: "candidate";
readonly requestId: string;
readonly kind: "request" | "response-consumer";
readonly method:
@@ -372,6 +379,7 @@ export interface JavaScriptSemanticBoundaryOperation {
/** One explicit built-in resource acquisition or release candidate. */
export interface JavaScriptSemanticResourceOperation {
readonly builtinAuthority?: "candidate";
readonly resourceId: string;
readonly kind: "acquire" | "release";
readonly method:
@@ -418,6 +426,8 @@ export interface JavaScriptSemanticFrontier {
/** Static import/export relationship retained for cross-file composition. */
export interface JavaScriptSemanticModuleLink {
/** The literal request is observed, but its CommonJS wrapper is unknown. */
readonly loaderAuthority?: "candidate";
readonly resolutionMode?: JavaScriptModuleResolutionMode;
readonly kind:
| "import"
@@ -16,7 +16,7 @@ import {
semanticOriginalGlobalProcessAt,
semanticMutableBuiltinOrigin,
semanticRequireExportsIdentity,
semanticRequireOrigin,
semanticRequireOriginCandidate,
semanticRequireLoaderAt,
hasSemanticCreateRequireFactoryOrigin,
} from "./javascriptSemanticRequireOrigin.js";
@@ -483,6 +483,7 @@ export function* collectSemanticMemberMutationsSteps(
path: PropertyPath,
effect: ValueEffect,
node: t.Node | undefined,
loaderAuthority?: "candidate",
): void => {
const identity = semanticRequireExportsIdentity(specifier, path);
let effectPath = identity.path;
@@ -508,6 +509,7 @@ export function* collectSemanticMemberMutationsSteps(
"required",
identity.specifier,
effect,
loaderAuthority,
effectPath,
node?.start,
node?.end,
@@ -516,7 +518,12 @@ export function* collectSemanticMemberMutationsSteps(
recordedRequiredEffects.add(key);
recordedEffectCount += 1;
const effects = state.requiredModuleEffects.get(identity.specifier) ?? [];
effects.push({ path: effectPath, node, kind: effect });
effects.push({
path: effectPath,
node,
kind: effect,
...(loaderAuthority === undefined ? {} : { loaderAuthority }),
});
state.requiredModuleEffects.set(identity.specifier, effects);
};
// Traversal states that differ only in coverage fields a record never
@@ -866,13 +873,14 @@ export function* collectSemanticMemberMutationsSteps(
)
continue;
const expression = unwrapJavaScriptExpression(current.node).node;
const required = semanticRequireOrigin(expression, state);
const required = semanticRequireOriginCandidate(expression, state, true);
if (required !== undefined) {
recordRequiredEffect(
required.origin.specifier,
[...required.origin.importedPath, ...current.path],
effect,
mutation,
required.origin.loaderAuthority,
);
continue;
}
@@ -1156,6 +1164,7 @@ export function* collectSemanticMemberMutationsSteps(
shared.path,
effect,
mutation,
origin.loaderAuthority,
);
}
const receiverInitializers = [
@@ -2108,7 +2117,7 @@ const observableMutationPaths = (
// exact member paths instead of widening the first path to a wildcard.
const hasParsedRequireOrigin = binding.initializers.some(
({ node }) =>
semanticRequireOrigin(node, state) !== undefined ||
semanticRequireOriginCandidate(node, state) !== undefined ||
hasSemanticCreateRequireFactoryOrigin(node, state),
);
if (
@@ -1983,8 +1983,8 @@ export const commonJsWrapperBindingDeclaration = (
// The index owns explicit wrapper parameters, including ones with no source
// redeclaration. Babel's module classification does not invalidate that fact.
if (binding.hostOrigin === "commonjs-wrapper") return binding;
// Without an explicit host fact, preserve the legacy script-var inference.
return state.program.sourceType === "script" &&
// A program var shares the wrapper binding only when the host is known.
return javaScriptRuntimeContext(state.program) === "commonjs" &&
binding.declarationKinds.size > 0
? binding
: undefined;
@@ -12,7 +12,7 @@ import {
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticScopeState,
} from "./javascriptSemanticState.js";
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
import { semanticRequireOriginCandidate } from "./javascriptSemanticRequireOrigin.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import {
completeSemanticSteps,
@@ -428,7 +428,7 @@ const collectRequireLink = (
node: t.VariableDeclarator,
state: JavaScriptSemanticAnalysisState,
): void => {
const required = semanticRequireOrigin(node.init, state);
const required = semanticRequireOriginCandidate(node.init, state);
if (required === undefined) return;
const { origin, call } = required;
const bindings = requirePatternBindings(node.id, origin.importedPath);
@@ -437,6 +437,9 @@ const collectRequireLink = (
state,
{
kind: "require",
...(origin.loaderAuthority === "candidate"
? { loaderAuthority: "candidate" as const }
: {}),
specifier: origin.specifier,
importedName: binding.importedName,
localName: binding.localName,
@@ -453,12 +456,15 @@ const collectUnboundRequireLink = (
state: JavaScriptSemanticAnalysisState,
): void => {
if (state.linkedRequireCalls.has(node)) return;
const required = semanticRequireOrigin(node, state);
const required = semanticRequireOriginCandidate(node, state);
if (required === undefined) return;
addModuleLink(
state,
{
kind: "require",
...(required.origin.loaderAuthority === "candidate"
? { loaderAuthority: "candidate" as const }
: {}),
specifier: required.origin.specifier,
importedName: null,
localName: null,
@@ -515,6 +521,7 @@ const collectCommonJsExport = (
exported.exportedName,
applicability === "uncertain" ? null : exported.value,
exported.projection,
exported.loaderAuthority,
);
}
};
@@ -525,13 +532,14 @@ const addCommonJsExport = (
exportedName: string,
exported: t.Node | null,
projection?: JavaScriptSemanticPropertyPath,
loaderAuthority?: "candidate",
): void => {
// Simple assignment chains yield their rightmost assigned value.
let value = exported;
while (t.isAssignmentExpression(value, { operator: "=" }))
value = value.right;
const required =
value === null ? undefined : semanticRequireOrigin(value, state);
value === null ? undefined : semanticRequireOriginCandidate(value, state);
// Copies retain their module provenance, but do not export the source object
// itself. A wildcard with exclusions cannot express one module link.
const origin = projection?.some(
@@ -545,6 +553,10 @@ const addCommonJsExport = (
state,
{
kind: "commonjs-export",
...(origin?.loaderAuthority === "candidate" ||
loaderAuthority === "candidate"
? { loaderAuthority: "candidate" as const }
: {}),
specifier: origin?.specifier ?? null,
importedName:
origin === undefined
@@ -1,6 +1,10 @@
import { describe, expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
import {
analyzeJavaScriptSemantics,
analyzeParsedJavaScriptSemantics,
} from "./javascriptSemanticAnalysis.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import {
onlyBinding,
topLevelBinding,
@@ -46,7 +50,7 @@ describe("JavaScript semantic binding provenance", () => {
});
it("recognizes explicit local values and preserves exact module aliases", () => {
const ir = analyzeJavaScriptSemantics(`
const source = `
const object = { value: 1 };
const array = [1, 2];
const template = \`feature\`;
@@ -60,7 +64,15 @@ describe("JavaScript semantic binding provenance", () => {
const alias = imported;
const sequence = (0, require("sequence-feature"));
const assignment = (slot = require("assignment-feature"));
`);
`;
const parsed = parseJavaScriptSource(source, "main.cjs");
if (parsed === null) throw new Error("fixture did not parse");
const ir = analyzeParsedJavaScriptSemantics(parsed);
const candidate = analyzeJavaScriptSemantics(source);
expect(topLevelBinding(candidate, "alias").provenance).toMatchObject({
status: "ambiguous",
origins: [{ specifier: "feature", loaderAuthority: "candidate" }],
});
for (const name of [
"object",
@@ -30,9 +30,16 @@ export const semanticOriginsProvenance = (
): JavaScriptBindingProvenance => {
const unique = uniqueSemanticOrigins(origins);
const origin = unique[0];
return unique.length === 1 && origin !== undefined
return unique.length === 1 &&
origin !== undefined &&
origin.loaderAuthority !== "candidate"
? { status: "module", origins: [origin], reason: null }
: semanticAmbiguousProvenance(unique, "Multiple module origins.");
: semanticAmbiguousProvenance(
unique,
unique.some(({ loaderAuthority }) => loaderAuthority === "candidate")
? "CommonJS loader authority is unknown."
: "Multiple module origins.",
);
};
/** Deduplicate and canonically order module origins. */
@@ -52,6 +59,7 @@ const semanticOriginKey = (origin: JavaScriptModuleOrigin): string =>
origin.specifier,
origin.importedPath,
origin.resolutionMode ?? "import",
origin.loaderAuthority ?? "proven",
]);
/** Canonically distinguish primitive values across type boundaries. */
@@ -1,10 +1,13 @@
import { expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
import { analyzeParsedJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
const provenance = (source: string, name: string) => {
const result = analyzeJavaScriptSemantics(source);
const file = parseJavaScriptSource(source, "fixture.cjs");
if (file === null) throw new Error("Expected valid CommonJS fixture");
const result = analyzeParsedJavaScriptSemantics(file);
const binding = result.bindings.find((candidate) => candidate.name === name);
if (binding === undefined) throw new Error(`Missing binding ${name}`);
return binding.provenance;
@@ -17,6 +17,7 @@ import {
semanticResolutionBlocked,
semanticStableInitializer,
type JavaScriptSemanticAnalysisState,
type JavaScriptBindingIndex,
} from "./javascriptSemanticState.js";
import {
commonJsWrapperBindingDeclaration,
@@ -173,7 +174,7 @@ const requireLoaderValueAt = (
if (loader.name !== "require") return "local";
if (binding === undefined) {
if (
javaScriptRuntimeContext(state.program) === "module" ||
javaScriptRuntimeContext(state.program) !== "commonjs" ||
semanticResolutionBlocked(state, loader, "require")
)
return "unknown";
@@ -199,7 +200,10 @@ const requireLoaderValueAt = (
t.isNewExpression(node)) &&
isWrapperRequireInitializerCall(binding, node, state)
)
return commonJsLoaderAt(node, state);
return binding.hostOrigin === "commonjs-wrapper" ||
javaScriptRuntimeContext(state.program) === "commonjs"
? commonJsLoaderAt(node, state)
: "unknown";
const wrapperBinding = commonJsWrapperBindingDeclaration(
loader,
"require",
@@ -376,13 +380,14 @@ const selectedRequireSourcesAt = (
):
| Pick<
JavaScriptRequireCallFact,
"loader" | "sourceRelative" | "sourceRelativePath"
"loader" | "sourceRelative" | "sourceRelativePath" | "loaderAuthority"
>
| undefined => {
const pending: ModuleObjectSource[] = [{ node: root, path: [] }];
const seen = new WeakMap<t.Node, Set<string>>();
let count = 0;
let found = false;
let candidate = false;
let loaderBinding = false;
let selectedLoader: JavaScriptRequireLoaderFact | undefined;
let unknown = false;
@@ -432,6 +437,10 @@ const selectedRequireSourcesAt = (
? commonJsLoaderAt(site, state)
: captured;
found = true;
candidate ||=
invoked === "unknown" &&
t.isIdentifier(node, { name: "require" }) &&
isConditionalRequireAt(node, node, state);
unknown ||=
invoked === "unknown" ||
(selectedLoader !== undefined && selectedLoader !== invoked);
@@ -655,6 +664,7 @@ const selectedRequireSourcesAt = (
? (selectedLoader ?? "unknown")
: "local",
sourceRelative: found && sourceRelative && !unknown,
...(candidate ? { loaderAuthority: "candidate" as const } : {}),
...(found &&
!unknown &&
sourceRelative &&
@@ -701,23 +711,41 @@ export interface JavaScriptRequireCallFact {
| readonly t.CallExpression["arguments"][number][]
| undefined;
readonly sourceRelative: boolean;
readonly loaderAuthority?: "candidate";
readonly memberUnproven?: true;
/** A createRequire filename relative to the current source directory, when proven. */
readonly sourceRelativePath?: string;
}
/** A child-property write does not replace the selected callable itself. */
export const semanticSelectedMemberMayHaveChanged = (
index: JavaScriptBindingIndex,
site: t.Node,
member: t.Node,
): boolean => {
const effects = index.effectsForReferenceAt(site, member);
const targetDepth = semanticMemberChain(member)?.members.length;
return (
!effects.memberEffectsIndexed ||
effects.effects.some(
({ kind, timing, path }) =>
timing !== "after" &&
(kind === "escape" ||
(kind === "member-write" &&
(targetDepth === undefined ||
path === undefined ||
path.length <= targetDepth))),
)
);
};
/** Classify the selected loader and normalize invocation arguments once. */
export const semanticRequireCallAt = (
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
state: JavaScriptSemanticAnalysisState,
factoryBindingIds: ReadonlySet<string>,
index?: JavaScriptBindingIndex,
): JavaScriptRequireCallFact | undefined => {
if (semanticModuleRequireCallAt(node, state))
return {
operation: "load",
loader: "unknown",
arguments: undefined,
sourceRelative: false,
};
const callee = unwrapJavaScriptExpression(node.callee).node;
if (
(t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
@@ -733,6 +761,59 @@ export const semanticRequireCallAt = (
arguments: undefined,
sourceRelative: false,
};
if (index !== undefined) {
let member = callee;
while (
t.isMemberExpression(member) ||
t.isOptionalMemberExpression(member)
) {
const key = semanticStaticPropertyKey(member.property, member.computed);
if (key === "call" || key === "apply" || key === "resolve") {
const loader = requireLoaderValueAt(
member.object,
node,
state,
factoryBindingIds,
);
const selected =
loader === "local"
? selectedRequireSourcesAt(
member.object,
node,
state,
factoryBindingIds,
)
: undefined;
if (
loader === "local" &&
(selected === undefined || selected.loader === "local")
) {
member = unwrapJavaScriptExpression(member.object).node;
continue;
}
if (semanticSelectedMemberMayHaveChanged(index, node, member))
return {
operation: "load",
loader: "unknown",
// A changed call/apply member has no trustworthy argument mapping.
arguments:
member === callee && key === "resolve"
? node.arguments
: undefined,
sourceRelative: false,
memberUnproven: true,
};
}
member = unwrapJavaScriptExpression(member.object).node;
}
}
if (semanticModuleRequireCallAt(node, state))
return {
operation: "load",
loader: "unknown",
arguments: undefined,
sourceRelative: false,
};
if (
(t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
semanticStaticPropertyKey(callee.property, callee.computed) === "resolve"
@@ -743,13 +824,19 @@ export const semanticRequireCallAt = (
state,
factoryBindingIds,
);
if (loader === "unknown")
if (loader === "unknown") {
const receiver = unwrapJavaScriptExpression(callee.object).node;
const candidate =
t.isIdentifier(receiver, { name: "require" }) &&
isConditionalRequireAt(receiver, node, state);
return {
operation: "load",
operation: "resolve",
loader,
arguments: undefined,
arguments: candidate ? node.arguments : undefined,
sourceRelative: false,
...(candidate ? { loaderAuthority: "candidate" as const } : {}),
};
}
// Bundler factory loaders can share the `require` shape, but their
// `.resolve` member is not Node's module resolver.
if (loader === "commonjs-loader" || loader === "create-require") {
@@ -790,6 +877,9 @@ export const semanticRequireCallAt = (
loader: selected?.loader ?? loader,
arguments: node.arguments,
sourceRelative: selected?.sourceRelative ?? loader !== "create-require",
...(selected?.loaderAuthority === "candidate"
? { loaderAuthority: "candidate" as const }
: {}),
...(selected?.sourceRelativePath === undefined
? {}
: { sourceRelativePath: selected.sourceRelativePath }),
@@ -804,8 +894,19 @@ export const semanticRequireCallAt = (
state,
factoryBindingIds,
);
if (loader !== "local" && loader !== "create-require")
return ["require-loader", loader, "relative"];
if (loader !== "local" && loader !== "create-require") {
const root = unwrapJavaScriptExpression(callee).node;
const candidate =
loader === "unknown" &&
t.isIdentifier(root, { name: "require" }) &&
isConditionalRequireAt(root, node, state);
return [
"require-loader",
loader,
"relative",
...(candidate ? ["candidate"] : []),
];
}
const selected = selectedRequireSourcesAt(
callee,
node,
@@ -817,6 +918,7 @@ export const semanticRequireCallAt = (
"require-loader",
selected.loader,
selected.sourceRelative ? "relative" : "unknown",
...(selected.loaderAuthority === "candidate" ? ["candidate"] : []),
];
return interop.intrinsicPath(callee);
});
@@ -836,6 +938,9 @@ export const semanticRequireCallAt = (
? "unknown"
: (invocation.path[1] as JavaScriptRequireLoaderFact),
arguments: invocation.arguments,
...(invocation.kind === "known" && invocation.path[3] === "candidate"
? { loaderAuthority: "candidate" as const }
: {}),
sourceRelative:
invocation.kind === "known" && invocation.path[2] === "relative",
...(selected?.sourceRelativePath === undefined
@@ -1267,6 +1372,18 @@ const hasCommonJsModuleSource = (
commonJsWrapperBindingDeclaration(value, "require", state) !==
undefined &&
!read.hasDefiniteInitializer;
// A top-level `var require` may refer to a host wrapper on an unknown
// runtime. Preserve a possible member-loader call without resolving it.
if (
includeUnknown &&
mainModulePath &&
javaScriptRuntimeContext(state.program) === "unknown" &&
state.scopesById.get(binding.scopeId)?.kind === "program" &&
binding.declarationKinds.size === 1 &&
binding.declarationKinds.has("var") &&
!read.hasDefiniteInitializer
)
return true;
if (
value.name === "module" &&
copyAdmitsModuleDelegate &&
@@ -1549,40 +1666,89 @@ export const semanticGlobalProcessPathOrigin = (
};
};
/** Check one unknown-host wrapper candidate at its source-ordered read. */
const isConditionalRequireAt = (
root: t.Identifier,
node: t.Node,
state: JavaScriptSemanticAnalysisState,
allowAmbientModule = false,
): boolean => {
const runtime = javaScriptRuntimeContext(state.program);
if (
(runtime !== "unknown" && !(allowAmbientModule && runtime === "module")) ||
semanticResolutionBlocked(state, root, "require") ||
commonJsDelegateChangedAt(node, state)
)
return false;
const binding = resolveSemanticBindingState(state, root, "require");
if (
binding !== undefined &&
(binding.hostOrigin === "commonjs-wrapper" ||
state.scopesById.get(binding.scopeId)?.kind !== "program" ||
binding.declarationKinds.size !== 1 ||
!binding.declarationKinds.has("var") ||
(semanticBindingHasPriorValueWrite(node, binding, state) &&
!(
(t.isCallExpression(node) ||
t.isOptionalCallExpression(node) ||
t.isNewExpression(node)) &&
isWrapperRequireInitializerCall(binding, node, state)
)))
)
return false;
return !(state.ambientBindingWrites.get("require") ?? []).some(
(write) =>
!semanticTopLevelEffectFollowsCapture(node, write, state.parentsByNode),
);
};
/** Recover an unshadowed literal require origin and its exact member path. */
export const semanticRequireOrigin = (
export const semanticRequireOriginCandidate = (
node: t.Node | null | undefined,
state: JavaScriptSemanticAnalysisState,
allowAmbientModule = false,
): JavaScriptRequireOrigin | undefined => {
const chain = semanticMemberChain(node);
const current = chain?.root;
if (
chain === undefined ||
!t.isCallExpression(current) ||
(!t.isCallExpression(current) && !t.isOptionalCallExpression(current)) ||
!t.isIdentifier(current.callee, { name: "require" })
)
return undefined;
if (commonJsDelegateChangedAt(current, state)) return undefined;
const requireBinding = resolveSemanticBindingState(
state,
current.callee,
"require",
);
const binding = resolveSemanticBindingState(state, current.callee, "require");
const runtime = javaScriptRuntimeContext(state.program);
const candidate =
(runtime === "unknown" ||
(allowAmbientModule && runtime === "module" && binding === undefined)) &&
binding?.hostOrigin !== "commonjs-wrapper";
if (
(requireBinding !== undefined &&
!isUnmodifiedCommonJsWrapperBinding(current.callee, "require", state) &&
!isWrapperRequireInitializerCall(requireBinding, current, state)) ||
(requireBinding === undefined &&
(javaScriptRuntimeContext(state.program) === "module" ||
semanticResolutionBlocked(state, current.callee, "require"))) ||
(state.ambientBindingWrites.get("require") ?? []).some(
(write) =>
!semanticTopLevelEffectFollowsCapture(
candidate
? !isConditionalRequireAt(
current.callee,
current,
write,
state.parentsByNode,
),
)
state,
allowAmbientModule,
)
: commonJsDelegateChangedAt(current, state) ||
(binding !== undefined &&
!isUnmodifiedCommonJsWrapperBinding(
current.callee,
"require",
state,
) &&
!isWrapperRequireInitializerCall(binding, current, state)) ||
(binding === undefined &&
(javaScriptRuntimeContext(state.program) === "module" ||
semanticResolutionBlocked(state, current.callee, "require"))) ||
(state.ambientBindingWrites.get("require") ?? []).some(
(write) =>
!semanticTopLevelEffectFollowsCapture(
current,
write,
state.parentsByNode,
),
)
)
return undefined;
const specifier = stringValue(current.arguments[0]);
@@ -1593,11 +1759,21 @@ export const semanticRequireOrigin = (
specifier,
importedPath: chain.members,
resolutionMode: "require",
...(candidate ? { loaderAuthority: "candidate" as const } : {}),
},
call: current,
};
};
/** Only a proven host loader may establish Node builtin identity. */
export const semanticRequireOrigin = (
node: t.Node | null | undefined,
state: JavaScriptSemanticAnalysisState,
): JavaScriptRequireOrigin | undefined => {
const result = semanticRequireOriginCandidate(node, state);
return result?.origin.loaderAuthority === "candidate" ? undefined : result;
};
/** Node's exported child objects retain their direct builtin-module identity. */
export const semanticRequireExportsIdentity = <Key>(
specifier: string,
@@ -1656,4 +1832,5 @@ export const semanticMutableBuiltinOrigin = <Key = never>(
};
semanticRequireOriginReaders.origin = semanticRequireOrigin;
semanticRequireOriginReaders.candidate = semanticRequireOriginCandidate;
semanticRequireOriginReaders.loader = semanticRequireLoaderAt;
@@ -5,7 +5,7 @@ import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.
/** An unshadowed literal require and the member path selected from its result. */
export interface JavaScriptRequireOrigin {
readonly origin: JavaScriptModuleOrigin;
readonly call: t.CallExpression;
readonly call: t.CallExpression | t.OptionalCallExpression;
}
export type JavaScriptRequireLoaderFact =
@@ -33,12 +33,19 @@ type RequireLoaderReader = (
*/
export const semanticRequireOriginReaders: {
origin: RequireOriginReader | undefined;
candidate: RequireOriginReader | undefined;
loader: RequireLoaderReader | undefined;
} = { origin: undefined, loader: undefined };
} = { origin: undefined, candidate: undefined, loader: undefined };
export const readSemanticRequireOrigin: RequireOriginReader = (node, state) =>
semanticRequireOriginReaders.origin?.(node, state);
/** Includes conditional requests whose unbound wrapper is not established. */
export const readSemanticRequireOriginCandidate: RequireOriginReader = (
node,
state,
) => semanticRequireOriginReaders.candidate?.(node, state);
export const readSemanticRequireLoader: RequireLoaderReader = (
node,
state,
@@ -20,7 +20,7 @@ import {
type JavaScriptSemanticBindingState,
} from "./javascriptSemanticState.js";
import {
builtinDataEffectMethod,
builtinDataEffectIdentity,
dataEffectMemberCallee,
outerDataEffectBinding,
traverseDataEffects,
@@ -45,6 +45,7 @@ const releaseMethodsFor = (
};
interface ResourceCandidate {
readonly builtinAuthority?: "candidate";
readonly node: t.CallExpression | t.OptionalCallExpression;
readonly specifier: string;
readonly method: JavaScriptSemanticResourceOperation["method"];
@@ -339,6 +340,9 @@ const collectAcquisitions = (
node,
specifier: reference.specifier,
method: reference.method,
...(reference.builtinAuthority === undefined
? {}
: { builtinAuthority: reference.builtinAuthority }),
ownerCallableId: context.callableStack.at(-1) ?? null,
resultBindingId: outerDataEffectBinding(node, context),
});
@@ -391,6 +395,9 @@ const collectReleases = (
ownerCallableId: context.callableStack.at(-1) ?? null,
resultBindingId: binding.bindingId,
linkedResourceIds: linked.map(resourceCandidateId).sort(),
...(linked.some((value) => value.builtinAuthority === "candidate")
? { builtinAuthority: "candidate" as const }
: {}),
resolution:
linked.length === 1 &&
holdsOnlyAcquisitions(binding, linked, context.state)
@@ -440,6 +447,9 @@ const immutableAcquisition = (
resourceId: resourceCandidateId(candidate),
kind: "acquire",
method: candidate.method,
...(candidate.builtinAuthority === undefined
? {}
: { builtinAuthority: candidate.builtinAuthority }),
location: range(candidate.node),
ownerCallableId: candidate.ownerCallableId,
resultBindingId: candidate.resultBindingId,
@@ -448,6 +458,7 @@ const immutableAcquisition = (
});
interface AcquisitionReference {
readonly builtinAuthority?: "candidate";
readonly specifier: string;
readonly method: JavaScriptSemanticResourceOperation["method"];
}
@@ -457,7 +468,9 @@ const acquisitionMethod = (
state: JavaScriptSemanticAnalysisState,
): AcquisitionReference | null => {
for (const specifier of BUILTIN_RESOURCE_SPECIFIERS) {
const method = builtinDataEffectMethod(callee, state, [specifier]);
const identity = builtinDataEffectIdentity(callee, state, [specifier]);
const method =
identity?.path.length === 1 ? (identity.path[0] ?? null) : null;
if (
method !== null &&
(
@@ -468,6 +481,9 @@ const acquisitionMethod = (
)
return {
specifier,
...(identity?.builtinAuthority === undefined
? {}
: { builtinAuthority: identity.builtinAuthority }),
method: method as JavaScriptSemanticResourceOperation["method"],
};
}
@@ -82,6 +82,7 @@ export interface JavaScriptSemanticReferenceEffectsAtSite {
export interface JavaScriptSemanticRequiredModuleEffect extends JavaScriptSemanticMemberEffect {
readonly kind: "write" | "escape";
readonly loaderAuthority?: "candidate";
}
export interface JavaScriptSemanticBindingState {
@@ -192,7 +193,9 @@ export interface JavaScriptSemanticAnalysisState {
readonly callableNodesById: Map<string, t.Node>;
readonly moduleLinks: JavaScriptSemanticModuleLink[];
/** Exact calls already represented by binding or dependency links. */
readonly linkedRequireCalls: WeakSet<t.CallExpression>;
readonly linkedRequireCalls: WeakSet<
t.CallExpression | t.OptionalCallExpression
>;
readonly moduleLinkBindings: WeakMap<
JavaScriptSemanticModuleLink,
JavaScriptSemanticBindingState
@@ -42,7 +42,7 @@ import {
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticBindingState,
} from "./javascriptSemanticState.js";
import { readSemanticRequireOrigin } from "./javascriptSemanticRequireOriginSlot.js";
import { readSemanticRequireOriginCandidate } from "./javascriptSemanticRequireOriginSlot.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import {
readExactJavaScriptLiteral,
@@ -1230,17 +1230,18 @@ const provenanceForBindingInitializers = (
} finally {
context.bindings.delete(binding.bindingId);
}
if (resolved.status !== "module" || initializer.projection.length === 0)
if (resolved.origins.length === 0 || initializer.projection.length === 0)
return resolved;
return semanticOriginsProvenance(
resolved.origins.map((origin) => ({
...origin,
importedPath: [
...origin.importedPath,
...initializer.projection.map((segment) => String(segment)),
],
})),
);
const projected = resolved.origins.map((origin) => ({
...origin,
importedPath: [
...origin.importedPath,
...initializer.projection.map((segment) => String(segment)),
],
}));
return resolved.status === "ambiguous"
? semanticAmbiguousProvenance(projected, resolved.reason)
: semanticOriginsProvenance(projected);
};
const provenanceForExpression = (
@@ -1273,7 +1274,7 @@ const provenanceForExpression = (
expressionDepth: context.expressionDepth + unwrapped.depth,
});
}
const required = readSemanticRequireOrigin(node, context.state);
const required = readSemanticRequireOriginCandidate(node, context.state);
if (required !== undefined)
return semanticOriginsProvenance([required.origin]);
if (t.isSequenceExpression(node)) {
@@ -1319,14 +1320,14 @@ const provenanceForExpression = (
}
const base = provenanceForExpression(current, nestedContext(context));
const path = members.reverse();
return base.status !== "module"
? base
: semanticOriginsProvenance(
base.origins.map((origin) => ({
...origin,
importedPath: [...origin.importedPath, ...path],
})),
);
if (base.origins.length === 0) return base;
const projected = base.origins.map((origin) => ({
...origin,
importedPath: [...origin.importedPath, ...path],
}));
return base.status === "ambiguous"
? semanticAmbiguousProvenance(projected, base.reason)
: semanticOriginsProvenance(projected);
}
if (t.isConditionalExpression(node) || t.isLogicalExpression(node)) {
const candidates = semanticSelectedBranches(node, context).map((branch) =>
@@ -3,6 +3,7 @@ import { expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
import { topLevelBinding } from "./javascriptSemanticAnalysis.fixture.js";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
it.each([
'var require;require("./before.cjs");',
@@ -45,7 +46,7 @@ it.each([
"({require}={require:()=>({BrowserWindow:class{}})});",
"[require]=[()=>({BrowserWindow:class{}})];",
])("preserves an Electron capture before the projected write: %s", (write) => {
const analysis = analyzeJavaScriptStaticSource(
const analysis = analyzeCommonJsStaticFixture(
`var require;const {BrowserWindow}=require("electron");${write}new BrowserWindow({webPreferences:{preload:"./before.cjs"}});`,
);
expect(analysis.parse_error_count).toBe(0);
@@ -65,7 +66,7 @@ it.each([
])(
"distinguishes definite and conditional wrapper replacement: %s",
(write, unknown) => {
const analysis = analyzeJavaScriptStaticSource(
const analysis = analyzeCommonJsStaticFixture(
`var module;${write}module.require("./unused.cjs");`,
);
expect(analysis.parse_error_count).toBe(0);
@@ -120,9 +120,17 @@ ipcMain.handle("layout:ready",()=>pick);`;
for (const path of ["main.js", "main.cjs", "main.mjs", "main.jsx"]) {
const result = analyze(source, path);
expect(result?.errors).toBe(0);
expect(result?.facts.electron.ipc).toContainEqual(
expect.objectContaining({ channel: "layout:ready", operation: "handle" }),
);
if (path === "main.mjs") {
expect(result?.facts.electron.ipc).toEqual([]);
} else {
expect(result?.facts.electron.ipc).toContainEqual(
expect.objectContaining({
channel: "layout:ready",
operation: "handle",
...(path === "main.cjs" ? {} : { loader_authority: "candidate" }),
}),
);
}
}
});
+25 -19
View File
@@ -1,6 +1,11 @@
import { posix } from "node:path";
import { parse, type ParserOptions, type ParserPlugin } from "@babel/parser";
import * as t from "@babel/types";
import { javaScriptDefinitionExpression } from "./javascriptAstValues.js";
import {
explicitJavaScriptLoaderMode,
hasCaseVariantJavaScriptLoaderSuffix,
} from "./javascriptModuleResolutionMode.js";
import {
hasOnlyInlineTypeExportSpecifiers,
hasOnlyInlineTypeImportSpecifiers,
@@ -78,27 +83,31 @@ const parseWithRuntimeContext = (
packageContext?: JavaScriptPackageType,
): ParsedJavaScriptSource => {
const file = parse(source, options);
const filenameMode = explicitJavaScriptLoaderMode(sourcePath);
runtimeContextByProgram.set(
file.program,
/\.c[jt]s$/iu.test(sourcePath ?? "")
? /\.d\.cts$/iu.test(sourcePath ?? "")
filenameMode === "commonjs"
? sourcePath?.toLowerCase().endsWith(".d.cts")
? "unknown"
: "commonjs"
: isExplicitModuleSourcePath(sourcePath)
: filenameMode === "module"
? "module"
: packageContext === "module" || packageContext === "commonjs"
? packageContext
: packageContext === "unspecified" && /\.js$/iu.test(sourcePath ?? "")
: packageContext === "unspecified" &&
posix.extname(sourcePath ?? "") === ".js"
? file.program.sourceType === "module"
? "module"
: "commonjs"
: file.program.body.some((node) => t.isTSExportAssignment(node))
? // CommonJS isolation does not establish strict ESM semantics.
"commonjs"
: file.program.sourceType === "module" &&
hasRuntimeModuleDeclaration(file.program)
? "module"
: "unknown",
: hasCaseVariantJavaScriptLoaderSuffix(sourcePath)
? "unknown"
: file.program.body.some((node) => t.isTSExportAssignment(node))
? // CommonJS isolation does not establish strict ESM semantics.
"commonjs"
: file.program.sourceType === "module" &&
hasRuntimeModuleDeclaration(file.program)
? "module"
: "unknown",
);
return file;
};
@@ -141,16 +150,12 @@ export const parserPluginsForPath = (sourcePath?: string): ParserPlugin[] => {
return jsxCapable ? [...syntax, "jsx", typescript] : [...syntax, typescript];
};
/** `.mjs` and `.mts` explicitly select module mode. */
export const isExplicitModuleSourcePath = (path?: string): boolean =>
/\.m[jt]s$/iu.test(path ?? "");
const runtimeSourceTypeForPath = (
path?: string,
): "commonjs" | "module" | "unambiguous" =>
/\.cjs$/iu.test(path ?? "")
explicitJavaScriptLoaderMode(path) === "commonjs" && path?.endsWith(".cjs")
? "commonjs"
: isExplicitModuleSourcePath(path)
: explicitJavaScriptLoaderMode(path) === "module"
? "module"
: "unambiguous";
@@ -162,7 +167,7 @@ const packageContextForPath = (
sourcePath: string | undefined,
observedPackageType: JavaScriptPackageType | undefined,
): JavaScriptPackageType | undefined =>
/\.(?:js|ts)$/iu.test(sourcePath ?? "") &&
[".js", ".ts"].includes(posix.extname(sourcePath ?? "")) &&
!/\.d\.ts$/iu.test(sourcePath ?? "")
? observedPackageType
: undefined;
@@ -193,7 +198,8 @@ export const parseRecoveredJavaScript = (
? filenameType
: packageContext === "module"
? "module"
: packageContext === "commonjs" && /\.js$/iu.test(sourcePath ?? "")
: packageContext === "commonjs" &&
posix.extname(sourcePath ?? "") === ".js"
? "commonjs"
: "unambiguous";
if (sourceType !== "unambiguous") {
@@ -1,4 +1,9 @@
import { expect, it } from "vitest";
import {
isJsonModuleTarget,
isNativeAddonModuleTarget,
moduleResolutionModeForJavaScriptReference,
} from "./javascriptModuleResolutionMode.js";
import * as t from "@babel/types";
import {
@@ -21,18 +26,54 @@ import {
parserPluginsForPath,
} from "./javascriptSourceParser.js";
it("does not treat suffix-only dotfiles as JSON or native addon loaders", () => {
expect(isJsonModuleTarget(".json", "json")).toBe(false);
expect(isNativeAddonModuleTarget(".node", "native-addon")).toBe(false);
expect(isJsonModuleTarget("value.json", "json")).toBe(true);
expect(isNativeAddonModuleTarget("value.node", "native-addon")).toBe(true);
});
it.each([
["entry.cjs", "require"],
["entry.cts", "require"],
["entry.CJS", "import"],
["entry.CTS", "import"],
["entry.TS", "compiler-dependent"],
] as const)(
"uses exact loader evidence for static imports from %s",
(path, expected) => {
expect(
moduleResolutionModeForJavaScriptReference(path, "static-import"),
).toBe(expected);
},
);
it.each([
[".js", "module", "unknown"],
[".ts", "commonjs", "unknown"],
[".mjs", "commonjs", "unknown"],
[".cjs", "module", "unknown"],
["entry.cjs", "module", "commonjs"],
["entry.cts", "module", "commonjs"],
["entry.mjs", "commonjs", "module"],
["entry.mts", "commonjs", "module"],
["entry.MJS", "commonjs", "unknown"],
["entry.CJS", "module", "unknown"],
["entry.MTS", "commonjs", "unknown"],
["entry.CTS", "module", "unknown"],
["entry.d.cts", "module", "unknown"],
["entry.D.cts", "module", "unknown"],
["entry.d.ts", "commonjs", "unknown"],
["entry.d.mts", "commonjs", "module"],
["entry.jsx", "commonjs", "unknown"],
["entry.tsx", "commonjs", "unknown"],
["entry.js", "module", "module"],
["entry.ts", "commonjs", "commonjs"],
["entry.JS", "module", "unknown"],
["entry.JS", "commonjs", "unknown"],
["entry.JS", "unspecified", "unknown"],
["entry.TS", "module", "unknown"],
["entry.TS", "commonjs", "unknown"],
["entry.js", "unspecified", "commonjs"],
["entry.ts", "unspecified", "unknown"],
["entry.js", "unknown", "unknown"],
@@ -64,6 +105,34 @@ it("does not derive factory authority from an ambient require in a proven module
expect([...referenceSourceRequireCalls(file, "entry.js")]).toEqual([]);
});
it.each([
"entry.JS",
"entry.MJS",
"entry.CJS",
"entry.TS",
"entry.MTS",
"entry.CTS",
".MJS",
])("retains unknown host authority for mixed module syntax in %s", (path) => {
const { file } = parseRecoveredJavaScript(
'import {join} from "node:path"; require("./dep.cjs");',
path,
{ plugins: parserPluginsForPath(path) },
"module",
);
expect(javaScriptRuntimeContext(file.program)).toBe("unknown");
expect(analyzeParsedJavaScriptSemantics(file).moduleLinks).toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "import", specifier: "node:path" }),
expect.objectContaining({
kind: "require",
specifier: "./dep.cjs",
loaderAuthority: "candidate",
}),
]),
);
});
it.each([
["without source redeclarations", "", "", true],
["with a var redeclaration", "var require;\n", "", true],
@@ -112,6 +181,86 @@ it.each([
},
);
it.each([
["unbound", 'require("./dep.cjs");'],
["var redeclaration", 'var require; require("./dep.cjs");'],
["var initializer", 'var require; require = require("./dep.cjs");'],
] as const)(
"does not infer a CommonJS wrapper from %s in unknown runtime context",
(_name, source) => {
const { file } = parseRecoveredJavaScript(
source,
"entry.MJS",
{},
"commonjs",
);
expect(javaScriptRuntimeContext(file.program)).toBe("unknown");
const links = analyzeParsedJavaScriptSemantics(file).moduleLinks;
expect(links.filter((link) => link.specifier === "./dep.cjs")).toEqual([
expect.objectContaining({
kind: "require",
loaderAuthority: "candidate",
}),
]);
},
);
it.each([
["module.exports = 1;", "default"],
["var module; module.exports = 1;", "default"],
["var exports; exports.value = 1;", "value"],
] as const)("retains unknown-host export candidates for %s", (source, name) => {
const { file } = parseRecoveredJavaScript(
source,
"entry.MJS",
{},
"commonjs",
);
expect(analyzeParsedJavaScriptSemantics(file).moduleLinks).toEqual(
expect.arrayContaining([
expect.objectContaining({
kind: "commonjs-export",
exportedName: name,
loaderAuthority: "candidate",
}),
]),
);
});
it("does not claim an unbound module export in a known ES module", () => {
const { file } = parseRecoveredJavaScript(
"module.exports = 1;",
"entry.mjs",
{},
);
expect(analyzeParsedJavaScriptSemantics(file).moduleLinks).toEqual([]);
});
it("retains conditional require provenance through aliases without claiming a host loader", () => {
const source =
'const { ipcMain: bus } = require("electron"); const alias = bus;';
const { file } = parseRecoveredJavaScript(
source,
"entry.MJS",
{},
"commonjs",
);
const ir = analyzeParsedJavaScriptSemantics(file);
for (const name of ["bus", "alias"]) {
const binding = ir.bindings.find((item) => item.name === name);
expect(binding?.provenance).toMatchObject({
status: "ambiguous",
origins: [
{
specifier: "electron",
importedPath: ["ipcMain"],
loaderAuthority: "candidate",
},
],
});
}
});
it("detects JavaScript module syntax within an observed unspecified package", () => {
const { file } = parseRecoveredJavaScript(
"void import.meta.url;",
@@ -1,4 +1,5 @@
import { expect, it } from "vitest";
import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
it.each([
@@ -36,7 +37,6 @@ it.each([
'import Database from "better-sqlite3"; new Database("known.db");',
'import Database = require("better-sqlite3"); new Database("known.db");',
'import sqlite = require("sqlite3"); new sqlite.Database("known.db");',
'const Database = require("better-sqlite3"); new Database("known.db");',
'import {Database} from "sqlite3"; new Database("known.db");',
'import * as sqlite from "sqlite3"; new sqlite.Database("known.db");',
])("retains proven SQLite API origins: %s", (source) => {
@@ -48,3 +48,15 @@ it.each([
});
expect(analysis.storage[0]).not.toHaveProperty("interpretation");
});
it("distinguishes conditional and proven CommonJS SQLite origins", () => {
const source =
'const Database = require("better-sqlite3"); new Database("known.db");';
expect(analyzeJavaScriptStaticSource(source).storage).toMatchObject([
{ kind: "sqlite", name: "known.db", interpretation: "candidate" },
]);
const proven = analyzeCommonJsStaticFixture(source).storage;
expect(proven).toHaveLength(1);
expect(proven[0]).toMatchObject({ kind: "sqlite", name: "known.db" });
expect(proven[0]).not.toHaveProperty("interpretation");
});
@@ -17,7 +17,7 @@ import {
} from "./javascriptSemanticTraversal.js";
import {
isElectronRendererLoad,
electronRendererLoadAuthority,
resolveElectronBrowserWindowReceiver,
} from "./electronStaticRenderer.js";
import { finishElectronNativeBindingsSteps } from "./electronStaticAnalysisNative.js";
@@ -121,7 +121,7 @@ export function* analyzeParsedJavaScriptStaticSourceSteps(
browserWindowReceiver: (node, site, suffix) =>
resolveElectronBrowserWindowReceiver(node, site, suffix, findings, index),
rendererLoad: (callee, site) =>
isElectronRendererLoad(callee, site, findings, index),
electronRendererLoadAuthority(callee, site, findings, index),
source,
accumulator,
vitePreloadDependencyTable: indexVitePreloadDependencyTable(
@@ -130,9 +130,12 @@ export function* analyzeParsedJavaScriptStaticSourceSteps(
),
openReceiverFacts: receivers.openReceivers,
electronBindings: electronIdentity.bindings,
electronCandidateBindings: electronIdentity.candidateBindings,
electronExportIntact: electronIdentity.intactExport,
electronInvocationPath: electronIdentity.invocationPath,
electronAppPathChangePossible: electronIdentity.mayChangeAppPath,
electronCandidateAppPathChangePossible:
electronIdentity.candidateMayChangeAppPath,
rpcScan: createJavaScriptRpcScan(source, receivers.rpcClientRoots, index),
expressionShapes: createSemanticExpressionShapeResolver(bindings),
};
@@ -211,6 +214,7 @@ const finalizeStaticAnalysis = (
value.operation === "load" &&
ELECTRON_REFERENCE_KINDS.has(value.kind) &&
ELECTRON_MODULE.test(value.specifier) &&
value.loader_authority !== "candidate" &&
// A `factory-loader` reference names a recovered bundle module, so its
// specifier is a module key inside the artifact rather than a package. A
// real external load in the same file keeps its `commonjs-loader`
@@ -218,6 +222,14 @@ const finalizeStaticAnalysis = (
value.loader !== "factory-loader",
);
const resourceLimits = [...bindings.valueResourceLimits].sort();
const candidateElectron = [
...accumulator.browserWindows,
...accumulator.contextBridgeApis,
...accumulator.ipc,
...accumulator.utilityProcesses,
...accumulator.protocols,
...accumulator.appPathChanges,
].some(({ value }) => value.loader_authority === "candidate");
const coverage: JavaScriptStaticCoverage = {
syntax:
parserErrors === 0
@@ -282,6 +294,11 @@ const finalizeStaticAnalysis = (
]
: []),
...(electronObserved ? [ELECTRON_IDENTITY_LIMITATION] : []),
...(candidateElectron
? [
"Some Electron syntax depends on an unproved CommonJS loader; its Electron identity remains conditional.",
]
: []),
"JavaScript syntax was parsed as data and was never evaluated.",
];
return {
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
import { aliasSelectionBudgetSource } from "../../../tests/fixtures/javascriptAliasSelectionBudget.js";
const expressionWrappers = [
@@ -408,7 +409,7 @@ it("retains endpoint observations for exact template and TypeScript member keys"
it("preserves preload and utility paths through exact computed join members", () => {
for (const key of ['"join"', "`join`", '"join" as const']) {
const analysis = analyzeJavaScriptStaticSource(`
const analysis = analyzeCommonJsStaticFixture(`
const { BrowserWindow, utilityProcess } = require("electron");
const path = require("node:path");
new BrowserWindow({ webPreferences: {
@@ -491,7 +492,7 @@ it("keeps dynamic, interpolated, and invalid template keys unresolved", () => {
]) {
const preload = `path[${key}](__dirname, "preload.js")`;
const worker = `path[${key}](__dirname, "worker.js")`;
const analysis = analyzeJavaScriptStaticSource(`
const analysis = analyzeCommonJsStaticFixture(`
const { BrowserWindow, utilityProcess } = require("electron");
const path = require("node:path");
client[${key}]("/api/items");
@@ -86,10 +86,26 @@ export const inspectCall = (
bindings,
accumulator.factoryRequireBindingIds,
) !== "local";
const unwrappedCallee = unwrapJavaScriptExpression(node.callee).node;
const invocationMember =
(t.isMemberExpression(unwrappedCallee) ||
t.isOptionalMemberExpression(unwrappedCallee)) &&
["call", "apply"].includes(
semanticStaticPropertyKey(
unwrappedCallee.property,
unwrappedCallee.computed,
) ?? "",
);
const deferredModuleRequire =
semanticModuleRequireReceiver(node) !== undefined ||
semanticModuleRequireCallAt(node, bindings, false) ||
deferredRequireResolve;
deferredRequireResolve ||
(invocationMember &&
semanticRequireCallAt(
node,
bindings,
accumulator.factoryRequireBindingIds,
) !== undefined);
if (deferredModuleRequire) accumulator.pendingModuleRequireCalls.push(node);
const requireCall = deferredModuleRequire
? null
@@ -106,6 +122,9 @@ export const inspectCall = (
...(requireCall.loader === undefined
? {}
: { loader: requireCall.loader }),
...(requireCall.loaderAuthority === undefined
? {}
: { loaderAuthority: requireCall.loaderAuthority }),
});
const chunkLoad = chunkLoadSpecifier(node, bindings, context);
if (chunkLoad !== null)
@@ -160,9 +179,16 @@ export const inspectCall = (
});
inspectEndpointCall(node, name, first, context, bindings);
inspectStorageCall(node, name, first, context, bindings);
const renderer =
name === "loadFile" ||
name.endsWith(".loadFile") ||
name === "loadURL" ||
name.endsWith(".loadURL")
? context.rendererLoad?.(node.callee, node)
: undefined;
if (
(name === "loadFile" || name.endsWith(".loadFile")) &&
context.rendererLoad?.(node.callee, node) === true
renderer !== undefined
) {
const argument = argumentNode(node.arguments[0]);
const filePath =
@@ -185,6 +211,7 @@ export const inspectCall = (
: {}),
resolution_context: "filesystem-expression",
mechanism: `call:${name}`,
...renderer,
module_key: null,
location: range(node),
},
@@ -192,7 +219,7 @@ export const inspectCall = (
}
if (
(name === "loadURL" || name.endsWith(".loadURL")) &&
context.rendererLoad?.(node.callee, node) === true
renderer !== undefined
) {
if (first === undefined) accumulator.unknownFindings += 1;
else
@@ -205,6 +232,7 @@ export const inspectCall = (
path: first,
resolution_context: "module-specifier",
mechanism: `call:${name}`,
...renderer,
module_key: null,
location: range(node),
},
@@ -279,28 +307,33 @@ const requireCallSpecifier = (
node: JavaScriptCallLike,
bindings: JavaScriptSemanticAnalysisState,
context: FindingContext,
resolveMemberChanged = false,
index?: JavaScriptBindingIndex,
): {
readonly specifier: string | undefined;
readonly operation: "load" | "resolve";
readonly resolutionBasePath?: string;
readonly loader?: "commonjs-loader" | "factory-loader" | "create-require";
readonly loaderAuthority?: "candidate";
} | null => {
const call = semanticRequireCallAt(
node,
bindings,
context.accumulator.factoryRequireBindingIds,
index,
);
if (call === undefined || call.loader === "local") return null;
if (
resolveMemberChanged ||
call.loader === "unknown" ||
!call.sourceRelative
) {
if (call.loader === "unknown" || !call.sourceRelative) {
context.accumulator.unknownFindings += 1;
const candidate =
!call.memberUnproven && call.loaderAuthority === "candidate"
? (argumentValue(call.arguments?.[0]) ?? undefined)
: undefined;
return {
specifier: undefined,
operation: resolveMemberChanged ? "load" : call.operation,
specifier: candidate,
operation: call.operation,
...(candidate === undefined
? {}
: { loaderAuthority: "candidate" as const }),
};
}
return {
@@ -325,23 +358,7 @@ export const finishModuleRequireCalls = (
index: JavaScriptBindingIndex,
): void => {
for (const node of context.accumulator.pendingModuleRequireCalls) {
const resolveMemberChanged =
isRequireResolveCall(node) &&
(t.isMemberExpression(node.callee) ||
t.isOptionalMemberExpression(node.callee)) &&
index
.effectsForReferenceAt(node, node.callee.object, ["resolve"])
.effects.some(
({ kind, timing }) =>
timing !== "after" &&
(kind === "member-write" || kind === "escape"),
);
const call = requireCallSpecifier(
node,
index.bindings,
context,
resolveMemberChanged,
);
const call = requireCallSpecifier(node, index.bindings, context, index);
if (call !== null)
addReference(context, {
node,
@@ -355,6 +372,9 @@ export const finishModuleRequireCalls = (
// still names a bundle module key, not a package. Dropping the loader here
// would read that key as an external specifier.
...(call.loader === undefined ? {} : { loader: call.loader }),
...(call.loaderAuthority === undefined
? {}
: { loaderAuthority: call.loaderAuthority }),
});
}
context.accumulator.pendingModuleRequireCalls.length = 0;
@@ -363,14 +383,14 @@ export const finishModuleRequireCalls = (
);
};
const isRequireResolveCall = (
node: JavaScriptCallLike,
): node is t.CallExpression | t.OptionalCallExpression =>
(t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
(t.isMemberExpression(node.callee) ||
t.isOptionalMemberExpression(node.callee)) &&
semanticStaticPropertyKey(node.callee.property, node.callee.computed) ===
"resolve";
const isRequireResolveCall = (node: JavaScriptCallLike): boolean => {
if (t.isNewExpression(node)) return false;
const callee = unwrapJavaScriptExpression(node.callee).node;
return (
(t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
semanticStaticPropertyKey(callee.property, callee.computed) === "resolve"
);
};
const chunkLoadSpecifier = (
node: JavaScriptCallLike,
@@ -551,7 +571,8 @@ const inspectEndpointCall = (
context: FindingContext,
bindings: JavaScriptSemanticAnalysisState,
): void => {
if (isKnownNetworkRequest(node, bindings, context)) {
const networkAuthority = networkRequestAuthority(node, bindings, context);
if (networkAuthority !== undefined) {
const target = argumentNode(node.arguments[0]);
if (target === undefined) return;
const literal =
@@ -565,6 +586,9 @@ const inspectEndpointCall = (
kind: "network",
value: literal,
mechanism: `call:${name}`,
...(networkAuthority === "candidate"
? { interpretation: "candidate" }
: {}),
});
return;
}
@@ -578,7 +602,7 @@ const inspectEndpointCall = (
context.accumulator.pendingNetworkCalls.push({ node, name });
return;
}
addPartialNetworkEndpoint(context, node, name, target);
addPartialNetworkEndpoint(context, node, name, target, networkAuthority);
return;
}
// A local or shadowed function with a familiar network name is not a known
@@ -642,21 +666,26 @@ const isNetworkRequestSpelling = (name: string): boolean =>
name === "navigator.sendBeacon";
/** Confirm that the call resolves to a built-in network API at this source site. */
const isKnownNetworkRequest = (
const networkRequestAuthority = (
node: JavaScriptCallLike,
bindings: JavaScriptSemanticAnalysisState,
context: FindingContext,
): boolean => {
if (isBrowserNetworkRequest(node, bindings)) return true;
): "proven" | "candidate" | undefined => {
if (isBrowserNetworkRequest(node, bindings)) return "proven";
const callee = unwrapJavaScriptExpression(node.callee).node;
if (
(t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
(t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
semanticStaticPropertyKey(callee.property, callee.computed) === "loadURL" &&
context.rendererLoad?.(node.callee, node) === true
)
return true;
return false;
semanticStaticPropertyKey(callee.property, callee.computed) === "loadURL"
) {
const renderer = context.rendererLoad?.(node.callee, node);
return renderer === undefined
? undefined
: renderer.loader_authority === "candidate"
? "candidate"
: "proven";
}
return undefined;
};
/** Finish queued targets after shared member-mutation facts are indexed. */
@@ -666,7 +695,8 @@ export const finishPartialNetworkEndpoints = (
index: JavaScriptBindingIndex,
): void => {
for (const { node, name } of context.accumulator.pendingNetworkCalls) {
if (!isKnownNetworkRequest(node, bindings, context)) {
const authority = networkRequestAuthority(node, bindings, context);
if (authority === undefined) {
if (!isIntrinsicMapLookup(node, bindings, index))
inspectLiteralEndpointCall(node, name, context);
continue;
@@ -680,8 +710,9 @@ export const finishPartialNetworkEndpoints = (
kind: "network",
value: resolved,
mechanism: `call:${name}`,
...(authority === "candidate" ? { interpretation: "candidate" } : {}),
});
else addPartialNetworkEndpoint(context, node, name, target);
else addPartialNetworkEndpoint(context, node, name, target, authority);
}
context.accumulator.pendingNetworkCalls.length = 0;
context.accumulator.endpoints.sort(
@@ -694,6 +725,7 @@ const addPartialNetworkEndpoint = (
node: JavaScriptCallLike,
name: string,
target: t.Node,
authority: "proven" | "candidate",
): void =>
addEndpoint(context, {
node,
@@ -701,6 +733,7 @@ const addPartialNetworkEndpoint = (
// Partial values preserve the original expression text, byte for byte.
value: sourceSlice(context.source, target),
mechanism: `call:${name}`,
...(authority === "candidate" ? { interpretation: "candidate" } : {}),
resolution: "partial",
});
@@ -997,6 +1030,9 @@ export const addReference = (
? {}
: { resolution_base_path: input.resolutionBasePath }),
...(input.loader === undefined ? {} : { loader: input.loader }),
...(input.loaderAuthority === undefined
? {}
: { loader_authority: input.loaderAuthority }),
module_key: null,
location: range(input.node),
},
@@ -4,6 +4,7 @@ import { runInNewContext } from "node:vm";
import { expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
it("keeps repeated Node resolver calls separate from loader delegate eligibility", () => {
const expression = "path.resolve(runtimePath,__dirname,'preload.js')";
@@ -33,7 +34,7 @@ it("keeps repeated Node resolver calls separate from loader delegate eligibility
"/fixture/app/preload.js",
"/fixture/app/preload.js",
]);
const analysis = analyzeJavaScriptStaticSource(source);
const analysis = analyzeCommonJsStaticFixture(source);
expect(analysis.parse_status).toBe("complete");
expect(analysis.references).toEqual([
expect.objectContaining({ kind: "require", specifier: "node:path" }),
@@ -1,7 +1,15 @@
import { runInNewContext } from "node:vm";
import * as t from "@babel/types";
import { indexJavaScriptBindingsWithReceiversSteps } from "./javascriptSemanticAnalysis.js";
import { completeSemanticSteps } from "./javascriptSemanticTraversal.js";
import { semanticRequireCallAt } from "./javascriptSemanticRequireOrigin.js";
import { expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import {
analyzeJavaScriptStaticSource,
analyzeParsedJavaScriptStaticSource,
} from "./javascriptStaticAnalysis.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
const changedIterator =
"Array.prototype[Symbol.iterator]=function*(){yield '../shared/window.cjs'};";
@@ -116,7 +124,103 @@ it.each(cases)(
const analysis = analyzeJavaScriptStaticSource(source);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.references).toEqual([
expect.objectContaining({ kind: "require", specifier }),
expect.objectContaining({
kind: "require",
specifier,
...(specifier === null ? {} : { loader_authority: "candidate" }),
}),
]);
},
);
it.each(["call", "apply"])(
"keeps overwritten require.%s unresolved",
(method) => {
for (const path of ["fixture.MJS", "fixture.cjs"]) {
const argument =
method === "call" ? 'null,"./a.cjs"' : 'null,["./a.cjs"]';
const source = `require.${method}=custom;require.${method}(${argument});`;
const unrelated = `require.other=custom;require.${method}(${argument});`;
const parse = (text: string) => {
const parsed = parseJavaScriptSource(text, path);
if (parsed === null) throw new Error("fixture did not parse");
return analyzeParsedJavaScriptStaticSource(text, parsed).references.at(
-1,
);
};
expect(parse(source)).toMatchObject({ kind: "require", specifier: null });
expect(parse(unrelated)).toMatchObject({
kind: "require",
specifier: "./a.cjs",
...(path === "fixture.MJS"
? { loader_authority: "candidate" }
: { loader: "commonjs-loader" }),
});
}
},
);
it("does not prove invocation members before the mutation index is complete", () => {
const source = 'require.call=custom;require?.call(null,"./a.cjs");';
const parsed = parseJavaScriptSource(source, "fixture.cjs");
if (parsed === null) throw new Error("fixture did not parse");
const index = completeSemanticSteps(
indexJavaScriptBindingsWithReceiversSteps(parsed.program),
);
const statement = parsed.program.body[1];
if (
!t.isExpressionStatement(statement) ||
!t.isOptionalCallExpression(statement.expression)
)
throw new Error("expected optional call");
const fact = semanticRequireCallAt(
statement.expression,
index.bindings,
new Set(),
index,
);
expect(fact).toMatchObject({ loader: "unknown", memberUnproven: true });
});
it("does not turn a mutated local callable into a loader candidate", () => {
const source =
'const f=()=>0;f.call=()=>0;const addon=f.call(null,"./x.node");';
const parsed = parseJavaScriptSource(source, "fixture.cjs");
if (parsed === null) throw new Error("fixture did not parse");
const result = analyzeParsedJavaScriptStaticSource(source, parsed);
expect(result.references).toEqual([]);
expect(result.electron.native_addon_bindings).toEqual([]);
expect(result.parse_status).toBe("complete");
});
it.each(["fixture.cjs", "fixture.MJS"])(
"does not invent nested loader requests after an inner invocation member is replaced: %s",
(path) => {
const parse = (source: string) => {
const parsed = parseJavaScriptSource(source, path);
if (parsed === null) throw new Error("fixture did not parse");
return analyzeParsedJavaScriptStaticSource(source, parsed);
};
const changed = parse(
'require.call=custom;const addon=require.call.call(require,"./receiver.cjs","./actual.node");',
);
expect(
changed.references.every(
(ref) =>
ref.specifier !== "./actual.node" &&
ref.specifier !== "./receiver.cjs",
),
).toBe(true);
expect(changed.electron.native_addon_bindings).toEqual([]);
const intact = parse('require.call.call(require,null,"./actual.node");');
expect(intact.references).toContainEqual(
expect.objectContaining({
kind: "require",
specifier: "./actual.node",
...(path.endsWith(".MJS")
? { loader_authority: "candidate" }
: { loader: "commonjs-loader" }),
}),
);
},
);
@@ -27,7 +27,7 @@ it.each([
expect(result.parse_status).toBe("partial");
});
it("retains complete coverage for an inline Electron factory export", () => {
it("retains conditional coverage for an inline Electron factory export", () => {
const result = analyzeJavaScriptStaticSource(`(() => {
const modules={
5(module,exports){
@@ -46,14 +46,42 @@ it("retains complete coverage for an inline Electron factory export", () => {
}
load(9);
})();`);
expect(result.parse_status).toBe("complete");
expect(result.parse_status).toBe("partial");
expect(result.references).toContainEqual(
expect.objectContaining({ kind: "require", specifier: "electron" }),
expect.objectContaining({
kind: "require",
specifier: "electron",
loader_authority: "candidate",
}),
);
expect(result.references.every(({ specifier }) => specifier !== null)).toBe(
true,
);
expect(result.electron.ipc).toContainEqual(
expect.objectContaining({ channel: "test:copy", side: "main" }),
expect.objectContaining({
channel: "test:copy",
side: "main",
loader_authority: "candidate",
}),
);
});
it("keeps captured unknown-host loader authority without promoting shadowed values", () => {
const cases = [
['const load=require;load("./candidate.cjs");', true],
['const load=require;require=()=>0;load("./candidate.cjs");', true],
['const require=()=>0;const load=require;load("./candidate.cjs");', false],
['require=()=>0;const load=require;load("./candidate.cjs");', false],
] as const;
for (const [source, candidate] of cases) {
const result = analyzeJavaScriptStaticSource(source);
expect(
result.references.some(
(reference) =>
reference.specifier === "./candidate.cjs" &&
reference.loader_authority === "candidate",
),
source,
).toBe(candidate);
}
});
@@ -3,11 +3,37 @@ import * as path from "node:path";
import { runInNewContext } from "node:vm";
import { expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import {
analyzeJavaScriptStaticSource,
analyzeParsedJavaScriptStaticSource,
} from "./javascriptStaticAnalysis.js";
import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import { referenceSourceRequireCalls } from "../referenceSourceRequireBindings.js";
import { parseReferenceSourceImports } from "../referenceSourceImportParsing.js";
it.each([
'(require.resolve as any)("./dep.cjs");',
'(require as any).resolve("./dep.cjs");',
'(require as any)?.["resolve"]("./dep.cjs");',
])(
"retains an unknown-host resolver through TypeScript wrappers: %s",
(source) => {
const parsed = parseJavaScriptSource(source, "entry.TS");
if (parsed === null) throw new Error("Expected TypeScript fixture");
expect(
analyzeParsedJavaScriptStaticSource(source, parsed).references,
).toContainEqual(
expect.objectContaining({
kind: "require",
specifier: "./dep.cjs",
operation: "resolve",
loader_authority: "candidate",
}),
);
},
);
it.each([
["require", "load"],
["require?.", "load"],
@@ -70,9 +96,60 @@ it.each([
);
const imported = "import {createRequire as make} from 'node:module';";
it.each([
["call", 'null,"./valid.cjs"', 'null,"./invented.cjs"'],
["apply", 'null,["./valid.cjs"]', 'null,["./invented.cjs"]'],
])(
"distinguishes nested loader-%s replacement from a child property write",
(method, valid, invented) => {
const references = (source: string) =>
analyzeCommonJsStaticFixture(source).references.map(
({ operation, specifier }) => ({ operation, specifier }),
);
expect(
references(
`const box={load:require};box.load.${method}.extra=1;box.load.${method}(${valid});`,
),
).toContainEqual({ operation: "load", specifier: "./valid.cjs" });
expect(
references(
`const box={load:require};box.load.${method}=custom;box.load.${method}(${invented});`,
),
).not.toContainEqual({ operation: "load", specifier: "./invented.cjs" });
},
);
it("preserves a native resolver before a later loader receiver escape", () => {
const references = (source: string) =>
analyzeCommonJsStaticFixture(source).references.map(
({ operation, specifier }) => ({ operation, specifier }),
);
expect(
references(
'require.resolve.paths=custom;require.resolve("./first.cjs");require.call.extra=1;require.call(null,"./second.cjs");',
),
).toEqual([
{ operation: "resolve", specifier: "./first.cjs" },
{ operation: "load", specifier: "./second.cjs" },
]);
expect(
references('opaque(require);require.resolve("./unsafe.cjs");').at(-1),
).toEqual({
operation: "load",
specifier: null,
});
for (const source of [
'function run(){require.resolve("./unsafe.cjs");opaque(require);}run();run();',
'function mutate(){opaque(require)}mutate();require.resolve("./unsafe.cjs");',
]) {
expect(references(source)).not.toContainEqual({
operation: "resolve",
specifier: "./unsafe.cjs",
});
}
});
it("retains consecutive native resolver calls while keeping replaced or escaped loaders uncertain", () => {
const references = (source: string) =>
analyzeJavaScriptStaticSource(source).references.map(
analyzeCommonJsStaticFixture(source).references.map(
({ operation, specifier }) => ({ operation, specifier }),
);
expect(
@@ -293,7 +370,7 @@ it.each([
require: (request: string) => module.require(request),
});
expect(result.join === path.join).toBe(host);
const analysis = analyzeJavaScriptStaticSource(source);
const analysis = analyzeCommonJsStaticFixture(source);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.references).toEqual(
specifier === undefined
@@ -493,3 +570,23 @@ it("retains unknown historical .js loader scope until a package manifest establi
}),
]);
});
it.each(["main.cjs", "main.MJS"])(
"does not use an unproven invocation receiver as a dependency: %s",
(path) => {
for (const source of [
'require.call=custom;require.call("./receiver.cjs","./actual.cjs");',
'require.call=custom;require.call.call(require,"./receiver.cjs","./actual.cjs");',
'require.call=custom;require.call.apply(require,["./receiver.cjs","./actual.cjs"]);',
]) {
const result = parseReferenceSourceImports(
path,
new TextEncoder().encode(source),
"JavaScript",
);
expect(
result.relationships.filter(({ kind }) => kind === "requires"),
).toEqual([]);
}
},
);
@@ -88,3 +88,16 @@ it.each(cases)(
);
},
);
it("keeps unknown wrapper eligibility separate from local replacement", () => {
const local = analyzeJavaScriptStaticSource(
'var require; require={main:{require:()=>0}}; require.main.require("node:path");',
);
expect(local.references).toEqual([]);
const conditional = analyzeJavaScriptStaticSource(
'var require; if(flag) require={main:{require:()=>0}}; require.main.require("node:path");',
);
expect(conditional.references).toMatchObject([
{ kind: "require", specifier: null },
]);
});
@@ -110,10 +110,17 @@ export interface JavaScriptFindingContext {
site: t.Node,
suffix: readonly string[],
) =>
| { readonly owner: t.NewExpression; readonly path: readonly string[] }
| {
readonly owner: t.NewExpression;
readonly path: readonly string[];
readonly authority: { readonly loader_authority?: "candidate" };
}
| undefined;
readonly xhrOpenRequest?: (node: t.Node) => boolean;
readonly rendererLoad?: (callee: t.Node, site: t.Node) => boolean;
readonly rendererLoad?: (
callee: t.Node,
site: t.Node,
) => { readonly loader_authority?: "candidate" } | undefined;
readonly rpcScan: JavaScriptRpcScan;
/** Shared bounded argv/nullish argument shapes for optional-slot signatures. */
readonly expressionShapes: JavaScriptExpressionShapeResolver;
@@ -123,12 +130,15 @@ export interface JavaScriptFindingContext {
readonly openReceiverFacts?: ReadonlyMap<number, JavaScriptOpenReceiverFact>;
/** Proven Electron export path segments by root offset. */
readonly electronBindings?: ReadonlyMap<number, readonly string[]>;
/** Conditional Electron export paths, keyed by call/construction root. */
readonly electronCandidateBindings?: ReadonlyMap<number, readonly string[]>;
/** Normalized invocation target from the shared Electron and intrinsic owners. */
readonly electronInvocationPath?: (
node: t.CallExpression | t.OptionalCallExpression,
) => readonly string[] | undefined;
/** A possible shared-app mutation does not establish concrete API identity. */
readonly electronAppPathChangePossible?: (node: t.Node) => boolean;
readonly electronCandidateAppPathChangePossible?: (node: t.Node) => boolean;
}
/** Candidate import, require, worker, or service-worker reference. */
@@ -139,6 +149,7 @@ export interface JavaScriptReferenceInput {
readonly specifier: string | undefined;
readonly resolutionBasePath?: string;
readonly loader?: JavaScriptStaticReference["loader"];
readonly loaderAuthority?: "candidate";
}
/** Candidate route or network endpoint literal. */
@@ -50,6 +50,8 @@ interface JavaScriptStaticReferenceState {
* when it reads like a package name.
*/
readonly loader?: "commonjs-loader" | "factory-loader" | "create-require";
/** The literal is observed but the CommonJS loader is not established. */
readonly loader_authority?: "candidate";
readonly location: JavaScriptSourceRange;
}
@@ -128,6 +130,7 @@ export interface JavaScriptBundlerRegistration {
/** Static Electron role paths discovered from package or JavaScript syntax. */
export interface JavaScriptRolePath {
readonly role: "preload" | "renderer";
readonly loader_authority?: "candidate";
readonly path: string;
readonly resolution_context: JavaScriptStaticPathContext;
/** Inferred from intact source-directory intrinsics; not a runtime/bundler guarantee. */
@@ -1,5 +1,6 @@
import { expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
import {
javaScriptStaticParseStatus,
unexaminedJavaScriptStaticCoverage,
@@ -7,7 +8,7 @@ import {
import { aliasSelectionBudgetSource } from "../../../tests/fixtures/javascriptAliasSelectionBudget.js";
it("separates dynamic semantic uncertainty from completed syntax and examination", () => {
const result = analyzeJavaScriptStaticSource(
const result = analyzeCommonJsStaticFixture(
"const {ipcMain}=require('electron'); ipcMain.on(channel, () => {});",
);
expect(result.parse_status).toBe("partial");
@@ -46,7 +47,7 @@ it("keeps framework applicability unknown without positive Electron evidence", (
const ordinary = analyzeJavaScriptStaticSource("export const value = 1;");
expect(ordinary.parse_status).toBe("complete");
expect(ordinary.analysis_coverage?.applicability.electron).toBe("unknown");
const electron = analyzeJavaScriptStaticSource(
const electron = analyzeCommonJsStaticFixture(
"const {ipcMain}=require('electron');",
);
expect(electron.analysis_coverage?.applicability.electron).toBe("observed");
@@ -82,6 +82,7 @@ it("substitutes numeric modules, named exports and captured wrapper arguments",
const env = resolver.bind(fn, []);
expect(resolver.evaluate(fn.node.body.callee, env)).toEqual({
kind: "external",
loaderAuthority: "candidate",
path: ["electron", "ipcRenderer", "send"],
});
expect(resolver.arguments(fn.node.body, env)[0]).toEqual({
@@ -127,7 +128,11 @@ it("checks block lexical initializers against their containing function call", (
);
},
});
expect(resolved).toEqual({ kind: "external", path: ["electron"] });
expect(resolved).toEqual({
kind: "external",
loaderAuthority: "candidate",
path: ["electron"],
});
});
it("keeps same-factory reads before initialization unresolved", () => {
@@ -236,8 +241,16 @@ it("keeps function declarations hoisted and checks closures at invocation", () =
inspect(localRead());
`);
expect(inspected[0]).toMatchObject({ kind: "function" });
expect(inspected[1]).toEqual({ kind: "external", path: ["electron"] });
expect(inspected[2]).toEqual({ kind: "external", path: ["electron"] });
expect(inspected[1]).toEqual({
kind: "external",
loaderAuthority: "candidate",
path: ["electron"],
});
expect(inspected[2]).toEqual({
kind: "external",
loaderAuthority: "candidate",
path: ["electron"],
});
});
it("keeps a synchronous cross-function callback before initialization unresolved", () => {
@@ -288,7 +301,11 @@ it("uses completed activation provenance for a returned closure", () => {
const value = require("electron");
return () => value;
}`);
expect(result).toEqual({ kind: "external", path: ["electron"] });
expect(result).toEqual({
kind: "external",
loaderAuthority: "candidate",
path: ["electron"],
});
});
it("does not treat statements after a returned closure as initialized", () => {
@@ -898,7 +915,11 @@ it.each([
})();`);
expect(value.kind).toBe(kind);
if (kind === "external")
expect(value).toEqual({ kind: "external", path: ["electron"] });
expect(value).toEqual({
kind: "external",
loaderAuthority: "candidate",
path: ["electron"],
});
},
);
@@ -910,7 +931,11 @@ it("reads a literal prototype only from its actual prototype setter", () => {
inspectOne(
'inspect(({__proto__:require("electron"),ordinary:1}).__proto__);',
),
).toEqual({ kind: "external", path: ["electron"] });
).toEqual({
kind: "external",
loaderAuthority: "candidate",
path: ["electron"],
});
});
it.each(["local", "outer"])(
@@ -1031,3 +1056,23 @@ it("keeps repeated cached queries within an independent depth budget", () => {
expect(values).toHaveLength(1);
expect(resolver.truncated).toBe(false);
});
it("retains each external selection guard once across deep member reads", () => {
const { parsed, resolver } = resolverFor("if (guard) {};");
const condition = { node: parsed.program.body[0]!, positive: true as const };
let value: SymbolicValue = {
kind: "external",
path: ["electron"],
loaderAuthority: "candidate",
conditions: [condition],
};
for (let index = 0; index < 32; index++) {
value = resolver.property(value, `member${index}`);
}
expect(value).toMatchObject({
kind: "external",
loaderAuthority: "candidate",
conditions: [condition],
});
expect(resolver.truncated).toBe(false);
});
@@ -11,6 +11,8 @@ import {
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import { createSemanticExpressionShapeResolver } from "./javascriptSemanticExpressionShapes.js";
import { semanticRequireOriginCandidate } from "./javascriptSemanticRequireOrigin.js";
import { javaScriptRuntimeContext } from "./javascriptSourceParser.js";
import { createGlobalReferenceReader } from "./javascriptSemanticAmbientWrites.js";
import { javascriptBootstrapAt } from "./javascriptBootstrap.js";
import { ELECTRON_MODULE } from "./javascriptElectronMemberWrites.js";
@@ -358,7 +360,13 @@ export class JavaScriptSymbolicBindings {
) {
return symbolicUnknown("overwritten Electron API member");
}
return { kind: "external", path };
return {
kind: "external",
path,
...(value.loaderAuthority === "candidate"
? { loaderAuthority: "candidate" as const }
: {}),
};
}
if (value.kind === "record") {
const property = value.values.get(key);
@@ -765,13 +773,25 @@ export class JavaScriptSymbolicBindings {
if (!binding)
return !semanticResolutionBlocked(this.state, node, node.name) &&
["require", "navigator", "String", "Boolean"].includes(node.name)
? { kind: "external", path: [node.name] }
? node.name === "require" &&
javaScriptRuntimeContext(this.state.program) === "module"
? symbolicUnknown("CommonJS require is unavailable in an ES module")
: {
kind: "external",
path: [node.name],
...(node.name === "require" &&
javaScriptRuntimeContext(this.state.program) === "unknown"
? { loaderAuthority: "candidate" as const }
: {}),
}
: symbolicUnknown("unbound identifier");
if (binding.definitions.some((d) => d.kind === "assignment"))
return symbolicUnknown("reassigned binding");
// Electron member identity is checked at the selected property. A write
// to one method must not erase the module origin or unaffected siblings.
const electronOrigin = this.#electronIdentity.exportPath(node);
const electronOrigin =
this.#electronIdentity.exportPath(node) ??
this.#electronIdentity.candidateExportPath(node);
if (electronOrigin === undefined && this.#mutated.has(binding.bindingId))
return symbolicUnknown("dynamic member writes");
for (let e: SymbolicEnvironment | undefined = env; e; e = e.parent) {
@@ -1176,7 +1196,15 @@ export class JavaScriptSymbolicBindings {
const spec = args[0];
result =
spec?.kind === "literal" && typeof spec.value === "string"
? { kind: "external", path: [spec.value] }
? {
kind: "external",
path: [spec.value],
...(semanticRequireOriginCandidate(node, this.state)?.origin
.loaderAuthority === "candidate" ||
value.loaderAuthority === "candidate"
? { loaderAuthority: "candidate" as const }
: {}),
}
: symbolicUnknown("dynamic require");
} else if (
value.kind !== "function" ||
@@ -47,7 +47,11 @@ export type SymbolicValue = (
readonly kind: "literal";
readonly value: string | number | boolean | null | undefined;
}
| { readonly kind: "external"; readonly path: readonly string[] }
| {
readonly kind: "external";
readonly path: readonly string[];
readonly loaderAuthority?: "candidate";
}
| {
readonly kind: "object";
readonly node: t.ObjectExpression;