fix(javascript): resolve electron findings through aliases of interop results (#8914)

* fix(javascript): resolve electron findings through aliases of interop results

* fix(javascript): resolve interop aliases iteratively and check target mutations

* fix(javascript): retain possible writes through uncertain aliases and guard positive reads

* perf(javascript): cache alias change status across semantic interop lookups
This commit is contained in:
highmachs authored and GitHub committed 2026-11-05 18:31:52 +08:00
1 parent 281dbc8514
commit 7b6fc6f13d
3 files changed
+223 -36

No files matched your search

@@ -323,6 +323,76 @@ describe("Electron module interop helper results", () => {
`);
expect(analysis.electron.browser_windows).toHaveLength(3);
});
it("preserves BrowserWindow findings through lexical aliases of proven interop results", () => {
const analysis = analyzeJavaScriptStaticSource(`
function identity(mod) { return mod; }
const electron = identity(require("electron"));
const api = electron;
new api.BrowserWindow({webPreferences:{preload:"./p.js"}});
`);
expect(analysis.electron.browser_windows).toMatchObject([
{ preload_path: "./p.js" },
]);
});
it("preserves BrowserWindow and IPC findings through aliases of Rolldown namespace conversions", () => {
const analysis = analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}
const electron = __toESM(require("electron"), 1);
const api = electron;
new api.BrowserWindow({webPreferences:{preload:"./p.js"}});
api.ipcMain.handle("ping", () => "pong");
`);
expect(analysis.electron.browser_windows).toMatchObject([
{ preload_path: "./p.js" },
]);
expect(analysis.electron.ipc).toMatchObject([
{ channel: "ping", side: "main" },
]);
});
});
describe("Electron module interop alias resolution", () => {
it("does not infer Electron identity through an alias when the target binding was modified", () => {
const analysis = analyzeJavaScriptStaticSource(`
${ROLLDOWN_MODULE_INTEROP}
function identity(mod) { return mod; }
let electron = identity(require("electron"));
eval("electron = { BrowserWindow: class {} }");
const api = electron;
new api.BrowserWindow({webPreferences:{preload:"./p.js"}});
`);
expect(analysis.electron.browser_windows).toEqual([]);
});
it("retains possible member writes through uncertain aliases of interop results", () => {
const analysis = analyzeJavaScriptStaticSource(`
${ROLLDOWN_MODULE_INTEROP}
function identity(mod) { return mod; }
const original = identity(require("electron"));
let electron = original;
eval("noop()");
const api = electron;
api.BrowserWindow = Fake;
new original.BrowserWindow({webPreferences:{preload:"./p.js"}});
`);
expect(analysis.electron.browser_windows).toEqual([]);
});
it("resolves Electron identity across long alias chains without stack overflow", () => {
const lines = [
ROLLDOWN_MODULE_INTEROP,
'const a0 = __toESM(require("electron"), 1);',
];
for (let i = 1; i <= 2000; i++) {
lines.push(`const a${i} = a${i - 1};`);
}
lines.push('new a2000.BrowserWindow({webPreferences:{preload:"./p.js"}});');
const analysis = analyzeJavaScriptStaticSource(lines.join("\n"));
expect(analysis.electron.browser_windows).toMatchObject([
{ preload_path: "./p.js" },
]);
});
});
describe("Electron module interop pattern writes", () => {
+151 -35
View File
@@ -30,6 +30,7 @@ import {
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import { semanticKnownArrayIterationAt } from "./javascriptSemanticReferenceSources.js";
import { semanticOpaqueBindingReadMayHaveChanged } from "./javascriptSemanticOpaqueEffects.js";
// Match the complete Rolldown runtime operations, including its live getters.
// Names and formatting are irrelevant; lexical roles, operators and keys are not.
@@ -87,6 +88,11 @@ export const createSemanticModuleInterop = (
semanticKnownArrayIterationAt(array, state);
const helpers = new Map<string, InteropHelper | null>();
const origins = new Map<string, InteropOrigin>();
const aliasTargets = new Map<
string,
{ targetBinding: JavaScriptSemanticBindingState; node: t.Node }
>();
const aliasChangedCache = new Map<string, boolean>();
const calls = new WeakMap<t.Node, InteropOrigin>();
const intrinsicBindings = new Map<string, readonly string[] | null>();
const writes: {
@@ -517,40 +523,9 @@ export const createSemanticModuleInterop = (
return result;
};
const origin = (
const twoInitializerOrigin = (
binding: JavaScriptSemanticBindingState,
): InteropOrigin | undefined => {
const single = semanticStableInitializer(binding, state);
if (single !== undefined) {
if (
!single.projection.every(
(key): key is string => typeof key === "string",
)
)
return undefined;
const converted = callOrigin(single.node);
if (
converted?.origin.importedPath.length === 0 &&
single.projection[0] === "default" &&
!converted.defaultIsModule
)
return undefined;
const result =
converted === undefined
? undefined
: {
...converted,
origin: {
...converted.origin,
importedPath: [
...converted.origin.importedPath,
...single.projection,
],
},
};
if (result !== undefined) origins.set(binding.bindingId, result);
return result;
}
const [initial, converted] = binding.initializers;
if (
binding.definitions.length !== 2 ||
@@ -600,13 +575,126 @@ export const createSemanticModuleInterop = (
if (required === undefined || required.importedPath.length !== 0)
return undefined;
if (convertedCall === undefined) return undefined;
const result = {
return {
origin: required,
dependencies: convertedCall.dependencies,
defaultIsModule: convertedCall.defaultIsModule,
};
origins.set(binding.bindingId, result);
return result;
};
const origin = (
binding: JavaScriptSemanticBindingState,
): InteropOrigin | undefined => {
const cached = origins.get(binding.bindingId);
if (cached !== undefined) return cached;
let currentBinding: JavaScriptSemanticBindingState | undefined = binding;
const visited = new Set<string>();
const steps: {
binding: JavaScriptSemanticBindingState;
segments: readonly string[];
}[] = [];
let baseOrigin: InteropOrigin | undefined;
while (currentBinding !== undefined) {
const alreadyCached = origins.get(currentBinding.bindingId);
if (alreadyCached !== undefined) {
baseOrigin = alreadyCached;
break;
}
if (visited.has(currentBinding.bindingId)) return undefined;
visited.add(currentBinding.bindingId);
const single = semanticStableInitializer(currentBinding, state);
if (single !== undefined) {
if (
!single.projection.every(
(key): key is string => typeof key === "string",
)
)
return undefined;
const converted = callOrigin(single.node);
if (converted !== undefined) {
if (
converted.origin.importedPath.length === 0 &&
single.projection[0] === "default" &&
!converted.defaultIsModule
)
return undefined;
baseOrigin = {
...converted,
origin: {
...converted.origin,
importedPath: [
...converted.origin.importedPath,
...(single.projection as readonly string[]),
],
},
};
break;
}
const { root, members } = electronMemberPath(single.node);
if (
!t.isIdentifier(root) ||
!members.every((member): member is string => member !== null)
)
return undefined;
const targetBinding = resolveSemanticBindingState(
state,
root,
root.name,
);
if (targetBinding === undefined || targetBinding === currentBinding)
return undefined;
aliasTargets.set(currentBinding.bindingId, {
targetBinding,
node: root,
});
steps.push({
binding: currentBinding,
segments: [
...(members as readonly string[]),
...(single.projection as readonly string[]),
],
});
currentBinding = targetBinding;
continue;
}
const twoInit = twoInitializerOrigin(currentBinding);
if (twoInit !== undefined) {
baseOrigin = twoInit;
break;
}
return undefined;
}
if (baseOrigin === undefined) return undefined;
origins.set(currentBinding!.bindingId, baseOrigin);
let currentOrigin = baseOrigin;
for (let i = steps.length - 1; i >= 0; i--) {
const step = steps[i]!;
const firstSegment = step.segments[0];
if (
currentOrigin.origin.importedPath.length === 0 &&
firstSegment === "default" &&
!currentOrigin.defaultIsModule
)
return undefined;
currentOrigin = {
...currentOrigin,
origin: {
...currentOrigin.origin,
importedPath: [
...currentOrigin.origin.importedPath,
...step.segments,
],
},
};
origins.set(step.binding.bindingId, currentOrigin);
}
return currentOrigin;
};
const invalidatedOrigin = (record: InteropOrigin | undefined): boolean =>
record?.dependencies.some(({ path, capturedAt }) => {
@@ -667,6 +755,34 @@ export const createSemanticModuleInterop = (
invalidatedCalls.set(node, result);
return result;
},
aliasMayHaveChanged: (binding: JavaScriptSemanticBindingState): boolean => {
if (state.opaqueEffects.byScope.size === 0) return false;
const cached = aliasChangedCache.get(binding.bindingId);
if (cached !== undefined) return cached;
origin(binding);
let current: JavaScriptSemanticBindingState | undefined = binding;
const seen = new Set<string>();
let changed = false;
while (current !== undefined) {
if (seen.has(current.bindingId)) break;
seen.add(current.bindingId);
const next = aliasTargets.get(current.bindingId);
if (next === undefined) break;
if (
semanticOpaqueBindingReadMayHaveChanged(
next.targetBinding,
next.node,
state,
)
) {
changed = true;
break;
}
current = next.targetBinding;
}
aliasChangedCache.set(binding.bindingId, changed);
return changed;
},
};
};
@@ -895,7 +895,8 @@ export function* indexJavaScriptElectronIdentitySteps(
const binding = resolveSemanticBindingState(state, root, root.name);
if (
binding !== undefined &&
semanticOpaqueBindingReadMayHaveChanged(binding, root, state)
(semanticOpaqueBindingReadMayHaveChanged(binding, root, state) ||
interop.aliasMayHaveChanged(binding))
)
return undefined;
}