mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 23:06:18 +00:00
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:
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", () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user