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:
Claude
2026-10-10 00:44:32 +00:00
parent 7aa4d768eb
commit e4a8e0b728
7 changed files with 186 additions and 20 deletions
@@ -25,9 +25,11 @@ import { finalizeLocatedFindings } from "./javascriptStaticAnalysisFindings.js";
import {
createJavaScriptAnalysisAccumulator,
type JavaScriptAnalysisAccumulator as AnalysisAccumulator,
type JavaScriptFindingContext,
} from "./javascriptStaticAnalysisState.js";
import type { JavaScriptStaticAnalysis } from "./javascriptStaticAnalysisTypes.js";
import {
classifyParsedJavaScriptElectronBindings,
classifyParsedJavaScriptOpenReceivers,
type JavaScriptOpenReceiverFact,
} from "./javascriptSemanticAnalysis.js";
@@ -57,21 +59,24 @@ export const analyzeParsedJavaScriptStaticSource = (
openReceiverFacts?: ReadonlyMap<number, JavaScriptOpenReceiverFact>,
): JavaScriptStaticAnalysis => {
const accumulator = createJavaScriptAnalysisAccumulator(source.length);
traverseStaticSource(source, file, accumulator, openReceiverFacts);
traverseStaticSource(file, {
source,
accumulator,
...(openReceiverFacts === undefined ? {} : { openReceiverFacts }),
electronBindings: classifyParsedJavaScriptElectronBindings(file),
});
addSourceMapDirectives(source, file.comments ?? [], accumulator);
return finalizeStaticAnalysis(source, file, accumulator);
};
const traverseStaticSource = (
source: string,
file: ParsedJavaScriptSource,
accumulator: AnalysisAccumulator,
openReceiverFacts?: ReadonlyMap<number, JavaScriptOpenReceiverFact>,
findings: JavaScriptFindingContext,
): void => {
traverseJavaScriptAst(file, {
enter: (node) => {
accumulator.visitedNodes += 1;
inspectNode(source, node, accumulator, openReceiverFacts);
findings.accumulator.visitedNodes += 1;
inspectNode(node, findings);
return undefined;
},
});
@@ -156,16 +161,10 @@ const finalizeStaticAnalysis = (
};
const inspectNode = (
source: string,
node: t.Node,
accumulator: AnalysisAccumulator,
openReceiverFacts?: ReadonlyMap<number, JavaScriptOpenReceiverFact>,
findings: JavaScriptFindingContext,
): void => {
const findings = {
source,
accumulator,
...(openReceiverFacts === undefined ? {} : { openReceiverFacts }),
};
const { source, accumulator } = findings;
inspectElectronStaticNode(node, findings);
if (t.isCallExpression(node)) {
inspectBundlerRegistration(source, node, accumulator);