Files
reverse-engineering-anything/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts
T

434 lines
13 KiB
TypeScript

import { Module, createRequire } from "node:module";
import * as path from "node:path";
import { runInNewContext } from "node:vm";
import { expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import { referenceSourceRequireCalls } from "../referenceSourceRequireBindings.js";
import { parseReferenceSourceImports } from "../referenceSourceImportParsing.js";
it.each(["require", "require.resolve", "require.main"])(
"distinguishes invoked loading from reference association for %s",
(callee) => {
const source = `${callee}('node:path');`;
const execute = () =>
runInNewContext(source, { require: createRequire(import.meta.url) });
if (callee === "require") expect(execute().join).toBe(path.join);
else if (callee === "require.resolve") expect(execute()).toBe("node:path");
else expect(execute).toThrow();
const analysis = analyzeJavaScriptStaticSource(source);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.references).toEqual(
callee === "require"
? [expect.objectContaining({ kind: "require", specifier: "node:path" })]
: callee === "require.resolve"
? [
expect.objectContaining({
kind: "require",
specifier: "node:path",
}),
]
: [],
);
const parsed = parseJavaScriptSource(source);
if (parsed === null) throw new Error("Expected loader association fixture");
expect(referenceSourceRequireCalls(parsed, "entry.cjs").size).toBe(
callee === "require.main" ? 0 : 1,
);
},
);
const imported = "import {createRequire as make} from 'node:module';";
it.each([
[
"proven factory",
`${imported}const require=make(import.meta.url);require('node:path');`,
1,
"node:path",
],
[
"renamed proven factory",
`${imported}const load=make(import.meta.url);load('node:path');`,
1,
"node:path",
],
[
"opaque replacement factory",
"import*as mod from'node:module';mod.default.createRequire=replacement;const require=mod.default.createRequire(import.meta.url);require('node:path');",
0,
null,
],
] as const)(
"uses invocation proof for external source association after a %s",
(_name, source, associated, specifier) => {
const parsed = parseJavaScriptSource(source);
if (parsed === null) throw new Error("Expected factory proof fixture");
expect(referenceSourceRequireCalls(parsed, "entry.mjs").size).toBe(
associated,
);
const analysis = analyzeJavaScriptStaticSource(source);
expect(analysis.parse_error_count).toBe(0);
expect(
analysis.references.filter(({ kind }) => kind === "require"),
).toEqual([expect.objectContaining({ kind: "require", specifier })]);
},
);
it("associates a renamed createRequire resolve method with search options", () => {
const parsed = parseJavaScriptSource(
`${imported}const load=make(import.meta.url);load.resolve('node:path',{paths:[]});`,
);
if (parsed === null) throw new Error("Expected factory proof fixture");
expect(referenceSourceRequireCalls(parsed, "entry.mjs").size).toBe(1);
});
it("keeps current-file and mixed createRequire bases distinct", () => {
const currentFile = parseJavaScriptSource(
`${imported}const load=make(import.meta.url);load('./dep.cjs');`,
);
if (currentFile === null)
throw new Error("Expected current-file base fixture");
const currentCall = [
...referenceSourceRequireCalls(currentFile, "sub/main.mjs").keys(),
][0];
expect(
referenceSourceRequireCalls(currentFile, "sub/main.mjs").get(currentCall!)
?.resolutionBase,
).toBe("source-file");
const mixed = parseJavaScriptSource(
`${imported}const load=make(flag?import.meta.url:new URL('./sub/entry.cjs',import.meta.url));load('./dep.cjs');`,
);
if (mixed === null) throw new Error("Expected mixed-base fixture");
const mixedBases = referenceSourceRequireCalls(mixed, "main.mjs");
expect(
[...mixedBases.values()].map(({ resolutionBase }) => resolutionBase),
).toEqual(["unknown"]);
});
it("keeps literal CommonJS resolve calls in historical source parsing", () => {
const source = [
'require.resolve("./dep.cjs");',
'require.resolve("./same-path.cjs", { paths: [__dirname] });',
'require.resolve("./dep.cjs", { paths: [__dirname + "/sub"] });',
'require.resolve("./dep.cjs", { paths: [] });',
'function shadow(__dirname) { require.resolve("./shadow.cjs", { paths: [__dirname] }); }',
].join("\n");
const result = parseReferenceSourceImports(
"main.cjs",
new TextEncoder().encode(source),
"JavaScript",
);
expect(
result.relationships.map(({ to, resolution, parse_state }) => ({
to,
resolution,
parse_state,
})),
).toEqual([
{ to: "./dep.cjs", resolution: "internal", parse_state: "parsed" },
{ to: "./same-path.cjs", resolution: "internal", parse_state: "parsed" },
{ to: "./dep.cjs", resolution: "unknown", parse_state: "partial" },
{ to: "./dep.cjs", resolution: "unknown", parse_state: "partial" },
{ to: "./shadow.cjs", resolution: "unknown", parse_state: "partial" },
]);
});
it("retains a mutated require.resolve request as an unknown fact", () => {
const source = `${imported}const load=make(import.meta.url);load.resolve=custom;load.resolve("./dep.cjs");`;
const result = parseReferenceSourceImports(
"main.mjs",
new TextEncoder().encode(source),
"JavaScript",
);
expect(
result.relationships.filter(({ kind }) => kind === "requires"),
).toEqual([
expect.objectContaining({
from_path: "main.mjs",
to: "./dep.cjs",
kind: "requires",
resolution: "unknown",
parse_state: "partial",
}),
]);
expect(
analyzeJavaScriptStaticSource(source).references.filter(
({ kind }) => kind === "require",
),
).toEqual([
expect.objectContaining({
kind: "require",
operation: "load",
specifier: null,
}),
]);
});
it("does not treat bundler factory resolve members as Node resolvers", () => {
const analysis = analyzeJavaScriptStaticSource(`
globalThis.webpackChunkApp.push([[1], {
"factory": function (module, exports, loader) {
loader.resolve("./not-a-dependency.cjs");
}
}]);
`);
expect(analysis.references.filter(({ kind }) => kind === "require")).toEqual(
[],
);
});
it.each([
[
"captured wrapper",
"const load=require;load('node:path')",
"node:path",
true,
],
[
"wrapper member",
"const box={load:require};box.load('node:path')",
"node:path",
true,
],
[
"wrapper array",
"const box=[require];box[0]('node:path')",
"node:path",
true,
],
[
"projected wrapper",
"const {load}={load:require};load('node:path')",
"node:path",
true,
],
[
"captured before wrapper replacement",
"const load=require;require=()=>({});load('node:path')",
"node:path",
true,
],
[
"shadowed wrapper",
"const require=()=>({});const load=require;load('node:path')",
undefined,
false,
],
[
"delegate replaced after capture",
"const load=require;module.require=()=>({});load('node:path')",
null,
false,
],
] as const)(
"retains the selected native %s value",
(_name, source, specifier, host) => {
const module = new Module("wrapper-alias.cjs");
const result = runInNewContext(source, {
module,
require: (request: string) => module.require(request),
});
expect(result.join === path.join).toBe(host);
const analysis = analyzeJavaScriptStaticSource(source);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.references).toEqual(
specifier === undefined
? []
: [expect.objectContaining({ kind: "require", specifier })],
);
},
);
it.each([
[
"renamed loader",
"const load=make(import.meta.url);load('./shared.cjs')",
"./shared.cjs",
],
[
"captured renamed loader",
"const load=make(import.meta.url);const saved=load;saved('./shared.cjs')",
"./shared.cjs",
],
[
"object member loader",
"const box={load:make(import.meta.url)};box.load('./shared.cjs')",
"./shared.cjs",
],
[
"destructured loader",
"const {load}={load:make(import.meta.url)};load('./shared.cjs')",
"./shared.cjs",
],
[
"array member loader",
"const box=[make(import.meta.url)];box[0]('./shared.cjs')",
"./shared.cjs",
],
[
"generic loader invocation",
"const load=make(import.meta.url);load.call(null,'./shared.cjs')",
"./shared.cjs",
],
[
"reflected created loader",
"const load=make(import.meta.url);Reflect.apply(load,null,['./shared.cjs'])",
"./shared.cjs",
],
[
"unknown create base",
"const load=make('/elsewhere/entry.cjs');load('./shared.cjs')",
null,
],
[
"conditional create bases",
"const load=flag?make(import.meta.url):make('/elsewhere/entry.cjs');load('./shared.cjs')",
null,
],
[
"opaque loader replacement",
"let load=make(import.meta.url);load=replacement;load('./shared.cjs')",
null,
],
[
"copied child with a later leaf replacement",
"const box={child:{load:make(import.meta.url)}};const copy={...box};box.child.load=make('/elsewhere/entry.cjs');copy.child.load('./shared.cjs')",
null,
],
[
"copied child before a parent replacement",
"const box={child:{load:make(import.meta.url)}};const copy={...box};box.child={load:make('/elsewhere/entry.cjs')};copy.child.load('./shared.cjs')",
"./shared.cjs",
],
[
"spread excludes an inherited loader",
"const box={__proto__:{load:make(import.meta.url)}};const copy={...box};copy.load('./shared.cjs')",
undefined,
],
[
"local loader replacement",
"let load=make(import.meta.url);load=()=>({});load('./shared.cjs')",
undefined,
],
["local loader", "const load=()=>({});load('./shared.cjs')", undefined],
[
"shadowed create factory",
"function f(make){const load=make(import.meta.url);load('./shared.cjs')}",
undefined,
],
] as const)(
"classifies %s from the selected loader value",
(_name, body, specifier) => {
const result = analyzeJavaScriptStaticSource(`${imported}${body};`);
expect(result.parse_error_count).toBe(0);
const calls = result.references.filter(({ kind }) => kind === "require");
expect(calls).toEqual(
specifier === undefined ? [] : [expect.objectContaining({ specifier })],
);
},
);
it("preserves a renamed native createRequire callable", () => {
const load = createRequire(import.meta.url);
const saved = load;
expect(saved("node:path").join).toBe(path.join);
});
it.each(["leaf", "parent"])(
"keeps the native created loader identity after a copied child %s write",
(write) => {
const selected = runInNewContext(
`const original=make(base);const outside=make(outsideBase);
const box={child:{load:original}};const copy={...box};
${write === "leaf" ? "box.child.load=outside" : "box.child={load:outside}"};
({actual:copy.child.load,expected:${write === "leaf" ? "outside" : "original"}});`,
{
make: createRequire,
base: import.meta.url,
outsideBase: new URL("./elsewhere/entry.cjs", import.meta.url).href,
},
);
expect(selected.actual).toBe(selected.expected);
},
);
it.each([
[
"direct Reflect.apply",
"Reflect.apply(module.require,module,['node:path'])",
true,
],
[
"captured Reflect.apply",
"const invoke=Reflect.apply;invoke(module.require,module,['node:path'])",
true,
],
[
"generic Reflect.apply",
"Reflect.apply.call(null,module.require,module,['node:path'])",
true,
],
[
"Reflect.apply captured before replacement",
"const invoke=Reflect.apply;Reflect.apply=()=>({join:false});invoke(module.require,module,['node:path'])",
true,
],
[
"local Reflect",
"const Reflect={apply(){return {join:false}}};Reflect.apply(module.require,module,['node:path'])",
false,
],
[
"local callable",
"Reflect.apply(()=>({join:false}),module,['node:path'])",
false,
],
[
"local module",
"const module={require(){return {join:false}}};Reflect.apply(module.require,module,['node:path'])",
false,
],
] as const)(
"preserves module delegate provenance through %s",
(_name, source, host) => {
const result = runInNewContext(source, {
module: new Module("reflected-loader.cjs"),
});
expect(result.join === path.join).toBe(host);
const analysis = analyzeJavaScriptStaticSource(source);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.references).toEqual(
host
? [expect.objectContaining({ kind: "require", specifier: null })]
: [],
);
},
);
it("retains unknown historical .js loader scope until a package manifest establishes it", () => {
const bytes = new TextEncoder().encode('require("./dep.cjs");');
const relationships = (
mode?: "module" | "commonjs" | "unspecified" | "unknown",
) =>
parseReferenceSourceImports("main.js", bytes, "JavaScript", mode)
.relationships;
expect(relationships()).toEqual([
expect.objectContaining({
to: "./dep.cjs",
resolution: "unknown",
parse_state: "partial",
}),
]);
expect(relationships("module")).toEqual([]);
for (const mode of ["commonjs", "unspecified"] as const)
expect(relationships(mode)).toEqual([
expect.objectContaining({
to: "./dep.cjs",
resolution: "internal",
parse_state: "parsed",
}),
]);
});