mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(javascript): require API identity for static application roles
This commit is contained in:
@@ -249,7 +249,8 @@ utility-process runtime paths retain the runtime filename selected by the caller
|
||||
Selected bounded text is then parsed as inert data to recover:
|
||||
|
||||
- `package.json` metadata and declared main or renderer entrypoints;
|
||||
- Electron preload paths and renderer files visible in static syntax;
|
||||
- Electron preload paths owned by proven BrowserWindow options and renderer
|
||||
files loaded through proven BrowserWindow instances;
|
||||
- local HTML script entrypoints;
|
||||
- Webpack and Rspack chunk registrations and module factories represented as
|
||||
AST literals, plus esbuild `__commonJS` module tables; recovered esbuild
|
||||
@@ -366,6 +367,18 @@ prefix is never presented as a complete stable fingerprint.
|
||||
|
||||
## Authority and unknowns
|
||||
|
||||
API identity is a derivation: a familiar method or property name does not
|
||||
establish a browser or Electron API. The shared lexical, ambient-write, and
|
||||
member-mutation owners must establish the receiver before static API facts become
|
||||
roles or storage observations. Path `join`, `resolve`, and `fileURLToPath` use
|
||||
proven Node imports; source-relative URL conversion additionally requires an
|
||||
unmodified global URL constructor and `import.meta.url`. Other calls preserve
|
||||
their expressions. A property named `preload` outside a proven BrowserWindow
|
||||
configuration does not establish a script entrypoint. Renderer recovery currently
|
||||
covers direct BrowserWindow instances, stable local aliases, and their
|
||||
`webContents`; other receiver-producing APIs remain unmodeled. Complete AST
|
||||
traversal does not imply complete API-model coverage.
|
||||
|
||||
Artifact bytes and AST syntax are observations. Entrypoint resolution, imports,
|
||||
loads, calls, and persistence relationships are static inferences and explicitly
|
||||
say that syntax does not prove runtime execution. A malformed or unavailable
|
||||
|
||||
@@ -2937,7 +2937,7 @@ describe("contextual JavaScript module identity", () => {
|
||||
new BrowserWindow({ webPreferences: {
|
||||
preload: new URL("./remote-base.js", location.href)
|
||||
}});
|
||||
window.loadURL("file:///renderer/index.html");
|
||||
new BrowserWindow().loadURL("file:///renderer/index.html");
|
||||
`,
|
||||
);
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ const inspectBrowserWindow = (
|
||||
context.source,
|
||||
lookup.status,
|
||||
resolvedPreferences,
|
||||
context.nodePathResolve,
|
||||
context.pathIntrinsics,
|
||||
);
|
||||
context.accumulator.unknownFindings += collected.unknown;
|
||||
const finding: ElectronBrowserWindowFinding = {
|
||||
@@ -154,6 +154,23 @@ const inspectBrowserWindow = (
|
||||
module_key: null,
|
||||
location: range(node),
|
||||
};
|
||||
if (
|
||||
finding.preload_path !== null &&
|
||||
finding.preload_resolution_context !== null
|
||||
)
|
||||
addLocatedFinding(context, {
|
||||
collection: context.accumulator.roles,
|
||||
key: `role\0preload\0${finding.preload_path}`,
|
||||
node,
|
||||
value: {
|
||||
role: "preload",
|
||||
path: finding.preload_path,
|
||||
resolution_context: finding.preload_resolution_context,
|
||||
mechanism: "BrowserWindow:webPreferences.preload",
|
||||
module_key: null,
|
||||
location: range(node),
|
||||
},
|
||||
});
|
||||
addLocatedFinding(context, {
|
||||
collection: context.accumulator.browserWindows,
|
||||
key: "electron-window\0BrowserWindow",
|
||||
@@ -166,7 +183,7 @@ const collectWindowOptions = (
|
||||
source: string,
|
||||
lookupStatus: "explicit" | "missing" | "dynamic",
|
||||
preferences: t.Node | undefined,
|
||||
nodePathResolve?: JavaScriptStaticPathIntrinsicQuery,
|
||||
pathIntrinsics?: JavaScriptStaticPathIntrinsicQuery,
|
||||
): {
|
||||
readonly status: ElectronBrowserWindowFinding["web_preferences_status"];
|
||||
readonly preferences: readonly ElectronWebPreference[];
|
||||
@@ -180,13 +197,13 @@ const collectWindowOptions = (
|
||||
preload: { preload_path: null, preload_resolution_context: null },
|
||||
unknown: lookupStatus === "missing" ? 0 : 1,
|
||||
};
|
||||
return collectWebPreferences(source, preferences, nodePathResolve);
|
||||
return collectWebPreferences(source, preferences, pathIntrinsics);
|
||||
};
|
||||
|
||||
const collectWebPreferences = (
|
||||
source: string,
|
||||
object: t.ObjectExpression,
|
||||
nodePathResolve?: JavaScriptStaticPathIntrinsicQuery,
|
||||
pathIntrinsics?: JavaScriptStaticPathIntrinsicQuery,
|
||||
): {
|
||||
readonly status: "object-literal";
|
||||
readonly preferences: readonly ElectronWebPreference[];
|
||||
@@ -198,7 +215,7 @@ const collectWebPreferences = (
|
||||
const effectivePreload = staticObjectProperty(object, "preload");
|
||||
const preloadPath =
|
||||
effectivePreload.status === "explicit"
|
||||
? staticPath(effectivePreload.property.value, nodePathResolve)
|
||||
? staticPath(effectivePreload.property.value, pathIntrinsics)
|
||||
: undefined;
|
||||
const preload: ElectronBrowserWindowPreload =
|
||||
effectivePreload.status === "explicit" && preloadPath !== undefined
|
||||
@@ -206,7 +223,7 @@ const collectWebPreferences = (
|
||||
preload_path: preloadPath,
|
||||
preload_resolution_context: staticPathResolutionContext(
|
||||
effectivePreload.property.value,
|
||||
nodePathResolve,
|
||||
pathIntrinsics,
|
||||
),
|
||||
}
|
||||
: { preload_path: null, preload_resolution_context: null };
|
||||
@@ -238,7 +255,7 @@ const collectWebPreferences = (
|
||||
}
|
||||
const path =
|
||||
name === "preload"
|
||||
? staticPath(property.value, nodePathResolve)
|
||||
? staticPath(property.value, pathIntrinsics)
|
||||
: undefined;
|
||||
const value: ElectronStaticValue =
|
||||
path === undefined
|
||||
@@ -316,7 +333,7 @@ const inspectUtilityProcess = (
|
||||
const modulePath =
|
||||
moduleNode === undefined
|
||||
? undefined
|
||||
: staticPath(moduleNode, context.nodePathResolve);
|
||||
: staticPath(moduleNode, context.pathIntrinsics);
|
||||
// Electron treats a non-array object in the argv slot as the options.
|
||||
const shapes = context.expressionShapes;
|
||||
const options = optionsAfterOptionalArgv(node.arguments, shapes);
|
||||
@@ -333,7 +350,7 @@ const inspectUtilityProcess = (
|
||||
const serviceName =
|
||||
declaredName === undefined
|
||||
? undefined
|
||||
: staticPath(declaredName, context.nodePathResolve);
|
||||
: staticPath(declaredName, context.pathIntrinsics);
|
||||
if (
|
||||
serviceNameProperty.status === "dynamic" ||
|
||||
(declaredName !== undefined && serviceName === undefined)
|
||||
@@ -351,7 +368,7 @@ const inspectUtilityProcess = (
|
||||
module_path: modulePath,
|
||||
module_resolution_context: staticPathResolutionContext(
|
||||
moduleNode,
|
||||
context.nodePathResolve,
|
||||
context.pathIntrinsics,
|
||||
),
|
||||
module_expression: null,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import { electronCalleePath } from "./electronStaticAnalysisValues.js";
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
|
||||
import {
|
||||
semanticStableInitializer,
|
||||
type JavaScriptBindingIndex,
|
||||
} from "./javascriptSemanticState.js";
|
||||
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 = (
|
||||
callee: t.Node,
|
||||
site: t.Node,
|
||||
context: JavaScriptFindingContext,
|
||||
index: JavaScriptBindingIndex,
|
||||
): boolean => {
|
||||
callee = unwrapJavaScriptExpression(callee).node;
|
||||
if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee))
|
||||
return false;
|
||||
const method = semanticStaticPropertyKey(callee.property, callee.computed);
|
||||
if (method !== "loadFile" && method !== "loadURL") return false;
|
||||
let receiver = unwrapJavaScriptExpression(callee.object).node;
|
||||
const path: string[] = [method];
|
||||
if (
|
||||
t.isMemberExpression(receiver) &&
|
||||
semanticStaticPropertyKey(receiver.property, receiver.computed) ===
|
||||
"webContents"
|
||||
) {
|
||||
receiver = unwrapJavaScriptExpression(receiver.object).node;
|
||||
path.unshift("webContents");
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
while (t.isIdentifier(receiver)) {
|
||||
const binding = index.bindingForReferenceAt(receiver);
|
||||
if (binding === undefined || seen.has(binding.bindingId)) return false;
|
||||
seen.add(binding.bindingId);
|
||||
const initializer = semanticStableInitializer(binding, index.bindings);
|
||||
if (
|
||||
initializer === undefined ||
|
||||
initializer.projection.length !== 0 ||
|
||||
!semanticBindingAtRead(binding, receiver, index.bindings.parentsByNode)
|
||||
.hasDefiniteInitializer
|
||||
)
|
||||
return false;
|
||||
completeSemanticSteps(index.indexMemberMutationFactsSteps());
|
||||
if (index.referenceMayHaveChangedAt(site, receiver, path)) return false;
|
||||
receiver = unwrapJavaScriptExpression(initializer.node).node;
|
||||
}
|
||||
if (!t.isNewExpression(receiver)) return false;
|
||||
const constructor = electronCalleePath(receiver.callee, context);
|
||||
return (
|
||||
constructor?.length === 1 &&
|
||||
constructor[0] === "BrowserWindow" &&
|
||||
context.electronExportIntact?.([
|
||||
"electron",
|
||||
"BrowserWindow",
|
||||
"prototype",
|
||||
...path,
|
||||
]) === true
|
||||
);
|
||||
};
|
||||
@@ -14,10 +14,11 @@ import {
|
||||
traverseJavaScriptAstSteps,
|
||||
} from "./javascriptSemanticTraversal.js";
|
||||
|
||||
import { isElectronRendererLoad } from "./electronStaticRenderer.js";
|
||||
import { inspectElectronStaticNode } from "./electronStaticAnalysis.js";
|
||||
import { hasPossibleCommonJsDelegateWrite } from "./javascriptSemanticRequireOrigin.js";
|
||||
import { semanticResourceLimitReason } from "./javascriptSemanticResourceLimits.js";
|
||||
import { createStaticNodePathResolveQuery } from "./javascriptStaticPathIntrinsics.js";
|
||||
import { createStaticPathIntrinsicQuery } from "./javascriptStaticPathIntrinsics.js";
|
||||
import { createSemanticExpressionShapeResolver } from "./javascriptSemanticExpressionShapes.js";
|
||||
import { finishElectronBrowserWindowsSteps } from "./electronStaticAnalysisBrowser.js";
|
||||
import {
|
||||
@@ -34,7 +35,6 @@ import {
|
||||
finishModuleRequireCalls,
|
||||
finishSqliteStorageCalls,
|
||||
inspectCall,
|
||||
inspectRoleProperty,
|
||||
inspectRpcNode,
|
||||
finishRpcScanSteps,
|
||||
indexVitePreloadDependencyTable,
|
||||
@@ -103,12 +103,15 @@ export function* analyzeParsedJavaScriptStaticSourceSteps(
|
||||
bindings,
|
||||
);
|
||||
const findings: JavaScriptFindingContext = {
|
||||
nodePathResolve: createStaticNodePathResolveQuery(index),
|
||||
pathIntrinsics: createStaticPathIntrinsicQuery(index),
|
||||
rendererLoad: (callee, site) =>
|
||||
isElectronRendererLoad(callee, site, findings, index),
|
||||
source,
|
||||
accumulator,
|
||||
vitePreloadDependencyTable: indexVitePreloadDependencyTable(file.program),
|
||||
openReceiverFacts: receivers.openReceivers,
|
||||
electronBindings: electronIdentity.bindings,
|
||||
electronExportIntact: electronIdentity.intactExport,
|
||||
electronInvocationPath: electronIdentity.invocationPath,
|
||||
electronAppPathChangePossible: electronIdentity.mayChangeAppPath,
|
||||
rpcScan: createJavaScriptRpcScan(source, receivers.rpcClientRoots, index),
|
||||
@@ -330,8 +333,5 @@ const inspectNode = (
|
||||
kind: "dynamic-import",
|
||||
specifier: stringValue(node.source),
|
||||
});
|
||||
if (t.isObjectProperty(node)) {
|
||||
inspectRoleProperty(node, findings);
|
||||
}
|
||||
inspectRpcNode(node, findings);
|
||||
};
|
||||
|
||||
@@ -433,7 +433,12 @@ it("preserves preload and utility paths through exact computed join members", ()
|
||||
),
|
||||
)
|
||||
.toEqual([
|
||||
["preload", "preload.js", "filesystem-expression", "property:preload"],
|
||||
[
|
||||
"preload",
|
||||
"preload.js",
|
||||
"filesystem-expression",
|
||||
"BrowserWindow:webPreferences.preload",
|
||||
],
|
||||
]);
|
||||
expect
|
||||
.soft(
|
||||
@@ -541,11 +546,14 @@ it("keeps dynamic, interpolated, and invalid template keys unresolved", () => {
|
||||
describe("static callee boundary matching", () => {
|
||||
it("matches complete callee members without suffix collisions", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
const {BrowserWindow} = require("electron");
|
||||
const win = new BrowserWindow();
|
||||
win.loadFile("./window.html");
|
||||
BrowserWindow.loadURL("https://app.example.test");
|
||||
new BrowserWindow().loadURL("https://app.example.test");
|
||||
navigator.serviceWorker.register("./worker.js");
|
||||
loadFile("./bare.html");
|
||||
win?.loadURL("https://optional.example.test");
|
||||
const optional = new BrowserWindow();
|
||||
optional?.loadURL("https://optional.example.test");
|
||||
|
||||
uploadFile("./upload.html");
|
||||
preloadFile("./preload.html");
|
||||
@@ -559,9 +567,8 @@ describe("static callee boundary matching", () => {
|
||||
analysis.role_paths.map(({ path, mechanism }) => [path, mechanism]),
|
||||
).toEqual([
|
||||
["./window.html", "call:win.loadFile"],
|
||||
["https://app.example.test", "call:BrowserWindow.loadURL"],
|
||||
["./bare.html", "call:loadFile"],
|
||||
["https://optional.example.test", "call:win.loadURL"],
|
||||
["https://app.example.test", "call:loadURL"],
|
||||
["https://optional.example.test", "call:optional.loadURL"],
|
||||
]);
|
||||
// The document base URL is unknown, so the script path stays unresolved.
|
||||
expect(
|
||||
|
||||
@@ -17,9 +17,6 @@ import {
|
||||
range,
|
||||
rangeForOffsets,
|
||||
sourceSlice,
|
||||
staticPath,
|
||||
staticPathResolutionContext,
|
||||
storageKind,
|
||||
stringValue,
|
||||
} from "./javascriptStaticAnalysisHelpers.js";
|
||||
import {
|
||||
@@ -147,11 +144,10 @@ export const inspectCall = (
|
||||
specifier: moduleUrlSpecifier(node, bindings),
|
||||
});
|
||||
inspectEndpointCall(node, name, first, context, bindings);
|
||||
inspectStorageCall(node, name, first, context);
|
||||
if (first === "audio" || first === "video")
|
||||
noteMediaElementPreloads(node, accumulator);
|
||||
inspectStorageCall(node, name, first, context, bindings);
|
||||
if (
|
||||
(name === "loadFile" || name.endsWith(".loadFile")) &&
|
||||
context.rendererLoad?.(node.callee, node) === true &&
|
||||
first !== undefined
|
||||
)
|
||||
addLocatedFinding(context, {
|
||||
@@ -167,7 +163,11 @@ export const inspectCall = (
|
||||
location: range(node),
|
||||
},
|
||||
});
|
||||
if ((name === "loadURL" || name.endsWith(".loadURL")) && first !== undefined)
|
||||
if (
|
||||
(name === "loadURL" || name.endsWith(".loadURL")) &&
|
||||
context.rendererLoad?.(node.callee, node) === true &&
|
||||
first !== undefined
|
||||
)
|
||||
addLocatedFinding(context, {
|
||||
collection: accumulator.roles,
|
||||
key: `role\0renderer\0${first}`,
|
||||
@@ -246,23 +246,6 @@ const workerConstructorKind = (
|
||||
: undefined;
|
||||
};
|
||||
|
||||
// Element factories such as jsx("audio", props) and createElement("video",
|
||||
// props) carry the HTML media `preload` hint ("none", "metadata", "auto"),
|
||||
// which never names an Electron preload script.
|
||||
const noteMediaElementPreloads = (
|
||||
node: JavaScriptCallLike,
|
||||
accumulator: AnalysisAccumulator,
|
||||
): void => {
|
||||
const props = node.arguments[1];
|
||||
if (!t.isObjectExpression(props)) return;
|
||||
for (const property of props.properties)
|
||||
if (
|
||||
t.isObjectProperty(property) &&
|
||||
semanticStaticPropertyName(property.key, property.computed) === "preload"
|
||||
)
|
||||
accumulator.mediaElementPreloads.add(property);
|
||||
};
|
||||
|
||||
const requireCallSpecifier = (
|
||||
node: JavaScriptCallLike,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
@@ -824,37 +807,12 @@ export function* finishRpcScanSteps(
|
||||
}
|
||||
}
|
||||
|
||||
export const inspectRoleProperty = (
|
||||
node: t.ObjectProperty,
|
||||
context: FindingContext,
|
||||
): void => {
|
||||
if (semanticStaticPropertyName(node.key, node.computed) !== "preload") return;
|
||||
if (context.accumulator.mediaElementPreloads.has(node)) return;
|
||||
const path = staticPath(node.value, context.nodePathResolve);
|
||||
if (path === undefined) return;
|
||||
addLocatedFinding(context, {
|
||||
collection: context.accumulator.roles,
|
||||
key: `role\0preload\0${path}`,
|
||||
node,
|
||||
value: {
|
||||
role: "preload",
|
||||
path,
|
||||
resolution_context: staticPathResolutionContext(
|
||||
node.value,
|
||||
context.nodePathResolve,
|
||||
),
|
||||
mechanism: "property:preload",
|
||||
module_key: null,
|
||||
location: range(node),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const inspectStorageCall = (
|
||||
node: JavaScriptCallLike,
|
||||
name: string,
|
||||
first: string | undefined,
|
||||
context: FindingContext,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
): void => {
|
||||
if (name === "Database" || name.endsWith(".Database")) {
|
||||
context.accumulator.pendingSqliteCalls.push({ node, name });
|
||||
@@ -870,7 +828,7 @@ const inspectStorageCall = (
|
||||
? "indexed-db"
|
||||
: receiverFact === "cache-storage"
|
||||
? "cache-storage"
|
||||
: storageKind(name);
|
||||
: browserStorageKind(node.callee, bindings);
|
||||
if (storage === undefined) return;
|
||||
addLocatedFinding(context, {
|
||||
collection: context.accumulator.storage,
|
||||
@@ -886,6 +844,35 @@ const inspectStorageCall = (
|
||||
});
|
||||
};
|
||||
|
||||
const browserStorageKind = (
|
||||
callee: t.Node,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
):
|
||||
| "local-storage"
|
||||
| "session-storage"
|
||||
| "indexed-db"
|
||||
| "cache-storage"
|
||||
| undefined => {
|
||||
callee = unwrapJavaScriptExpression(callee).node;
|
||||
if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee))
|
||||
return undefined;
|
||||
const method = semanticStaticPropertyKey(callee.property, callee.computed);
|
||||
if (method === null) return undefined;
|
||||
if (["getItem", "setItem", "removeItem", "clear", "key"].includes(method)) {
|
||||
if (isUnmodifiedGlobalReference(callee, bindings, ["localStorage", method]))
|
||||
return "local-storage";
|
||||
if (
|
||||
isUnmodifiedGlobalReference(callee, bindings, ["sessionStorage", method])
|
||||
)
|
||||
return "session-storage";
|
||||
}
|
||||
if (isUnmodifiedGlobalReference(callee, bindings, ["indexedDB", "open"]))
|
||||
return "indexed-db";
|
||||
if (isUnmodifiedGlobalReference(callee, bindings, ["caches", "open"]))
|
||||
return "cache-storage";
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Constructor spelling selects a candidate; source provenance owns certainty. */
|
||||
export const finishSqliteStorageCalls = (
|
||||
context: FindingContext,
|
||||
|
||||
@@ -173,29 +173,34 @@ export const staticArrayValues = (
|
||||
/** Classify whether inert path syntax is a module specifier or file expression. */
|
||||
export type JavaScriptStaticPathIntrinsicQuery = (
|
||||
node: t.Node,
|
||||
fact?: "source-directory",
|
||||
fact?:
|
||||
| "resolve"
|
||||
| "join"
|
||||
| "file-url-to-path"
|
||||
| "source-url"
|
||||
| "source-directory",
|
||||
) => boolean;
|
||||
|
||||
export const staticPathResolutionContext = (
|
||||
node: t.Node,
|
||||
nodePathResolve?: JavaScriptStaticPathIntrinsicQuery,
|
||||
pathIntrinsics?: JavaScriptStaticPathIntrinsicQuery,
|
||||
): JavaScriptStaticPathContext =>
|
||||
isFilesystemPathExpression(node, nodePathResolve)
|
||||
isFilesystemPathExpression(node, pathIntrinsics)
|
||||
? "filesystem-expression"
|
||||
: "module-specifier";
|
||||
|
||||
/** Resolve a path string composed only from inert literal syntax. */
|
||||
export const staticPath = (
|
||||
node: t.Node,
|
||||
nodePathResolve?: JavaScriptStaticPathIntrinsicQuery,
|
||||
pathIntrinsics?: JavaScriptStaticPathIntrinsicQuery,
|
||||
): string | undefined => {
|
||||
node = unwrapJavaScriptExpression(node).node;
|
||||
const value = stringValue(node);
|
||||
if (value !== undefined) return value;
|
||||
if (t.isBinaryExpression(node, { operator: "+" }))
|
||||
return staticConcatenation(node, nodePathResolve);
|
||||
return staticConcatenation(node, pathIntrinsics);
|
||||
if (t.isCallExpression(node) || t.isNewExpression(node))
|
||||
return staticCallPath(node, nodePathResolve);
|
||||
return staticCallPath(node, pathIntrinsics);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -206,7 +211,7 @@ export const staticPath = (
|
||||
*/
|
||||
const staticConcatenation = (
|
||||
node: t.BinaryExpression,
|
||||
nodePathResolve?: JavaScriptStaticPathIntrinsicQuery,
|
||||
pathIntrinsics?: JavaScriptStaticPathIntrinsicQuery,
|
||||
): string | undefined => {
|
||||
const operands: t.Node[] = [];
|
||||
let current: t.Node = node;
|
||||
@@ -214,11 +219,11 @@ const staticConcatenation = (
|
||||
operands.push(current.right);
|
||||
current = unwrapJavaScriptExpression(current.left).node;
|
||||
}
|
||||
const anchored = isDirectoryPath(current, nodePathResolve);
|
||||
const anchored = isDirectoryPath(current, pathIntrinsics);
|
||||
if (!anchored) operands.push(current);
|
||||
let text = "";
|
||||
for (const operand of operands.reverse()) {
|
||||
const value = staticPath(operand, nodePathResolve);
|
||||
const value = staticPath(operand, pathIntrinsics);
|
||||
if (value === undefined) return undefined;
|
||||
text += value;
|
||||
}
|
||||
@@ -228,26 +233,27 @@ const staticConcatenation = (
|
||||
|
||||
const staticCallPath = (
|
||||
node: t.CallExpression | t.NewExpression,
|
||||
nodePathResolve?: JavaScriptStaticPathIntrinsicQuery,
|
||||
pathIntrinsics?: JavaScriptStaticPathIntrinsicQuery,
|
||||
): string | undefined => {
|
||||
if (t.isCallExpression(node) && nodePathResolve?.(node.callee))
|
||||
return staticResolvedPath(node, nodePathResolve);
|
||||
const name = calleeName(node.callee);
|
||||
if (name === "fileURLToPath" || name.endsWith(".fileURLToPath")) {
|
||||
return staticFileUrlPath(node, nodePathResolve);
|
||||
}
|
||||
if (name.endsWith(".resolve")) return undefined;
|
||||
if (!name.endsWith(".join") && name !== "join") return undefined;
|
||||
if (t.isCallExpression(node) && pathIntrinsics?.(node.callee))
|
||||
return staticResolvedPath(node, pathIntrinsics);
|
||||
if (
|
||||
t.isCallExpression(node) &&
|
||||
pathIntrinsics?.(node.callee, "file-url-to-path")
|
||||
)
|
||||
return staticFileUrlPath(node, pathIntrinsics);
|
||||
if (!t.isCallExpression(node) || !pathIntrinsics?.(node.callee, "join"))
|
||||
return undefined;
|
||||
const parts: string[] = [];
|
||||
for (const argument of node.arguments) {
|
||||
if (isDirectoryPath(argumentNode(argument), nodePathResolve)) {
|
||||
if (isDirectoryPath(argumentNode(argument), pathIntrinsics)) {
|
||||
// join never resets at an absolute segment, so `/preload.js` after the
|
||||
// source directory stays inside it.
|
||||
parts.push(".");
|
||||
continue;
|
||||
}
|
||||
const value = t.isNode(argument)
|
||||
? staticPath(argument, nodePathResolve)
|
||||
? staticPath(argument, pathIntrinsics)
|
||||
: undefined;
|
||||
if (value === undefined) return undefined;
|
||||
parts.push(value);
|
||||
@@ -270,15 +276,15 @@ export const staticSourceRelativeUrlPath = (
|
||||
/** Convert an exact static file URL expression to a POSIX path. */
|
||||
export const staticFileUrlPath = (
|
||||
node: t.CallExpression | t.NewExpression,
|
||||
nodePathResolve?: JavaScriptStaticPathIntrinsicQuery,
|
||||
pathIntrinsics?: JavaScriptStaticPathIntrinsicQuery,
|
||||
): string | undefined => {
|
||||
if (node.arguments.length !== 1) return undefined;
|
||||
const rawArgument = argumentNode(node.arguments[0]);
|
||||
if (rawArgument === undefined) return undefined;
|
||||
const argument = unwrapJavaScriptExpression(rawArgument).node;
|
||||
if (isSourceFileUrl(argument))
|
||||
if (isSourceFileUrl(argument) && pathIntrinsics?.(argument, "source-url"))
|
||||
return sourceRelativeFileUrlPath(argument)?.path;
|
||||
const value = staticPath(argument, nodePathResolve);
|
||||
const value = staticPath(argument, pathIntrinsics);
|
||||
const rawPath = value?.split(/[?#]/u, 1)[0] ?? "";
|
||||
if (
|
||||
value === undefined ||
|
||||
@@ -354,19 +360,19 @@ const isSourceFileUrl = (
|
||||
|
||||
const staticResolvedPath = (
|
||||
node: t.CallExpression | t.NewExpression,
|
||||
nodePathResolve?: JavaScriptStaticPathIntrinsicQuery,
|
||||
pathIntrinsics?: JavaScriptStaticPathIntrinsicQuery,
|
||||
): string | undefined => {
|
||||
const parts: string[] = [];
|
||||
let anchored = false;
|
||||
// Node walks right-to-left and never inspects arguments before its anchor.
|
||||
for (let index = node.arguments.length - 1; index >= 0; index -= 1) {
|
||||
const argument = node.arguments[index];
|
||||
if (isDirectoryPath(argumentNode(argument), nodePathResolve)) {
|
||||
if (isDirectoryPath(argumentNode(argument), pathIntrinsics)) {
|
||||
anchored = true;
|
||||
break;
|
||||
}
|
||||
const value = t.isNode(argument)
|
||||
? staticPath(argument, nodePathResolve)
|
||||
? staticPath(argument, pathIntrinsics)
|
||||
: undefined;
|
||||
if (value === undefined) return undefined;
|
||||
parts.push(value);
|
||||
@@ -383,45 +389,27 @@ const staticResolvedPath = (
|
||||
|
||||
const isFilesystemPathExpression = (
|
||||
node: t.Node,
|
||||
nodePathResolve?: JavaScriptStaticPathIntrinsicQuery,
|
||||
pathIntrinsics?: JavaScriptStaticPathIntrinsicQuery,
|
||||
): boolean => {
|
||||
node = unwrapJavaScriptExpression(node).node;
|
||||
const value = stringValue(node);
|
||||
if (value !== undefined) return value.startsWith("/");
|
||||
if (isFileIdentity(node) || isDirectoryPath(node, nodePathResolve))
|
||||
if (isFileIdentity(node) || isDirectoryPath(node, pathIntrinsics))
|
||||
return true;
|
||||
if (t.isBinaryExpression(node, { operator: "+" }))
|
||||
return (
|
||||
isFilesystemPathExpression(node.left, nodePathResolve) ||
|
||||
isFilesystemPathExpression(node.right, nodePathResolve)
|
||||
isFilesystemPathExpression(node.left, pathIntrinsics) ||
|
||||
isFilesystemPathExpression(node.right, pathIntrinsics)
|
||||
);
|
||||
if (t.isCallExpression(node) || t.isNewExpression(node)) {
|
||||
const name = calleeName(node.callee);
|
||||
if (
|
||||
nodePathResolve?.(node.callee) &&
|
||||
staticCallPath(node, nodePathResolve) !== undefined
|
||||
)
|
||||
return true;
|
||||
// A URL object is not a path string until fileURLToPath converts it.
|
||||
if (name === "URL" || name.endsWith(".URL")) return false;
|
||||
if (name === "fileURLToPath" || name.endsWith(".fileURLToPath"))
|
||||
return (
|
||||
staticFileUrlPath(node, nodePathResolve) !== undefined ||
|
||||
node.arguments.some((argument) => {
|
||||
const value = argumentNode(argument);
|
||||
return (
|
||||
value !== undefined &&
|
||||
isFilesystemPathExpression(value, nodePathResolve)
|
||||
);
|
||||
})
|
||||
);
|
||||
return node.arguments.some((argument) => {
|
||||
const value = argumentNode(argument);
|
||||
return (
|
||||
value !== undefined &&
|
||||
isFilesystemPathExpression(value, nodePathResolve)
|
||||
);
|
||||
});
|
||||
if (!t.isCallExpression(node)) return false;
|
||||
const native =
|
||||
pathIntrinsics?.(node.callee) ||
|
||||
pathIntrinsics?.(node.callee, "join") ||
|
||||
pathIntrinsics?.(node.callee, "file-url-to-path");
|
||||
return (
|
||||
native === true && staticCallPath(node, pathIntrinsics) !== undefined
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -429,11 +417,11 @@ const isFilesystemPathExpression = (
|
||||
/** `__dirname` or the dirname of the source file, as a string. */
|
||||
const isDirectoryPath = (
|
||||
node: t.Node | undefined,
|
||||
nodePathResolve?: JavaScriptStaticPathIntrinsicQuery,
|
||||
pathIntrinsics?: JavaScriptStaticPathIntrinsicQuery,
|
||||
): boolean => {
|
||||
if (node !== undefined) node = unwrapJavaScriptExpression(node).node;
|
||||
return (
|
||||
node !== undefined && nodePathResolve?.(node, "source-directory") === true
|
||||
node !== undefined && pathIntrinsics?.(node, "source-directory") === true
|
||||
);
|
||||
};
|
||||
|
||||
@@ -704,8 +692,8 @@ export const endpointArgument = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Classify a recognized storage API call name. */
|
||||
export const storageKind = (
|
||||
/** Spelling-only candidate exclusion for overloaded open calls; not API identity. */
|
||||
const storageKind = (
|
||||
name: string,
|
||||
): JavaScriptStaticStorage["kind"] | undefined => {
|
||||
if (name.includes("localStorage.")) return "local-storage";
|
||||
|
||||
@@ -80,8 +80,6 @@ export interface JavaScriptAnalysisAccumulator {
|
||||
readonly moduleRangeIndex: JavaScriptModuleRangeIndex;
|
||||
/** Binding identities proven as third-parameter loaders of recovered factories. */
|
||||
readonly factoryRequireBindingIds: Set<string>;
|
||||
/** `preload` attributes of audio and video elements, which are not scripts. */
|
||||
readonly mediaElementPreloads: WeakSet<t.Node>;
|
||||
readonly seen: Set<string>;
|
||||
visitedNodes: number;
|
||||
unknownFindings: number;
|
||||
@@ -89,8 +87,10 @@ export interface JavaScriptAnalysisAccumulator {
|
||||
|
||||
/** Shared source and accumulator for helper inspections. */
|
||||
export interface JavaScriptFindingContext {
|
||||
/** Node path.resolve identity established by shared callee and mutation owners. */
|
||||
readonly nodePathResolve?: JavaScriptStaticPathIntrinsicQuery;
|
||||
/** Native path/URL identities established by shared lexical and mutation owners. */
|
||||
readonly pathIntrinsics?: JavaScriptStaticPathIntrinsicQuery;
|
||||
readonly electronExportIntact?: (path: readonly string[]) => boolean;
|
||||
readonly rendererLoad?: (callee: t.Node, site: t.Node) => boolean;
|
||||
readonly rpcScan: JavaScriptRpcScan;
|
||||
/** Shared bounded argv/nullish argument shapes for optional-slot signatures. */
|
||||
readonly expressionShapes: JavaScriptExpressionShapeResolver;
|
||||
@@ -159,7 +159,6 @@ export const createJavaScriptAnalysisAccumulator = (
|
||||
nativeAddonBindings: [],
|
||||
moduleRangeIndex: new JavaScriptModuleRangeIndex(sourceLength),
|
||||
factoryRequireBindingIds: new Set(),
|
||||
mediaElementPreloads: new WeakSet(),
|
||||
seen: new Set(),
|
||||
visitedNodes: 0,
|
||||
unknownFindings: 0,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
||||
|
||||
const windowPrefix =
|
||||
"import {BrowserWindow} from 'electron'; import {fileURLToPath} from 'node:url';";
|
||||
|
||||
it.each([
|
||||
'class URL {constructor(input, base) {return new globalThis.URL("./actual.js", base);}}',
|
||||
"globalThis.URL = class {};",
|
||||
'import.meta.url = "file:///another/main.js";',
|
||||
])(
|
||||
"retains a dynamic preload when source URL identity is unavailable: %s",
|
||||
(prefix) => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`${windowPrefix}${prefix}
|
||||
new BrowserWindow({webPreferences:{preload:fileURLToPath(new URL("./decoy.js", import.meta.url))}});`);
|
||||
expect(analysis.electron.browser_windows).toMatchObject([
|
||||
{ preload_path: null },
|
||||
]);
|
||||
expect(analysis.role_paths).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
'["a", "b"].join("/")',
|
||||
'settings.join("/app", "preload.js")',
|
||||
'settings.fileURLToPath("file:///decoy.js")',
|
||||
])("does not assign native path semantics to %s", (expression) => {
|
||||
const analysis =
|
||||
analyzeJavaScriptStaticSource(`import {BrowserWindow} from 'electron';
|
||||
const settings = {join() {}, fileURLToPath() {}};
|
||||
new BrowserWindow({webPreferences:{preload:${expression}}});`);
|
||||
expect(analysis.electron.browser_windows).toMatchObject([
|
||||
{ preload_path: null },
|
||||
]);
|
||||
expect(analysis.role_paths).toEqual([]);
|
||||
});
|
||||
|
||||
it("recovers native aliases and emits preload roles only for their Electron owner", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`${windowPrefix}
|
||||
import {join as append} from 'node:path';
|
||||
const unrelated = {preload: "./unrelated.js"};
|
||||
new BrowserWindow({webPreferences:{preload:append(__dirname, "preload.js")}});
|
||||
new BrowserWindow({webPreferences:{preload:fileURLToPath(new URL("./native.js", import.meta.url))}});`);
|
||||
expect(
|
||||
analysis.electron.browser_windows.map(({ preload_path }) => preload_path),
|
||||
).toEqual(["preload.js", "./native.js"]);
|
||||
expect(analysis.role_paths.map(({ path }) => path)).toEqual([
|
||||
"preload.js",
|
||||
"./native.js",
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(["localStorage", "sessionStorage"])(
|
||||
"requires browser identity for %s",
|
||||
(name) => {
|
||||
const local = analyzeJavaScriptStaticSource(
|
||||
`const ${name} = {setItem(key, value) {return value;}}; ${name}.setItem("ghost", "value");`,
|
||||
);
|
||||
expect(local.storage).toEqual([]);
|
||||
const global = analyzeJavaScriptStaticSource(
|
||||
`globalThis.${name}.setItem("real", "value");`,
|
||||
);
|
||||
expect(global.storage).toMatchObject([{ name: "real" }]);
|
||||
const overwritten = analyzeJavaScriptStaticSource(
|
||||
`globalThis.${name}.setItem = () => {}; ${name}.setItem("ghost", "value");`,
|
||||
);
|
||||
expect(overwritten.storage).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it("requires an intact Electron instance for renderer roles", () => {
|
||||
const local = analyzeJavaScriptStaticSource(
|
||||
`const cache = {loadFile(value) {return value;}};cache.loadFile("data.json");`,
|
||||
);
|
||||
expect(local.role_paths).toEqual([]);
|
||||
const native = analyzeJavaScriptStaticSource(
|
||||
`import {BrowserWindow as Win} from 'electron';const window = new Win(); window.loadFile("index.html");`,
|
||||
);
|
||||
expect(native.role_paths).toMatchObject([
|
||||
{ role: "renderer", path: "index.html" },
|
||||
]);
|
||||
const replaced = analyzeJavaScriptStaticSource(
|
||||
`import {BrowserWindow as Win} from 'electron';const window = new Win(); window.loadFile = () => {};window.loadFile("data.json");`,
|
||||
);
|
||||
expect(replaced.role_paths).toEqual([]);
|
||||
});
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
import { unwrapJavaScriptExpression } from "./javascriptAstValues.js";
|
||||
import type { JavaScriptStaticPathIntrinsicQuery } from "./javascriptStaticAnalysisHelpers.js";
|
||||
|
||||
/** Prove Node's path resolver before applying its argument-selection semantics. */
|
||||
export const createStaticNodePathResolveQuery = (
|
||||
/** Prove path and URL intrinsic identity before applying native semantics. */
|
||||
export const createStaticPathIntrinsicQuery = (
|
||||
index: JavaScriptBindingIndex,
|
||||
): JavaScriptStaticPathIntrinsicQuery => {
|
||||
const facts = new WeakMap<t.Node, boolean>();
|
||||
const facts = new WeakMap<t.Node, Map<string, boolean>>();
|
||||
const nativeMethod = (
|
||||
callee: t.Node,
|
||||
method: string,
|
||||
@@ -64,10 +64,24 @@ export const createStaticNodePathResolveQuery = (
|
||||
nativeMethod(callee.callee, "dirname", ["path", "path/posix"]) &&
|
||||
sourceFile(callee.arguments[0]);
|
||||
}
|
||||
const cached = facts.get(callee);
|
||||
const kind = fact ?? "resolve";
|
||||
const cached = facts.get(callee)?.get(kind);
|
||||
if (cached !== undefined) return cached;
|
||||
const proven = nativeMethod(callee, "resolve", ["path", "path/posix"]);
|
||||
facts.set(callee, proven);
|
||||
const proven =
|
||||
kind === "source-url"
|
||||
? t.isNewExpression(callee) &&
|
||||
callee.arguments.length === 2 &&
|
||||
isUnmodifiedGlobalReference(callee.callee, index.bindings, "URL") &&
|
||||
isUnmodifiedImportMetaUrl(callee.arguments[1], index.bindings)
|
||||
: kind === "file-url-to-path"
|
||||
? nativeMethod(callee, "fileURLToPath", ["url"])
|
||||
: nativeMethod(callee, kind, ["path", "path/posix"]);
|
||||
let entry = facts.get(callee);
|
||||
if (entry === undefined) {
|
||||
entry = new Map();
|
||||
facts.set(callee, entry);
|
||||
}
|
||||
entry.set(kind, proven);
|
||||
return proven;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -417,9 +417,14 @@ const assertPathProjection = (
|
||||
) => {
|
||||
const node = parsePathExpression(expression);
|
||||
// This syntax matrix supplies identity explicitly; full-source cases prove it.
|
||||
const nodePathResolve = (
|
||||
const pathIntrinsics = (
|
||||
callee: import("@babel/types").Node,
|
||||
fact?: "source-directory",
|
||||
fact?:
|
||||
| "resolve"
|
||||
| "join"
|
||||
| "file-url-to-path"
|
||||
| "source-url"
|
||||
| "source-directory",
|
||||
) => {
|
||||
if (fact === "source-directory") {
|
||||
const name = calleeName(
|
||||
@@ -429,36 +434,27 @@ const assertPathProjection = (
|
||||
name === "__dirname" || name === "dirname" || name.endsWith(".dirname")
|
||||
);
|
||||
}
|
||||
if (fact === "source-url") return true;
|
||||
const name = calleeName(callee);
|
||||
return name.endsWith(".resolve") && !name.includes("win32");
|
||||
if (fact === "file-url-to-path") return name.endsWith("fileURLToPath");
|
||||
return name.endsWith(`.${fact ?? "resolve"}`) && !name.includes("win32");
|
||||
};
|
||||
expect(staticPath(node, nodePathResolve)).toBe(path);
|
||||
expect(staticPathResolutionContext(node, nodePathResolve)).toBe(context);
|
||||
const {
|
||||
analysis,
|
||||
property,
|
||||
genericPrefix,
|
||||
windowPrefix,
|
||||
windowExpression,
|
||||
utilityExpression,
|
||||
} = analyzePathConsumers(expression);
|
||||
expect(staticPath(node, pathIntrinsics)).toBe(path);
|
||||
expect(staticPathResolutionContext(node, pathIntrinsics)).toBe(context);
|
||||
const { analysis, windowExpression, utilityExpression } =
|
||||
analyzePathConsumers(expression);
|
||||
expect(analysis.parse_status).toBe("complete");
|
||||
expect(analysis.parse_error_count).toBe(0);
|
||||
expect(analysis.role_paths).toEqual(
|
||||
(
|
||||
[
|
||||
[1, genericPrefix.length],
|
||||
[2, windowPrefix.length],
|
||||
] as const
|
||||
).map(([line, column]) => ({
|
||||
expect(analysis.role_paths).toEqual([
|
||||
{
|
||||
role: "preload",
|
||||
path,
|
||||
resolution_context: context,
|
||||
mechanism: "property:preload",
|
||||
mechanism: "BrowserWindow:webPreferences.preload",
|
||||
module_key: null,
|
||||
location: lineRange(line, column, column + property.length),
|
||||
})),
|
||||
);
|
||||
location: lineRange(2, 0, windowExpression.length),
|
||||
},
|
||||
]);
|
||||
expect(analysis.electron.browser_windows).toEqual([
|
||||
{
|
||||
options_status: "object-literal",
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
|
||||
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"preserves static API identity through %s",
|
||||
async (surface, { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-static-api-identity-");
|
||||
await writeFixtureFiles(root, {
|
||||
"package.json": JSON.stringify({ main: "main.mjs", type: "module" }),
|
||||
"main.mjs": `
|
||||
import {BrowserWindow as Win} from 'electron';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
class URL {constructor(input, base) {return new globalThis.URL('./actual.js', base);}}
|
||||
const cache = {loadFile(value) {return value;}};
|
||||
cache.loadFile('data.json');
|
||||
const localStorage = {setItem(key, value) {return value;}};
|
||||
localStorage.setItem('ghost', 'value');
|
||||
globalThis.localStorage.setItem('real', 'value');
|
||||
const unrelated = {preload: './decoy.js'};
|
||||
new Win({webPreferences:{preload:fileURLToPath(new URL('./decoy.js', import.meta.url))}});
|
||||
new Win({webPreferences:{preload:'./preload.js'}}).loadFile('index.html');
|
||||
`,
|
||||
"actual.js": "export {};",
|
||||
"decoy.js": "export {};",
|
||||
"preload.js": "export {};",
|
||||
"data.json": "{}",
|
||||
"index.html": "<!doctype html><title>Fixture</title>",
|
||||
});
|
||||
const result = await analyzeJavaScriptDirectory(surface, cli, root);
|
||||
expect(result.statistics.parse_failures).toBe(0);
|
||||
const roles = result.graph.nodes.filter(
|
||||
({ kind }) => kind === "electron-preload" || kind === "electron-renderer",
|
||||
);
|
||||
expect(
|
||||
roles.flatMap(({ observations }) =>
|
||||
observations.map(({ properties }) => properties.resolved_path),
|
||||
),
|
||||
).toEqual(expect.arrayContaining(["preload.js", "index.html"]));
|
||||
expect(
|
||||
roles.flatMap(({ observations }) =>
|
||||
observations.map(({ properties }) => properties.resolved_path),
|
||||
),
|
||||
).not.toEqual(expect.arrayContaining(["decoy.js"]));
|
||||
expect(
|
||||
roles.flatMap(({ observations }) =>
|
||||
observations.map(({ properties }) => properties.resolved_path),
|
||||
),
|
||||
).not.toEqual(expect.arrayContaining(["data.json"]));
|
||||
const storage = result.graph.nodes.filter(({ kind }) => kind === "storage");
|
||||
expect(
|
||||
storage.flatMap(({ observations }) =>
|
||||
observations.map(({ properties }) => properties.name),
|
||||
),
|
||||
).toEqual(["real"]);
|
||||
},
|
||||
);
|
||||
@@ -55,8 +55,7 @@ cliTest.for(
|
||||
const { graph } = result;
|
||||
const windows = graph.nodes.filter(({ kind }) => kind === "browser-window");
|
||||
expect(windows).toHaveLength(expectedRoles);
|
||||
// Generic `property:preload` discovery is separate from a proven
|
||||
// BrowserWindow identity; only the latter may create this association.
|
||||
// Preload roles share the BrowserWindow identity owner.
|
||||
expect(
|
||||
graph.nodes.filter(
|
||||
({ kind, observations }) =>
|
||||
@@ -67,6 +66,9 @@ cliTest.for(
|
||||
),
|
||||
),
|
||||
).toHaveLength(expectedRoles);
|
||||
expect(
|
||||
graph.nodes.filter(({ kind }) => kind === "electron-preload"),
|
||||
).toHaveLength(expectedRoles);
|
||||
const main = findSourceModule(graph, "main.cjs");
|
||||
expect(main).toBeDefined();
|
||||
const imports = graph.edges.filter(
|
||||
|
||||
@@ -45,8 +45,10 @@ describe("Electron option keys in reconstructed application graphs", () => {
|
||||
expect(result.electron_summary.preload_entrypoints).toBe(0);
|
||||
expect(result.electron_summary.explicit_web_preferences).toBe(0);
|
||||
|
||||
// The independent property:preload heuristic can still discover the nested
|
||||
// literal. It must not carry a BrowserWindow-specific observation or edge.
|
||||
// A nested literal cannot bypass the unresolved options owner.
|
||||
expect(
|
||||
result.graph.nodes.filter(({ kind }) => kind === "electron-preload"),
|
||||
).toEqual([]);
|
||||
expect(windowPreloadNodes(result)).toEqual([]);
|
||||
expect(
|
||||
result.graph.edges.filter(
|
||||
|
||||
Reference in New Issue
Block a user