Files
rea/src/domain/javascript/javascriptElectronMemberWrites.ts
T
fe574c0c9f fix(javascript): recover Electron APIs through default imports and optional calls (#1673)
`import electron from "electron"` bound the module with a leading `default`
export segment, so `electron.ipcMain.handle(...)`, `new
electron.BrowserWindow(...)` and destructuring from that binding matched no
Electron API. Electron's CommonJS exports define no `default` member, so the
segment can only denote the module object; drop it when classifying
Electron bindings.

The binding classifier already recorded optional-call roots, but the IPC,
context-bridge, utility-process and sender-validation visitors accepted only
plain calls. `ipcMain?.handle(...)`, `contextBridge?.exposeInMainWorld(...)`
and the common `event.senderFrame?.url.startsWith(...)` check were dropped
without a limitation.

Refs #1656

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:31:08 +08:00

72 lines
2.8 KiB
TypeScript

import * as t from "@babel/types";
import {
semanticStaticPropertyKey,
semanticMemberWriteTargets,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import { SemanticPropertyPathCoverage } from "./javascriptSemanticPropertyPaths.js";
import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js";
/** Match Electron's supported module entry points. */
export const ELECTRON_MODULE =
/^electron(?:\/(?:common|main|renderer|utility))?$/u;
/**
* Electron's CommonJS exports define no `default` member, so a leading
* `default` segment can only denote the module object itself, as an ES module
* default import (`import electron from "electron"`) binds it.
*/
export const electronExportPath = (
importedPath: readonly string[],
): readonly string[] =>
importedPath[0] === "default" ? importedPath.slice(1) : importedPath;
/** Static identity recovery cannot establish the absence of Electron usage. */
export const ELECTRON_IDENTITY_LIMITATION =
"Electron API identity follows proven lexical module aliases. Reassigned bindings, dynamic selections, and visibly overwritten namespace members are unresolved; missing findings do not establish absence. Arbitrary call side effects and runtime registration are not evaluated.";
/** Preserve exact property segments instead of flattening literal dotted keys. */
export const electronMemberPath = (
node: t.Node,
): {
readonly root: t.Node;
readonly members: readonly (string | null)[];
} => {
let root = unwrapJavaScriptExpression(node).node;
const members: (string | null)[] = [];
while (t.isMemberExpression(root) || t.isOptionalMemberExpression(root)) {
members.push(semanticStaticPropertyKey(root.property, root.computed));
root = unwrapJavaScriptExpression(root.object).node;
}
return { root, members: members.toReversed() };
};
/** Index visible writes through proven Electron aliases, without executing them. */
export function* collectElectronMemberWritesSteps(
program: t.Program,
origin: (root: t.Node) => readonly string[] | undefined,
): Generator<void, SemanticPropertyPathCoverage> {
const writes = new SemanticPropertyPathCoverage();
yield* traverseJavaScriptAstSteps(program, {
enter: (node) => {
const target = t.isAssignmentExpression(node)
? node.left
: t.isUpdateExpression(node)
? node.argument
: t.isUnaryExpression(node, { operator: "delete" })
? node.argument
: t.isForOfStatement(node) || t.isForInStatement(node)
? node.left
: undefined;
if (target === undefined) return;
for (const member of semanticMemberWriteTargets(target)) {
const { root, members } = electronMemberPath(member);
const exported = origin(root);
if (exported !== undefined) writes.retain([...exported, ...members]);
}
},
});
return writes;
}