fix(javascript): preserve proven interop result projections
This commit is contained in:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user