mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
test(reference): preserve optional resolver identity regressions (#6913)
fix(reference): retain optional CommonJS resolver dependencies Co-authored-by: Plastic-time <Plastic-time@users.noreply.github.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
@@ -8,21 +8,31 @@ import { parseJavaScriptSource } from "./javascriptSourceParser.js";
|
||||
import { referenceSourceRequireCalls } from "../referenceSourceRequireBindings.js";
|
||||
import { parseReferenceSourceImports } from "../referenceSourceImportParsing.js";
|
||||
|
||||
it.each(["require", "require.resolve", "require.main"])(
|
||||
it.each([
|
||||
["require", "load"],
|
||||
["require?.", "load"],
|
||||
["require.resolve", "resolve"],
|
||||
["require.resolve?.", "resolve"],
|
||||
["require?.resolve", "resolve"],
|
||||
["require?.resolve?.", "resolve"],
|
||||
['require?.["resolve"]', "resolve"],
|
||||
["(require?.resolve)", "resolve"],
|
||||
["require.main", "not-callable"],
|
||||
] as const)(
|
||||
"distinguishes invoked loading from reference association for %s",
|
||||
(callee) => {
|
||||
(callee, operation) => {
|
||||
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");
|
||||
if (operation === "load") expect(execute().join).toBe(path.join);
|
||||
else if (operation === "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"
|
||||
operation === "load"
|
||||
? [expect.objectContaining({ kind: "require", specifier: "node:path" })]
|
||||
: callee === "require.resolve"
|
||||
: operation === "resolve"
|
||||
? [
|
||||
expect.objectContaining({
|
||||
kind: "require",
|
||||
@@ -34,7 +44,27 @@ it.each(["require", "require.resolve", "require.main"])(
|
||||
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,
|
||||
operation === "not-callable" ? 0 : 1,
|
||||
);
|
||||
const historical = parseReferenceSourceImports(
|
||||
"entry.cjs",
|
||||
new TextEncoder().encode(source),
|
||||
"JavaScript",
|
||||
);
|
||||
expect(historical.parse_failures).toEqual([]);
|
||||
expect(historical.relationships).toEqual(
|
||||
operation === "not-callable"
|
||||
? []
|
||||
: [
|
||||
{
|
||||
from_path: "entry.cjs",
|
||||
to: "node:path",
|
||||
kind: "requires",
|
||||
resolution: "external",
|
||||
parse_state: "parsed",
|
||||
resolutionStrategy: "runtime-require",
|
||||
},
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -107,64 +137,72 @@ it("keeps current-file and mixed createRequire bases distinct", () => {
|
||||
).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.each(["require.resolve", "require.resolve?.", "require?.resolve"])(
|
||||
"keeps search-path uncertainty for historical %s calls",
|
||||
(callee) => {
|
||||
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")
|
||||
.replaceAll("require.resolve", callee);
|
||||
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.each(["load.resolve", "load.resolve?.", "load?.resolve"])(
|
||||
"retains a mutated %s request as an unknown fact",
|
||||
(callee) => {
|
||||
const source = `${imported}const load=make(import.meta.url);load.resolve=custom;${callee}("./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(`
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { expect } from "vitest";
|
||||
import { historicalSourceGraphSchema } from "../../../src/domain/referenceSourceGraph.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest.skipIf(process.platform === "win32")(
|
||||
"retains optional loader dependencies with their original identity and lookup uncertainty",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-reference-optional-require-",
|
||||
);
|
||||
const config = await createTestTempDirectory(
|
||||
"rea-reference-optional-config-",
|
||||
);
|
||||
const resolverCallees = [
|
||||
"require.resolve",
|
||||
"require.resolve?.",
|
||||
"require?.resolve",
|
||||
"require?.resolve?.",
|
||||
'require?.["resolve"]',
|
||||
"(require?.resolve)",
|
||||
];
|
||||
const sources: Record<string, string> = Object.fromEntries(
|
||||
resolverCallees.map((callee, index) => [
|
||||
`resolver-${index}.cjs`,
|
||||
`console.log(${callee}("./dep.cjs"));`,
|
||||
]),
|
||||
);
|
||||
Object.assign(sources, {
|
||||
"bare.cjs": 'require?.("./dep.cjs");',
|
||||
"shadow.cjs": 'function load(require) { require?.resolve("./dep.cjs"); }',
|
||||
"null.cjs":
|
||||
'function load() { const require = null; require?.resolve("./dep.cjs"); }',
|
||||
"computed.cjs":
|
||||
'const member = "resolve"; require?.[member]("./dep.cjs");',
|
||||
"dynamic.cjs": "require.resolve?.(getTarget());",
|
||||
"missing-wrapper.mjs": 'require?.resolve("./dep.cjs");',
|
||||
"overwritten.cjs":
|
||||
'require.resolve = () => "other"; require?.resolve("./dep.cjs");',
|
||||
"deleted.cjs": 'delete require.resolve; require.resolve?.("./dep.cjs");',
|
||||
"later-write.cjs":
|
||||
'require?.resolve("./dep.cjs"); require.resolve = () => "other";',
|
||||
"search-paths.cjs": 'require?.resolve("./dep.cjs", { paths: [] });',
|
||||
"own-directory.cjs":
|
||||
'require.resolve?.("./dep.cjs", { paths: [__dirname] });',
|
||||
"factory.mjs":
|
||||
'import { createRequire } from "node:module"; const local = createRequire(new URL("./sub/entry.cjs", import.meta.url)); local?.resolve("./dep.cjs");',
|
||||
"dep.cjs": 'throw new Error("Dependency must not execute");',
|
||||
"sub/dep.cjs": 'throw new Error("Nested dependency must not execute");',
|
||||
"sub/entry.cjs": "",
|
||||
});
|
||||
await mkdir(join(root, "sub"));
|
||||
await Promise.all(
|
||||
Object.entries(sources).map(([path, source]) =>
|
||||
writeFile(join(root, path), source),
|
||||
),
|
||||
);
|
||||
|
||||
// Resolve-only Node controls confirm the equivalent spellings without loading
|
||||
// the throwing dependency. REA only reads these self-authored fixtures.
|
||||
for (const [index] of resolverCallees.entries()) {
|
||||
const executed = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[join(root, `resolver-${index}.cjs`)],
|
||||
{ timeout: 5_000 },
|
||||
);
|
||||
expect(executed.stdout.trim()).toBe(join(root, "dep.cjs"));
|
||||
expect(executed.stderr).toBe("");
|
||||
}
|
||||
const result = await cli.run({
|
||||
arguments: ["import-reference-source", root, "--json"],
|
||||
environment: {
|
||||
HOME: config,
|
||||
XDG_CONFIG_HOME: config,
|
||||
XDG_CACHE_HOME: config,
|
||||
REA_REFERENCE_SECRET_PATTERNS_JSON: "[]",
|
||||
},
|
||||
});
|
||||
expect(result.exitCode, result.stderr + JSON.stringify(result.json)).toBe(
|
||||
0,
|
||||
);
|
||||
const graph = historicalSourceGraphSchema.parse(result.json);
|
||||
expect(graph.parse_failures).toEqual([]);
|
||||
for (const [path, source] of Object.entries(sources))
|
||||
expect(graph.entries).toContainEqual(
|
||||
expect.objectContaining({
|
||||
path,
|
||||
sha256: createHash("sha256").update(source).digest("hex"),
|
||||
}),
|
||||
);
|
||||
for (const path of [
|
||||
"bare.cjs",
|
||||
"later-write.cjs",
|
||||
"own-directory.cjs",
|
||||
...resolverCallees.map((_, index) => `resolver-${index}.cjs`),
|
||||
])
|
||||
expect(
|
||||
graph.relationships.filter(({ from_path }) => from_path === path),
|
||||
).toEqual([
|
||||
{
|
||||
from_path: path,
|
||||
to: "dep.cjs",
|
||||
kind: "requires",
|
||||
resolution: "internal",
|
||||
parse_state: "parsed",
|
||||
},
|
||||
]);
|
||||
for (const path of [
|
||||
"shadow.cjs",
|
||||
"null.cjs",
|
||||
"computed.cjs",
|
||||
"dynamic.cjs",
|
||||
"missing-wrapper.mjs",
|
||||
])
|
||||
expect(
|
||||
graph.relationships.filter(({ from_path }) => from_path === path),
|
||||
).toEqual([]);
|
||||
for (const path of ["overwritten.cjs", "deleted.cjs", "search-paths.cjs"])
|
||||
expect(
|
||||
graph.relationships.filter(({ from_path }) => from_path === path),
|
||||
).toEqual([
|
||||
{
|
||||
from_path: path,
|
||||
to: "./dep.cjs",
|
||||
kind: "requires",
|
||||
resolution: "unknown",
|
||||
parse_state: "partial",
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
graph.relationships.filter(
|
||||
({ from_path, kind }) =>
|
||||
from_path === "factory.mjs" && kind === "requires",
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
from_path: "factory.mjs",
|
||||
to: "sub/dep.cjs",
|
||||
kind: "requires",
|
||||
resolution: "internal",
|
||||
parse_state: "parsed",
|
||||
},
|
||||
]);
|
||||
},
|
||||
);
|
||||
@@ -20,23 +20,27 @@ const hashedSourceEntry = (path: string, source: string) => ({
|
||||
limitations: [],
|
||||
});
|
||||
|
||||
const memberFixtures = [
|
||||
"require",
|
||||
"require.resolve",
|
||||
'require["resolve"]',
|
||||
"require.main",
|
||||
'require["main"]',
|
||||
'require[("resolve")]',
|
||||
'require["res\\u006flve"]',
|
||||
"require?.",
|
||||
"require.resolve?.",
|
||||
'require["resolve"]?.',
|
||||
"require?.resolve",
|
||||
'require?.["resolve"]',
|
||||
"(require?.resolve)",
|
||||
].map((callee, index, callees) => ({
|
||||
callee,
|
||||
path: `member-${String(index).padStart(String(callees.length - 1).length, "0")}.cjs`,
|
||||
}));
|
||||
|
||||
it("distinguishes computed require member values from literal members in source evidence", async () => {
|
||||
const root = await createTestTempDirectory("rea-reference-require-members-");
|
||||
const literalCallees = [
|
||||
"require",
|
||||
"require.resolve",
|
||||
'require["resolve"]',
|
||||
"require.main",
|
||||
'require["main"]',
|
||||
'require[("resolve")]',
|
||||
'require["res\\u006flve"]',
|
||||
"require?.",
|
||||
"require.resolve?.",
|
||||
'require["resolve"]?.',
|
||||
"require?.resolve",
|
||||
'require?.["resolve"]',
|
||||
"(require?.resolve)",
|
||||
];
|
||||
const ignoredSources = [
|
||||
'require[resolve]("./dep.js");',
|
||||
'require[main]("./dep.js");',
|
||||
@@ -76,7 +80,7 @@ it("distinguishes computed require member values from literal members in source
|
||||
"ignored.cjs": ignoredSources.map((source) => `{ ${source} }`).join("\n"),
|
||||
"unknown.cjs": [
|
||||
"function load(target) {",
|
||||
...literalCallees.flatMap((callee) => [
|
||||
...memberFixtures.flatMap(({ callee }) => [
|
||||
` ${callee}(target);`,
|
||||
` ${callee}(\`./dep.js\`);`,
|
||||
` ${callee}(getTarget());`,
|
||||
@@ -86,9 +90,8 @@ it("distinguishes computed require member values from literal members in source
|
||||
"}",
|
||||
].join("\n"),
|
||||
};
|
||||
for (const [index, callee] of literalCallees.entries())
|
||||
sources[`member-${String(index).padStart(2, "0")}.cjs`] =
|
||||
`${callee}("./dep.js");\n`;
|
||||
for (const { path, callee } of memberFixtures)
|
||||
sources[path] = `${callee}("./dep.js");\n`;
|
||||
await Promise.all(
|
||||
Object.entries(sources).map(([path, source]) =>
|
||||
writeFile(join(root, path), source),
|
||||
@@ -133,12 +136,12 @@ it("distinguishes computed require member values from literal members in source
|
||||
resolution: "internal",
|
||||
parse_state: "parsed",
|
||||
},
|
||||
...literalCallees.flatMap((callee, index) =>
|
||||
...memberFixtures.flatMap(({ callee, path }) =>
|
||||
callee === "require.main" || callee === 'require["main"]'
|
||||
? []
|
||||
: [
|
||||
{
|
||||
from_path: `member-${String(index).padStart(2, "0")}.cjs`,
|
||||
from_path: path,
|
||||
to: "dep.js",
|
||||
kind: "requires",
|
||||
resolution: "internal",
|
||||
|
||||
Reference in New Issue
Block a user