refactor(javascript): name retained require call facts explicitly

This commit is contained in:
morluto
2026-10-23 21:34:45 +08:00
parent 1a430f055d
commit e966b9f9a5
6 changed files with 77 additions and 36 deletions
+8 -3
View File
@@ -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.
@@ -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"]);
+4 -4
View File
@@ -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<CallExpression, ReferenceSourceRequireCall>,
requireCalls: ReadonlyMap<CallExpression, ReferenceSourceRequireCall>,
): 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 {
+1 -1
View File
@@ -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<ParsedJavaScriptSource, "program">,
path: string,
): ReadonlyMap<t.CallExpression, ReferenceSourceRequireCall> => {
@@ -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: "<dynamic-import>",
@@ -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));
});
@@ -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",
})),
);
});