fix(javascript): preserve proven interop result projections

This commit is contained in:
morluto
2026-10-12 00:10:02 +08:00
parent 6b460683d7
commit 3d1d5b56b2
4 changed files with 141 additions and 14 deletions
+6 -2
View File
@@ -327,8 +327,12 @@ Electron API identity follows proven lexical module aliases. Namespace interop
also preserves identity when the complete Rolldown helper operations, lexical
roles, live getters, and captured intrinsic origins match, or when a stable
helper simply returns its module parameter. Harmless single-statement control
blocks preserve recognition; added operations still require proof. Recognition
uses structure rather than emitted names; other helper bodies and ambiguous
blocks preserve recognition; added operations still require proof. Literal
member and destructuring projections are retained from proven results.
Omitted or literal node-mode arguments preserve named exports; a module-valued
default projection requires a truthy literal node mode. Computed or extra
arguments remain unresolved.
Recognition uses structure rather than emitted names; other helper bodies and ambiguous
runtime fallbacks remain unresolved. Captured function identities survive later global-property
replacement, including copies through initialized aliases. Reads before an
alias initializer remain unresolved. Visible intrinsic writes before capture or
@@ -200,6 +200,78 @@ describe("Electron module interop uncertainty", () => {
});
describe("Electron module interop helper results", () => {
it.each([
'const {BrowserWindow} = __toESM(require("electron"), 1); new BrowserWindow({});',
'const {BrowserWindow: Window} = __toESM(require("electron"), 1); new Window({});',
'const {default: {BrowserWindow}} = __toESM(require("electron"), 1); new BrowserWindow({});',
'const Window = __toESM(require("electron"), 1).BrowserWindow; new Window({});',
])("preserves a proven result projection: %s", (source) => {
expect(
analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}\n${source}`)
.electron.browser_windows,
).toHaveLength(1);
});
it.each([
'const {[key]: Window} = __toESM(require("electron"), 1); new Window({});',
'const {...electron} = __toESM(require("electron"), 1); new electron.BrowserWindow({});',
'let {BrowserWindow} = __toESM(require("electron"), 1); BrowserWindow = Fake; new BrowserWindow({});',
])("keeps an uncertain result projection unresolved: %s", (source) => {
expect(
analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}\n${source}`)
.electron.browser_windows,
).toEqual([]);
});
it.each([
"",
", 0",
", false",
", true",
", 1",
', "node"',
', ""',
", null",
])("preserves named exports with a literal node mode: %s", (mode) => {
const analysis = analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}
const fresh = __toESM(require("electron")${mode});
new fresh.BrowserWindow({});
const {BrowserWindow} = __toESM(require("electron")${mode});
new BrowserWindow({});
let electron = require("electron");
electron = __toESM(electron${mode}); ${WINDOW}
`);
expect(analysis.electron.browser_windows).toHaveLength(3);
});
it.each(["", ", 0", ", false"])(
"does not assume a module-valued default with node mode: %s",
(mode) => {
const analysis = analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}
const original = require("electron");
original.__esModule = true;
original.default = {BrowserWindow: Fake};
const {default: {BrowserWindow}} = __toESM(require("electron")${mode});
new BrowserWindow({});
const other = __toESM(require("electron")${mode}).default;
new other.BrowserWindow({});
new (__toESM(require("electron")${mode}).default.BrowserWindow)({});
`);
expect(analysis.electron.browser_windows).toEqual([]);
},
);
it.each(["__toESM", "identity"])(
"rejects side-effecting extra arguments to %s",
(helper) => {
const analysis = analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}
function identity(mod) { return mod; }
const electron = ${helper}(require("electron"), mutate()); ${WINDOW}
`);
expect(analysis.electron.browser_windows).toEqual([]);
},
);
it.each([
"const __toESM = () => ({BrowserWindow:FakeWindow});",
"const __toESM = async (mod) => mod;",
@@ -17,7 +17,10 @@ import {
} from "./javascriptSemanticTraversal.js";
import { electronMemberPath } from "./javascriptElectronMemberWrites.js";
import { SemanticPropertyPathCoverage } from "./javascriptSemanticPropertyPaths.js";
import { unwrapJavaScriptExpression } from "./javascriptAstValues.js";
import {
readExactJavaScriptLiteral,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
// Match the complete Rolldown runtime operations, including its live getters.
// Names and formatting are irrelevant; lexical roles, operators and keys are not.
@@ -55,6 +58,7 @@ interface IntrinsicDependency {
interface InteropOrigin {
readonly origin: JavaScriptModuleOrigin;
readonly dependencies: readonly IntrinsicDependency[];
readonly defaultIsModule: boolean;
}
interface InteropHelper {
@@ -275,17 +279,26 @@ export const createSemanticModuleInterop = (
);
const proof = helper === undefined ? undefined : helperProof(helper);
const input = node.arguments[0];
const mode = node.arguments[1];
const literalMode =
mode === undefined
? { found: true, value: false }
: t.isExpression(mode)
? readExactJavaScriptLiteral(mode)
: { found: false };
if (
proof === undefined ||
proof.readyAfter >= (node.start ?? -1) ||
!t.isExpression(input) ||
(!proof.identity &&
(node.arguments.length !== 2 ||
!t.isNumericLiteral(node.arguments[1], { value: 1 })))
node.arguments.length > 2 ||
!literalMode.found
)
return undefined;
return {
input,
// A truthy node mode forces the module-valued default. Other modes
// preserve named exports without proving the source's default property.
defaultIsModule: !proof.identity && Boolean(literalMode.value),
dependencies: proof.dependencies.map((dependency) => ({
...dependency,
capturedAt: dependency.capturedAt ?? node,
@@ -296,7 +309,10 @@ export const createSemanticModuleInterop = (
const callOrigin = (node: t.Node): InteropOrigin | undefined => {
const cached = calls.get(node);
if (cached !== undefined) return cached;
const converted = provenCall(node);
const { root, members } = electronMemberPath(node);
if (!members.every((member): member is string => member !== null))
return undefined;
const converted = provenCall(root);
const required =
converted === undefined
? undefined
@@ -304,10 +320,15 @@ export const createSemanticModuleInterop = (
if (
converted === undefined ||
required === undefined ||
required.importedPath.length !== 0
required.importedPath.length !== 0 ||
(members[0] === "default" && !converted.defaultIsModule)
)
return undefined;
const result = { origin: required, dependencies: converted.dependencies };
const result = {
origin: { ...required, importedPath: members },
dependencies: converted.dependencies,
defaultIsModule: converted.defaultIsModule,
};
calls.set(node, result);
return result;
};
@@ -315,9 +336,34 @@ export const createSemanticModuleInterop = (
const origin = (
binding: JavaScriptSemanticBindingState,
): InteropOrigin | undefined => {
const single = unprojectedInitializer(binding, state);
const single = semanticStableInitializer(binding, state);
if (single !== undefined) {
const result = callOrigin(single);
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;
}
@@ -373,6 +419,7 @@ export const createSemanticModuleInterop = (
const result = {
origin: required,
dependencies: convertedCall.dependencies,
defaultIsModule: convertedCall.defaultIsModule,
};
origins.set(binding.bindingId, result);
return result;
@@ -63,6 +63,10 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
new direct.BrowserWindow({});
const fresh = __toESM(require("electron"), 1);
new fresh.BrowserWindow({});
const {BrowserWindow: ProjectedWindow} = __toESM(require("electron"));
new ProjectedWindow({});
const nonNode = __toESM(require("electron"), 0);
new nonNode.BrowserWindow({});
function identity(mod) {return mod;}
const passthrough = identity(require("electron"));
new passthrough.BrowserWindow({});
@@ -95,7 +99,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(document).normalized_result;
expect(result.summary).toMatchObject({
browser_windows: 5,
browser_windows: 7,
preload_entrypoints: 1,
});
expect(result.statistics.parse_failures).toBe(0);
@@ -103,11 +107,11 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
const windows = result.graph.nodes.filter(
({ kind }) => kind === "browser-window",
);
expect(windows).toHaveLength(5);
expect(windows).toHaveLength(7);
const expectedLines = source
.split("\n")
.flatMap((line, index) =>
/^\s*new (?:electron|direct|fresh|passthrough)\.BrowserWindow/u.test(
/^\s*new (?:(?:electron|direct|fresh|passthrough|nonNode)\.BrowserWindow|ProjectedWindow)\(/u.test(
line,
)
? [index + 1]