fix(javascript): resolve proven URL constructor aliases

Remove the redundant spelling gate after the shared intrinsic owner proves native constructor and source-base identity. Retain local-constructor and replaced-export uncertainty through public application analysis.
This commit is contained in:
morluto committed 2026-10-24 10:17:23 +08:00
1 parent 263a98c6ce
commit 37f24125ad
4 files changed
+49 -21

No files matched your search

@@ -427,6 +427,8 @@ member-mutation owners must establish the receiver before static API facts becom
roles or storage observations. Path `join`, `resolve`, and `fileURLToPath` use
proven Node imports; source-relative URL conversion additionally requires an
unmodified global or proven Node `url.URL` constructor and `import.meta.url`.
Native constructor aliases retain that identity; their local spelling does not
determine URL semantics, and replaced exports or local constructors remain unknown.
Relative results of native path operations retain filesystem-expression context;
their spelling does not turn them into module specifiers. Other calls preserve
their expressions. A property named `preload` outside a proven BrowserWindow
@@ -267,12 +267,6 @@ export interface JavaScriptStaticSourceUrlPath {
readonly directory: boolean;
}
/** Read a literal `new URL(relative, import.meta.url)` shape without evaluation. */
export const staticSourceRelativeUrlPath = (
node: t.Node,
): JavaScriptStaticSourceUrlPath | undefined =>
isSourceFileUrl(node) ? sourceRelativeFileUrlPath(node) : undefined;
/** Convert an exact static file URL expression to a POSIX path. */
export const staticFileUrlPath = (
node: t.CallExpression | t.NewExpression,
@@ -282,8 +276,8 @@ export const staticFileUrlPath = (
const rawArgument = argumentNode(node.arguments[0]);
if (rawArgument === undefined) return undefined;
const argument = unwrapJavaScriptExpression(rawArgument).node;
if (isSourceFileUrl(argument) && pathIntrinsics?.(argument, "source-url"))
return sourceRelativeFileUrlPath(argument)?.path;
if (t.isNewExpression(argument) && pathIntrinsics?.(argument, "source-url"))
return staticSourceRelativeUrlPath(argument)?.path;
const value = staticPath(argument, pathIntrinsics);
const rawPath = value?.split(/[?#]/u, 1)[0] ?? "";
if (
@@ -308,9 +302,9 @@ export const staticFileUrlPath = (
}
};
/** Project a known source-relative URL through the actual file URL decoder. */
const sourceRelativeFileUrlPath = (
node: t.CallExpression | t.NewExpression,
/** Decode a source-relative URL after the caller proves constructor and base identity. */
export const staticSourceRelativeUrlPath = (
node: t.NewExpression,
): JavaScriptStaticSourceUrlPath | undefined => {
const value = stringValue(node.arguments[0]);
if (
@@ -350,14 +344,6 @@ const sourceRelativeFileUrlPath = (
}
};
const isSourceFileUrl = (
node: t.Node,
): node is t.CallExpression | t.NewExpression =>
(t.isCallExpression(node) || t.isNewExpression(node)) &&
(calleeName(node.callee) === "URL" ||
calleeName(node.callee).endsWith(".URL")) &&
isFileIdentity(argumentNode(node.arguments[1]));
const staticResolvedPath = (
node: t.CallExpression | t.NewExpression,
pathIntrinsics?: JavaScriptStaticPathIntrinsicQuery,
@@ -36,6 +36,42 @@ it.each([
},
);
it.each([
[
"named import",
"import {URL as Constructor} from 'node:url';",
"./native.js",
],
[
"captured export",
"import url from 'node:url'; const Constructor = url.URL;",
"./native.js",
],
[
"replaced export",
"import url from 'node:url'; url.URL = class {}; const Constructor = url.URL;",
null,
],
[
"local constructor",
"class Constructor {constructor(input, base) {return new globalThis.URL('./actual.js', base);}}",
null,
],
] as const)(
"uses the proven URL constructor identity for a %s alias",
(_kind, declaration, expected) => {
const analysis =
analyzeJavaScriptStaticSource(`${windowPrefix}${declaration}
new BrowserWindow({webPreferences:{preload:fileURLToPath(new Constructor('./native.js', import.meta.url))}});`);
expect(analysis.electron.browser_windows).toMatchObject([
{ preload_path: expected },
]);
expect(analysis.role_paths.map(({ path }) => path)).toEqual(
expected === null ? [] : [expected],
);
},
);
it.each([
'["a", "b"].join("/")',
'settings.join("/app", "preload.js")',
@@ -13,7 +13,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
"package.json": JSON.stringify({ main: "main.mjs", type: "module" }),
"main.mjs": `
import {BrowserWindow as Win} from 'electron';
import {fileURLToPath} from 'node:url';
import {fileURLToPath, URL as NativeConstructor} from 'node:url';
class URL {constructor(input, base) {return new globalThis.URL('./actual.js', base);}}
const cache = {loadFile(value) {return value;}};
cache.loadFile('data.json');
@@ -22,11 +22,13 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
globalThis.localStorage.setItem('real', 'value');
const unrelated = {preload: './decoy.js'};
new Win({webPreferences:{preload:fileURLToPath(new URL('./decoy.js', import.meta.url))}});
new Win({webPreferences:{preload:fileURLToPath(new NativeConstructor('./native.js', import.meta.url))}});
new Win({webPreferences:{preload:'./preload.js'}}).loadFile('index.html');
`,
"actual.js": "export {};",
"decoy.js": "export {};",
"preload.js": "export {};",
"native.js": "export {};",
"data.json": "{}",
"index.html": "<!doctype html><title>Fixture</title>",
});
@@ -39,7 +41,9 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
roles.flatMap(({ observations }) =>
observations.map(({ properties }) => properties.resolved_path),
),
).toEqual(expect.arrayContaining(["preload.js", "index.html"]));
).toEqual(
expect.arrayContaining(["preload.js", "native.js", "index.html"]),
);
expect(
roles.flatMap(({ observations }) =>
observations.map(({ properties }) => properties.resolved_path),