From f8ff1579ad535fb0e63f6d247865207ca446d893 Mon Sep 17 00:00:00 2001 From: RRFZ <247457381+Plastic-time@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:46:22 -0400 Subject: [PATCH] test(reference): preserve optional resolver identity regressions (#6913) fix(reference): retain optional CommonJS resolver dependencies Co-authored-by: Plastic-time Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> --- ...iptStaticAnalysisLoaderInvocations.test.ts | 166 +++++++++++------- .../referenceSourceOptionalRequire.test.ts | 150 ++++++++++++++++ .../referenceSourceComputedRequire.test.ts | 45 ++--- 3 files changed, 276 insertions(+), 85 deletions(-) create mode 100644 tests/boundary/cli/referenceSourceOptionalRequire.test.ts diff --git a/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts b/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts index 84b6b39745..023b54aeca 100644 --- a/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts @@ -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(` diff --git a/tests/boundary/cli/referenceSourceOptionalRequire.test.ts b/tests/boundary/cli/referenceSourceOptionalRequire.test.ts new file mode 100644 index 0000000000..256caa0e34 --- /dev/null +++ b/tests/boundary/cli/referenceSourceOptionalRequire.test.ts @@ -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 = 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", + }, + ]); + }, +); diff --git a/tests/boundary/filesystem/referenceSourceComputedRequire.test.ts b/tests/boundary/filesystem/referenceSourceComputedRequire.test.ts index 1c1921678e..87ebb5d434 100644 --- a/tests/boundary/filesystem/referenceSourceComputedRequire.test.ts +++ b/tests/boundary/filesystem/referenceSourceComputedRequire.test.ts @@ -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",