Fixes #3341 Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
417 lines
14 KiB
TypeScript
417 lines
14 KiB
TypeScript
import { expect, it } from "vitest";
|
|
|
|
import { JavaScriptModuleRangeIndex } from "./javascriptStaticAnalysisFindings.js";
|
|
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
|
|
|
it.each(["direct", "call"])(
|
|
"recovers standalone module factories dispatched with %s",
|
|
(style) => {
|
|
const dispatch =
|
|
style === "call"
|
|
? "modules[id].call(module.exports, module, module.exports, load)"
|
|
: "modules[id](module, module.exports, load)";
|
|
const source = `(() => {
|
|
const modules = {
|
|
17(module, exports, req) { exports.read = req(23); },
|
|
23(module) { module.exports = require("electron"); }
|
|
};
|
|
const cache = {};
|
|
function load(id) {
|
|
if (cache[id]) return cache[id].exports;
|
|
const module = cache[id] = { exports: {} };
|
|
${dispatch};
|
|
return module.exports;
|
|
}
|
|
load(17);
|
|
})();`;
|
|
const analysis = analyzeJavaScriptStaticSource(source);
|
|
expect(analysis.bundler_registrations).toMatchObject([
|
|
{
|
|
bundler: "webpack",
|
|
entry_module_keys: ["17"],
|
|
modules: [{ module_key: "17" }, { module_key: "23" }],
|
|
},
|
|
]);
|
|
expect(analysis.references).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ specifier: "23", module_key: "17" }),
|
|
expect.objectContaining({ specifier: "electron", module_key: "23" }),
|
|
]),
|
|
);
|
|
},
|
|
);
|
|
|
|
it("does not infer a bootstrap from a function table without a matching loader", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(`(() => {
|
|
const handlers = { 17(module) { module.exports = require("electron"); } };
|
|
function load(id) { return handlers[id](userInput); }
|
|
load(17);
|
|
})();`);
|
|
expect(analysis.bundler_registrations).toEqual([]);
|
|
});
|
|
|
|
it("does not treat a same-named shadow as the factory exports parameter", () => {
|
|
const analysis =
|
|
analyzeJavaScriptStaticSource(`globalThis.webpackChunkApp.push([[1], {
|
|
first(module, e, require) {
|
|
{ const e = {}; require.d(e, { shadowed: () => 1 }); e.other = 2; }
|
|
}
|
|
}]);`);
|
|
expect(analysis.bundler_registrations[0]?.modules).toMatchObject([
|
|
{ module_key: "first", exports: [] },
|
|
]);
|
|
});
|
|
|
|
it("does not resolve a parameter named require as a module dependency", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(
|
|
`function run(require) { return require("./ghost.cjs"); }`,
|
|
);
|
|
expect(analysis.references).not.toContainEqual(
|
|
expect.objectContaining({ specifier: "./ghost.cjs" }),
|
|
);
|
|
expect(analysis.references).toContainEqual(
|
|
expect.objectContaining({
|
|
kind: "require",
|
|
specifier: null,
|
|
expression: 'require("./ghost.cjs")',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("keeps a formatless top-level require redeclaration unresolved", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(
|
|
`var require; require("./wrapper.cjs");`,
|
|
);
|
|
expect(analysis.references).toContainEqual(
|
|
expect.objectContaining({ kind: "require", specifier: null }),
|
|
);
|
|
});
|
|
|
|
it("retains the wrapper dependency evaluated inside its replacing assignment", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(
|
|
`var require; require = require("./before-replacement.cjs");`,
|
|
);
|
|
expect(analysis.references).toContainEqual(
|
|
expect.objectContaining({ specifier: "./before-replacement.cjs" }),
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"a deferred function assignment",
|
|
`var require; function load() { require = require("./maybe.cjs"); } load(); load();`,
|
|
],
|
|
[
|
|
"an assignment repeated by a loop",
|
|
`var require; while (condition) { require = require("./maybe.cjs"); }`,
|
|
],
|
|
])("keeps %s unknown", (_label, source) => {
|
|
const analysis = analyzeJavaScriptStaticSource(source);
|
|
expect(analysis.references).not.toContainEqual(
|
|
expect.objectContaining({ specifier: "./maybe.cjs" }),
|
|
);
|
|
expect(analysis.references).toContainEqual(
|
|
expect.objectContaining({ kind: "require", specifier: null }),
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"a skipped wrapper write",
|
|
`var require; if (false) require = localLoader; require("./ghost.cjs");`,
|
|
"./ghost.cjs",
|
|
],
|
|
[
|
|
"a skipped createRequire write",
|
|
`import { createRequire } from "node:module"; let require = () => "local"; if (false) require = createRequire(import.meta.url); require("./ghost.cjs");`,
|
|
"./ghost.cjs",
|
|
],
|
|
[
|
|
"a class static field write before its computed key in source order",
|
|
`var require; class C { static value = (require = () => "local"); [require("./computed-key.cjs")]() {} }`,
|
|
"./computed-key.cjs",
|
|
],
|
|
[
|
|
"a loop condition write before a later call",
|
|
`var require; while ((require = () => "local", false)) {} require("./after-loop.cjs");`,
|
|
"./after-loop.cjs",
|
|
],
|
|
])("preserves uncertainty for %s", (_label, source, specifier) => {
|
|
const analysis = analyzeJavaScriptStaticSource(source);
|
|
expect(analysis.references).not.toContainEqual(
|
|
expect.objectContaining({ specifier }),
|
|
);
|
|
expect(analysis.references).toContainEqual(
|
|
expect.objectContaining({ kind: "require", specifier: null }),
|
|
);
|
|
});
|
|
|
|
it("keeps a proven createRequire binding", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
import { createRequire as makeRequire } from "node:module";
|
|
const require = makeRequire(import.meta.url);
|
|
require("./created.cjs");
|
|
`);
|
|
expect(analysis.references).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ specifier: "./created.cjs" }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("recognizes only factory loader calls and supported bind references", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
globalThis.webpackChunkApp.push([[1], {
|
|
"factory": function (module, exports, loader) {
|
|
loader.resolve("./not-a-dependency.cjs");
|
|
loader("./dependency.cjs");
|
|
loader.bind(module, "./bound-dependency.cjs");
|
|
}
|
|
}]);
|
|
`);
|
|
expect(analysis.references.map(({ specifier }) => specifier)).toEqual(
|
|
expect.arrayContaining(["./dependency.cjs", "./bound-dependency.cjs"]),
|
|
);
|
|
expect(analysis.references).not.toContainEqual(
|
|
expect.objectContaining({ specifier: "./not-a-dependency.cjs" }),
|
|
);
|
|
});
|
|
|
|
it("keeps reassigned factory loaders unknown instead of resolving imports", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
globalThis.webpackChunkApp.push([[1], {
|
|
"factory": function (module, exports, loader) {
|
|
loader = userInput;
|
|
loader("./uncertain.cjs");
|
|
}
|
|
}]);
|
|
`);
|
|
expect(analysis.references).not.toContainEqual(
|
|
expect.objectContaining({ specifier: "./uncertain.cjs" }),
|
|
);
|
|
expect(analysis.references).toContainEqual(
|
|
expect.objectContaining({ kind: "require", specifier: null }),
|
|
);
|
|
});
|
|
|
|
it("keeps a reassigned function named require unknown", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
function require() { return userInput; }
|
|
require = replacement;
|
|
require("./uncertain.cjs");
|
|
`);
|
|
expect(analysis.references).not.toContainEqual(
|
|
expect.objectContaining({ specifier: "./uncertain.cjs" }),
|
|
);
|
|
expect(analysis.references).toContainEqual(
|
|
expect.objectContaining({ kind: "require", specifier: null }),
|
|
);
|
|
});
|
|
|
|
it("does not treat a shadowed webpack loader name as bundler ownership", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
function local(__webpack_require__) {
|
|
__webpack_require__("./ghost.cjs");
|
|
__webpack_require__.e("ghost-chunk");
|
|
}
|
|
`);
|
|
expect(analysis.references).not.toContainEqual(
|
|
expect.objectContaining({ specifier: "./ghost.cjs" }),
|
|
);
|
|
expect(analysis.references).not.toContainEqual(
|
|
expect.objectContaining({ specifier: "chunk:ghost-chunk" }),
|
|
);
|
|
});
|
|
|
|
it("uses recovered factory and runtime loader identities for webpack imports and chunks", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
globalThis.webpackChunkApp.push([[1], {
|
|
"factory": function (module, exports, __webpack_require__) {
|
|
__webpack_require__("./factory-dependency.cjs");
|
|
__webpack_require__.e("factory-chunk");
|
|
}
|
|
}, function (__webpack_require__) {
|
|
__webpack_require__("factory");
|
|
__webpack_require__.e("runtime-chunk");
|
|
}]);
|
|
`);
|
|
expect(analysis.references.map(({ specifier }) => specifier)).toEqual(
|
|
expect.arrayContaining([
|
|
"./factory-dependency.cjs",
|
|
"chunk:factory-chunk",
|
|
"chunk:runtime-chunk",
|
|
]),
|
|
);
|
|
expect(analysis.bundler_registrations[0]?.async_chunk_keys).toEqual(
|
|
expect.arrayContaining(["factory-chunk", "runtime-chunk"]),
|
|
);
|
|
});
|
|
|
|
it("does not attribute a shadowed factory loader parameter to async chunks", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
globalThis.webpackChunkApp.push([[1], {
|
|
"factory": function (module, exports, loader) {
|
|
function local(__webpack_require__) {
|
|
__webpack_require__.e("ghost-chunk");
|
|
}
|
|
loader.e("real-chunk");
|
|
}
|
|
}]);
|
|
`);
|
|
expect(analysis.bundler_registrations[0]?.async_chunk_keys).toEqual([
|
|
"real-chunk",
|
|
]);
|
|
expect(analysis.references).not.toContainEqual(
|
|
expect.objectContaining({ specifier: "chunk:ghost-chunk" }),
|
|
);
|
|
});
|
|
|
|
it("does not confuse a shadowed dispatch table with the outer module table", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(`(() => {
|
|
const modules = { 17(module) { module.exports = require("electron"); } };
|
|
function load(id) {
|
|
const modules = userInput;
|
|
const module = { exports: {} };
|
|
modules[id](module, module.exports, load);
|
|
return module.exports;
|
|
}
|
|
load(17);
|
|
})();`);
|
|
expect(analysis.bundler_registrations).toEqual([]);
|
|
});
|
|
|
|
it("retains a stable var module table without treating declaration mutability as a write", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(`(() => {
|
|
var modules = { 17(module) { module.exports = require("electron"); } };
|
|
function load(id) {
|
|
const module = { exports: {} };
|
|
modules[id](module, module.exports, load);
|
|
return module.exports;
|
|
}
|
|
load(17);
|
|
})();`);
|
|
expect(analysis.bundler_registrations).toMatchObject([
|
|
{ entry_module_keys: ["17"], modules: [{ module_key: "17" }] },
|
|
]);
|
|
});
|
|
|
|
it("only attributes bootstrap entries to the recovered loader binding", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(`(() => {
|
|
const modules = { 17(module) { module.exports = require("electron"); } };
|
|
function load(id) {
|
|
const module = { exports: {} };
|
|
modules[id](module, module.exports, load);
|
|
return module.exports;
|
|
}
|
|
{ const load = userInput; load(99); }
|
|
load(17);
|
|
})();`);
|
|
expect(analysis.bundler_registrations).toMatchObject([
|
|
{ entry_module_keys: ["17"] },
|
|
]);
|
|
});
|
|
|
|
it("preserves first insertion precedence and inclusive interval bounds", () => {
|
|
const nested = new JavaScriptModuleRangeIndex(100);
|
|
expect(nested.find(0)).toBeUndefined();
|
|
nested.add({
|
|
start: 10,
|
|
end: 90,
|
|
key: "outer-first",
|
|
runtime: "webpackChunkApp",
|
|
requireName: null,
|
|
});
|
|
nested.add({
|
|
start: 20,
|
|
end: 30,
|
|
key: "inner-second",
|
|
runtime: "webpackChunkApp",
|
|
requireName: null,
|
|
});
|
|
nested.add({
|
|
start: 30,
|
|
end: 40,
|
|
key: "later-overlap",
|
|
runtime: "webpackChunkApp",
|
|
requireName: null,
|
|
});
|
|
expect(nested.find(9)).toBeUndefined();
|
|
expect(nested.find(10)?.key).toBe("outer-first");
|
|
expect(nested.find(20)?.key).toBe("outer-first");
|
|
expect(nested.find(30)?.key).toBe("outer-first");
|
|
expect(nested.find(90)?.key).toBe("outer-first");
|
|
expect(nested.find(91)).toBeUndefined();
|
|
expect(nested.find(100)).toBeUndefined();
|
|
});
|
|
|
|
it("rejects missing and out-of-range lookup offsets", () => {
|
|
const index = new JavaScriptModuleRangeIndex(20);
|
|
index.add({
|
|
start: 5,
|
|
end: 10,
|
|
key: "bounded",
|
|
runtime: "webpackChunkApp",
|
|
requireName: null,
|
|
});
|
|
expect(index.find(undefined)).toBeUndefined();
|
|
expect(index.find(null)).toBeUndefined();
|
|
expect(index.find(-1)).toBeUndefined();
|
|
expect(index.find(0)).toBeUndefined();
|
|
expect(index.find(11)).toBeUndefined();
|
|
expect(index.find(20)).toBeUndefined();
|
|
expect(index.find(Number.NaN)).toBeUndefined();
|
|
expect(index.find(5)?.key).toBe("bounded");
|
|
expect(index.find(10)?.key).toBe("bounded");
|
|
});
|
|
|
|
it("keeps module references attributed across many real factory ranges", () => {
|
|
const count = 200;
|
|
const factories = Array.from({ length: count }, (_, index) =>
|
|
index === 0
|
|
? '"module-0": function(module, exports, req) { globalThis.webpackChunkInner.push([[2], { "inner": function(innerModule, innerExports, req) { req("nested-dependency"); } }]); req("dependency-0"); }'
|
|
: `"module-${index}": function(module, exports, req) { req("dependency-${index}"); }`,
|
|
).join(",\n");
|
|
const analysis = analyzeJavaScriptStaticSource(
|
|
`require("outside");\nglobalThis.webpackChunkApp.push([[1], {\n${factories}\n}]);`,
|
|
);
|
|
expect(analysis.references).toHaveLength(count + 2);
|
|
expect(
|
|
analysis.bundler_registrations.some(
|
|
({ runtime, modules }) =>
|
|
runtime === "webpackChunkInner" &&
|
|
modules.some(({ module_key }) => module_key === "inner"),
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
analysis.references.find(({ specifier }) => specifier === "outside")
|
|
?.module_key,
|
|
).toBeNull();
|
|
for (let index = 0; index < count; index += 1) {
|
|
expect(
|
|
analysis.references.find(
|
|
({ specifier }) => specifier === `dependency-${index}`,
|
|
),
|
|
).toMatchObject({
|
|
module_key: `module-${index}`,
|
|
module_runtime: "webpackChunkApp",
|
|
});
|
|
}
|
|
expect(
|
|
analysis.references.find(
|
|
({ specifier }) => specifier === "nested-dependency",
|
|
)?.module_key,
|
|
).toBe("module-0");
|
|
});
|
|
|
|
it("retains the chunk runtime through a TypeScript-only expression wrapper", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(
|
|
'(globalThis.webpackChunkApp satisfies unknown).push([[1], { "entry": function () {} }]);',
|
|
);
|
|
expect(analysis.bundler_registrations).toEqual([
|
|
expect.objectContaining({
|
|
runtime: "webpackChunkApp",
|
|
modules: [expect.objectContaining({ module_key: "entry" })],
|
|
}),
|
|
]);
|
|
});
|