Merge pull request #1391 from marnixpluim/fix/electron-import-aliases

fix(javascript): recognize Electron APIs through renamed bindings
This commit is contained in:
morluto authored and GitHub committed 2026-10-10 13:57:24 +08:00
commit 817efaca9a
7 files changed
+250 -20

No files matched your search

@@ -32,6 +32,7 @@ import {
semanticVariableScope,
} from "./javascriptSemanticState.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
import { collectSemanticMemberMutations } from "./javascriptSemanticMemberMutations.js";
import {
collectSemanticReturns,
@@ -285,6 +286,92 @@ 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;
// A var/let declaration permits writes without proving one occurred.
// Require one unconditional initializer and no actual binding writes.
if (
initializer === undefined ||
binding.initializers.length !== 1 ||
binding.definitions.some(({ kind }) => kind === "assignment") ||
state.conditionalInitializers.has(initializer.node)
)
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),