fix(javascript): recognize Electron APIs through renamed bindings
Electron recognition matched callee spellings, so a bundler rename such as
esbuild's `import { ipcMain as ipcMain2 } from "electron"`, or a destructured
`const { contextBridge: bridge } = require("electron")`, hid every IPC
handler, BrowserWindow, contextBridge and utility process behind it.
Resolve each call and construction root lexically to its Electron import or
require origin, and classify the callee under that export's name. Shadowed
locals keep their own meaning.
On Hermes Desktop v0.21.6, whose main process is one esbuild ESM bundle,
recognized main IPC handlers rise from 1 to 204, BrowserWindow constructions
from 0 to 11, and paired renderer transmissions from 1 to 201.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPY2GmufMNGQKoEgRUx1pd
This commit is contained in:
7 files changed
+186
-20
No files matched your search
@@ -29,6 +29,7 @@ import {
|
||||
semanticVariableScope,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
||||
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
|
||||
import { collectSemanticMemberMutations } from "./javascriptSemanticMemberMutations.js";
|
||||
import {
|
||||
collectSemanticReturns,
|
||||
@@ -282,6 +283,84 @@ const resolveGlobalAliasFact = (
|
||||
return openGlobalFact(name);
|
||||
};
|
||||
|
||||
const ELECTRON_MODULE = /^electron(?:\/(?:common|main|renderer|utility))?$/u;
|
||||
|
||||
/**
|
||||
* Electron export paths for call and construction roots bound to Electron
|
||||
* under another name, such as esbuild's `import { ipcMain as ipcMain2 }`.
|
||||
* Keys are root identifier offsets; roots spelled as their export are omitted.
|
||||
*/
|
||||
export const classifyParsedJavaScriptElectronBindings = (
|
||||
file: ParsedJavaScriptSource,
|
||||
): ReadonlyMap<number, string> => {
|
||||
const facts = new Map<number, string>();
|
||||
const state = createState(file.program);
|
||||
collectDefinitions(file.program, state);
|
||||
const exportsByBinding = new Map<string, string | null>();
|
||||
traverseJavaScriptAst(file.program, {
|
||||
enter: (node) => {
|
||||
if (
|
||||
!t.isCallExpression(node) &&
|
||||
!t.isOptionalCallExpression(node) &&
|
||||
!t.isNewExpression(node)
|
||||
)
|
||||
return;
|
||||
const root = calleeRoot(node.callee);
|
||||
if (root === undefined) return;
|
||||
const binding = resolveSemanticBindingState(state, root, root.name);
|
||||
if (binding === undefined) return;
|
||||
let exported = exportsByBinding.get(binding.bindingId);
|
||||
if (exported === undefined) {
|
||||
exported = electronExport(binding, state);
|
||||
exportsByBinding.set(binding.bindingId, exported);
|
||||
}
|
||||
if (exported !== null && exported !== root.name)
|
||||
facts.set(root.start ?? -1, exported);
|
||||
},
|
||||
});
|
||||
return facts;
|
||||
};
|
||||
|
||||
const calleeRoot = (node: t.Node): t.Identifier | undefined => {
|
||||
while (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
if (!t.isNode(node.object)) return undefined;
|
||||
node = node.object;
|
||||
}
|
||||
return t.isIdentifier(node) ? node : undefined;
|
||||
};
|
||||
|
||||
// Read import and require origins directly; general value evaluation is
|
||||
// unnecessary here and costly across large vendor bundles.
|
||||
const electronExport = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): string | null => {
|
||||
const origin = bindingOrigin(binding, state);
|
||||
return origin !== undefined &&
|
||||
ELECTRON_MODULE.test(origin.specifier) &&
|
||||
origin.importedPath.length > 0
|
||||
? origin.importedPath.join(".")
|
||||
: null;
|
||||
};
|
||||
|
||||
const bindingOrigin = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptModuleOrigin | undefined => {
|
||||
if (binding.directOrigins.length > 0)
|
||||
return binding.directOrigins.length === 1
|
||||
? binding.directOrigins[0]
|
||||
: undefined;
|
||||
const [initializer] = binding.initializers;
|
||||
if (binding.mutable || binding.initializers.length !== 1) return undefined;
|
||||
const origin = semanticRequireOrigin(initializer?.node, state);
|
||||
const projection = initializer?.projection ?? [];
|
||||
return origin === undefined ||
|
||||
!projection.every((key): key is string => typeof key === "string")
|
||||
? undefined
|
||||
: { ...origin, importedPath: [...origin.importedPath, ...projection] };
|
||||
};
|
||||
|
||||
const createState = (program: t.Program): JavaScriptSemanticAnalysisState => {
|
||||
const root: JavaScriptSemanticScopeState = {
|
||||
scopeId: semanticScopeId("program", program),
|
||||
|
||||
Reference in new issue
Block a user