From e966b9f9a526e1f4bb8a77742fac540a2f2d7add Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:34:45 +0800 Subject: [PATCH] refactor(javascript): name retained require call facts explicitly --- docs/cli.md | 11 +++- ...iptStaticAnalysisLoaderInvocations.test.ts | 17 +++--- src/domain/referenceSourceImportParsing.ts | 8 +-- src/domain/referenceSourceRequireBindings.ts | 2 +- .../referenceSourceComputedRequire.test.ts | 57 +++++++++++++------ .../referenceSourceShadowedRequire.test.ts | 18 +++++- 6 files changed, 77 insertions(+), 36 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 12226d6e7a..d54b7f89eb 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -410,11 +410,16 @@ strings, expression-free templates and runtime-transparent TypeScript wrappers retain their targets; computed imports remain unknown. TypeScript import-equals dependencies remain `requires` relationships whether or not they are exported. -CommonJS `require` relationships retain the requested specifier. REA resolves -them only when the loader base is proven: this includes the exact +REA derives historical CommonJS paths only when the selected loader and its +base are proven: this includes the exact `createRequire(new URL("./file.cjs", import.meta.url))` form and `require.resolve` with `{ paths: [__dirname] }`. Other custom bases and search -options remain partial with unknown resolution. An excluded `package.json` +options remain partial with unknown resolution and the literal requested +specifier. Overwritten resolver methods retain that uncertainty; they are not +attributed to Node. `require.main` is a module object, so calling it does not +establish a loader reference. These relationships do not establish execution; +Node's [`require.resolve`](https://nodejs.org/docs/v24.22.1/api/modules.html#requireresolverequest-options) +performs lookup without loading the target. An excluded `package.json` also leaves package-directory selection unknown rather than treating the manifest as absent and applying an `index` fallback. diff --git a/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts b/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts index ca6c1cbad5..1e2c5d3405 100644 --- a/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts @@ -5,7 +5,7 @@ import { expect, it } from "vitest"; import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; -import { referenceSourceRequireCallBases } from "../referenceSourceRequireBindings.js"; +import { referenceSourceRequireCalls } from "../referenceSourceRequireBindings.js"; import { parseReferenceSourceImports } from "../referenceSourceImportParsing.js"; it.each(["require", "require.resolve", "require.main"])( @@ -33,7 +33,7 @@ it.each(["require", "require.resolve", "require.main"])( ); const parsed = parseJavaScriptSource(source); if (parsed === null) throw new Error("Expected loader association fixture"); - expect(referenceSourceRequireCallBases(parsed, "entry.cjs").size).toBe( + expect(referenceSourceRequireCalls(parsed, "entry.cjs").size).toBe( callee === "require.main" ? 0 : 1, ); }, @@ -64,7 +64,7 @@ it.each([ (_name, source, associated, specifier) => { const parsed = parseJavaScriptSource(source); if (parsed === null) throw new Error("Expected factory proof fixture"); - expect(referenceSourceRequireCallBases(parsed, "entry.mjs").size).toBe( + expect(referenceSourceRequireCalls(parsed, "entry.mjs").size).toBe( associated, ); const analysis = analyzeJavaScriptStaticSource(source); @@ -80,7 +80,7 @@ it("associates a renamed createRequire resolve method with search options", () = `${imported}const load=make(import.meta.url);load.resolve('node:path',{paths:[]});`, ); if (parsed === null) throw new Error("Expected factory proof fixture"); - expect(referenceSourceRequireCallBases(parsed, "entry.mjs").size).toBe(1); + expect(referenceSourceRequireCalls(parsed, "entry.mjs").size).toBe(1); }); it("keeps current-file and mixed createRequire bases distinct", () => { @@ -90,19 +90,18 @@ it("keeps current-file and mixed createRequire bases distinct", () => { if (currentFile === null) throw new Error("Expected current-file base fixture"); const currentCall = [ - ...referenceSourceRequireCallBases(currentFile, "sub/main.mjs").keys(), + ...referenceSourceRequireCalls(currentFile, "sub/main.mjs").keys(), ][0]; expect( - referenceSourceRequireCallBases(currentFile, "sub/main.mjs").get( - currentCall!, - )?.resolutionBase, + 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 = referenceSourceRequireCallBases(mixed, "main.mjs"); + const mixedBases = referenceSourceRequireCalls(mixed, "main.mjs"); expect( [...mixedBases.values()].map(({ resolutionBase }) => resolutionBase), ).toEqual(["unknown"]); diff --git a/src/domain/referenceSourceImportParsing.ts b/src/domain/referenceSourceImportParsing.ts index 0962e1fe11..c6864ede92 100644 --- a/src/domain/referenceSourceImportParsing.ts +++ b/src/domain/referenceSourceImportParsing.ts @@ -12,7 +12,7 @@ import { parserPluginsForPath, } from "./javascript/javascriptSourceParser.js"; import { - referenceSourceRequireCallBases, + referenceSourceRequireCalls, type ReferenceSourceRequireCall, } from "./referenceSourceRequireBindings.js"; import { isRelativeJavaScriptModuleSpecifier } from "./javascript/javascriptModuleSpecifier.js"; @@ -245,7 +245,7 @@ const extractRequireAndDynamicImports = ( from_path: string, relationships: ReferenceSourceImportRelationship[], limitations: string[], - requireCallBases: ReadonlyMap, + requireCalls: ReadonlyMap, ): void => { // Single traversal owner: iterative, VISITOR_KEYS-gated, no recursion over // loc/comment objects. Survives deeply nested generated member chains that @@ -274,7 +274,7 @@ const extractRequireAndDynamicImports = ( // The shared exact-literal reader owns argument exactness: it unwraps // runtime-transparent TS wrappers (`as const`, `!`, `satisfies`) and // admits quoted and expression-free template spellings alike. - const requireCall = requireCallBases.get(call); + const requireCall = requireCalls.get(call); if (requireCall !== undefined) { const result = moduleSpecifierFromExpression(requireCall.arguments?.[0]); if (result !== undefined) { @@ -411,7 +411,7 @@ export const parseReferenceSourceImports = ( path, relationships, limitations, - referenceSourceRequireCallBases(ast, path), + referenceSourceRequireCalls(ast, path), ); return { diff --git a/src/domain/referenceSourceRequireBindings.ts b/src/domain/referenceSourceRequireBindings.ts index f4efb6da86..4c9c15fd63 100644 --- a/src/domain/referenceSourceRequireBindings.ts +++ b/src/domain/referenceSourceRequireBindings.ts @@ -33,7 +33,7 @@ export interface ReferenceSourceRequireCall { } /** Identify exact calls through a source-owned Node loader and their resolver base. */ -export const referenceSourceRequireCallBases = ( +export const referenceSourceRequireCalls = ( file: Pick, path: string, ): ReadonlyMap => { diff --git a/tests/boundary/filesystem/referenceSourceComputedRequire.test.ts b/tests/boundary/filesystem/referenceSourceComputedRequire.test.ts index 3b4be9d53b..c8b19bcb65 100644 --- a/tests/boundary/filesystem/referenceSourceComputedRequire.test.ts +++ b/tests/boundary/filesystem/referenceSourceComputedRequire.test.ts @@ -9,6 +9,17 @@ import { expect, it } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { importReferenceSource } from "../../support/referenceSourceResourceScope.js"; +const hashedSourceEntry = (path: string, source: string) => ({ + path, + kind: "file", + sha256: createHash("sha256").update(source).digest("hex"), + size: Buffer.byteLength(source), + language: "JavaScript", + classifications: ["source"], + content_state: "hashed", + limitations: [], +}); + it("distinguishes computed require member values from literal members in source evidence", async () => { const root = await createTestTempDirectory("rea-reference-require-members-"); const literalCallees = [ @@ -107,6 +118,13 @@ it("distinguishes computed require member values from literal members in source const graph = result.value; expect(graph.parse_failures).toEqual([]); expect(graph.relationships).toEqual([ + { + from_path: "ignored.cjs", + to: "./dep.js", + kind: "requires", + resolution: "unknown", + parse_state: "partial", + }, { from_path: "literal.cjs", to: "dep.js", @@ -114,13 +132,26 @@ it("distinguishes computed require member values from literal members in source resolution: "internal", parse_state: "parsed", }, - ...literalCallees.map((_, index) => ({ - from_path: `member-${index}.cjs`, - to: "dep.js", + ...literalCallees.flatMap((callee, index) => + callee === "require.main" || callee === 'require["main"]' + ? [] + : [ + { + from_path: `member-${index}.cjs`, + to: "dep.js", + kind: "requires", + resolution: "internal", + parse_state: "parsed", + }, + ], + ), + { + from_path: "unknown.cjs", + to: "./dep.js", kind: "requires", - resolution: "internal", - parse_state: "parsed", - })), + resolution: "unknown", + parse_state: "partial", + }, { from_path: "unknown.cjs", to: "", @@ -137,16 +168,6 @@ it("distinguishes computed require member values from literal members in source }, ]); expect(graph.entries).toHaveLength(Object.keys(sources).length); - for (const [path, source] of Object.entries(sources)) { - expect(graph.entries).toContainEqual({ - path, - kind: "file", - sha256: createHash("sha256").update(source).digest("hex"), - size: Buffer.byteLength(source), - language: "JavaScript", - classifications: ["source"], - content_state: "hashed", - limitations: [], - }); - } + for (const [path, source] of Object.entries(sources)) + expect(graph.entries).toContainEqual(hashedSourceEntry(path, source)); }); diff --git a/tests/boundary/filesystem/referenceSourceShadowedRequire.test.ts b/tests/boundary/filesystem/referenceSourceShadowedRequire.test.ts index 9975c1c679..8cdcc954de 100644 --- a/tests/boundary/filesystem/referenceSourceShadowedRequire.test.ts +++ b/tests/boundary/filesystem/referenceSourceShadowedRequire.test.ts @@ -332,7 +332,10 @@ it("does not attribute overwritten require members to Node's resolver", async () if (!result.ok) throw result.error; expect( result.value.relationships - .filter(({ kind }) => kind === "requires") + .filter( + ({ kind, resolution }) => + kind === "requires" && resolution === "internal", + ) .map(({ from_path, to }) => [from_path, to]) .sort((left, right) => (left[0] ?? "").localeCompare(right[0] ?? "")), ).toEqual([ @@ -341,4 +344,17 @@ it("does not attribute overwritten require members to Node's resolver", async () ["local-loader.cjs", "loaded.cjs"], ["optional-call.cjs", "loaded.cjs"], ]); + expect( + result.value.relationships.filter( + ({ kind, resolution }) => kind === "requires" && resolution === "unknown", + ), + ).toEqual( + ["assigned.cjs", "computed.cjs", "defined.cjs"].map((from_path) => ({ + from_path, + to: "./loaded.cjs", + kind: "requires", + resolution: "unknown", + parse_state: "partial", + })), + ); });