From 36f528efcb9133016fcae55df3bfc7b54f917590 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 07:28:09 +0800 Subject: [PATCH] fix(javascript): share immediately returned loader facts Reuse the existing direct-call returned-reference owner for immediate synchronous function results and feed proven loader facts into historical imports. Simple returned require retains its identity; mixed, opaque, and canonical shim alternatives retain explicit uncertainty rather than disappearing. Focused semantic, Electron, and public CLI regressions passed, including CommonJS, ESM, async/generator, mutable argument, direct-loader, and source-provenance controls. No runtime target or Proxy evaluator was introduced. Fixes #3535 --- docs/javascript-application-workflows.md | 9 ++ .../javascriptSemanticRequireOrigin.ts | 72 ++++++++-- ...criptStaticAnalysisReturnedLoaders.test.ts | 125 ++++++++++++++++++ .../referenceSourceReturnedLoaders.test.ts | 71 ++++++++++ 4 files changed, 265 insertions(+), 12 deletions(-) create mode 100644 src/domain/javascript/javascriptStaticAnalysisReturnedLoaders.test.ts create mode 100644 tests/boundary/cli/referenceSourceReturnedLoaders.test.ts diff --git a/docs/javascript-application-workflows.md b/docs/javascript-application-workflows.md index 776ac8bc61..44645da9ee 100644 --- a/docs/javascript-application-workflows.md +++ b/docs/javascript-application-workflows.md @@ -501,6 +501,15 @@ Identifier references and closure captures share Babel syntax-role facts plus explicit binding/write roles; noncomputed declaration keys and import/meta names are not lexical reads. Provenance-only expression-depth omissions enter the same typed source coverage as value-evaluation bounds and remain visible in retained application Evidence. +Immediate synchronous function results also share the loader reference owner. +An expression arrow returning the retained CommonJS `require` can preserve that +loader identity. Possible loader results with opaque alternatives or block +bodies retain candidate uncertainty; async and generator results are their +wrapper values. +This does not interpret `typeof` or Proxy behavior in bundler shims. Historical +source imports consume the same proven loader fact and keep their existing +requirement for a known loader association. + Direct generator and async calls produce iterators and Promises; body returns do not become direct call-result flows. Their unresolved consumption remains an explicit frontier. Replaced emitter bindings select their diff --git a/src/domain/javascript/javascriptSemanticRequireOrigin.ts b/src/domain/javascript/javascriptSemanticRequireOrigin.ts index 0d7ffd3dba..b75b1fa6a6 100644 --- a/src/domain/javascript/javascriptSemanticRequireOrigin.ts +++ b/src/domain/javascript/javascriptSemanticRequireOrigin.ts @@ -140,7 +140,15 @@ export const semanticRequireLoaderAt = ( ) ? callee.object : callee; - return requireLoaderValueAt(loader, node, state, factoryBindingIds); + const selected = selectedRequireSourcesAt( + loader, + node, + state, + factoryBindingIds, + )?.loader; + return selected !== undefined && selected !== "unknown" + ? selected + : requireLoaderValueAt(loader, node, state, factoryBindingIds); }; /** Classify Node's wrapper and exact factory bindings by their lexical owner. */ @@ -547,6 +555,38 @@ const selectedRequireSourcesAt = ( }, ); } else if (t.isCallExpression(node)) { + const callee = unwrapJavaScriptExpression(node.callee).node; + if ( + target === "loader" && + current.path.length === 0 && + t.isFunction(callee) + ) { + if (callee.async || callee.generator) { + // A direct async/generator invocation returns a wrapper, not its contents. + local = true; + continue; + } + const returned = returnedLoaderReferences(state, "direct-call")( + node.callee, + node, + ); + // The shared result owner is a may-reference graph. A block may also + // fall through or throw; only an expression arrow supplies its whole result. + unknown ||= + returned.unresolvedInvocation === true || + !t.isArrowFunctionExpression(callee) || + t.isBlockStatement(callee.body); + pending.push( + ...inheritLoaderSourceContext( + current, + returned.references.map((reference) => ({ + node: reference.node, + path: reference.projection, + })), + ), + ); + continue; + } if (target === "factory") { const origin = semanticRequireOrigin(node, state)?.origin; if ( @@ -874,6 +914,24 @@ const moduleReturnedReferences = new WeakMap< > >(); +/** Reuse one result-reference owner for loader values and module receivers. */ +const returnedLoaderReferences = ( + state: JavaScriptSemanticAnalysisState, + mode: "direct-call" | "awaited-call", +): ReturnType => { + let factories = moduleReturnedReferences.get(state); + if (factories === undefined) { + factories = new Map(); + moduleReturnedReferences.set(state, factories); + } + let returned = factories.get(mode); + if (returned === undefined) { + returned = createSemanticReturnedReferences(state, mode); + factories.set(mode, returned); + } + return returned; +}; + type ModuleObjectSource = { readonly node: t.Node; readonly path: SemanticAliasSelectionPath; @@ -1127,20 +1185,10 @@ const hasCommonJsModuleSource = ( t.isOptionalCallExpression(value) || t.isNewExpression(value) ) { - let factories = moduleReturnedReferences.get(state); - if (factories === undefined) { - factories = new Map(); - moduleReturnedReferences.set(state, factories); - } const mode = awaitDepths.includes(path.length) ? "awaited-call" : "direct-call"; - let returned = factories.get(mode); - if (returned === undefined) { - returned = createSemanticReturnedReferences(state, mode); - factories.set(mode, returned); - } - const returnedResult = returned( + const returnedResult = returnedLoaderReferences(state, mode)( value.callee, t.isNewExpression(value) ? undefined : value, ); diff --git a/src/domain/javascript/javascriptStaticAnalysisReturnedLoaders.test.ts b/src/domain/javascript/javascriptStaticAnalysisReturnedLoaders.test.ts new file mode 100644 index 0000000000..6d32929474 --- /dev/null +++ b/src/domain/javascript/javascriptStaticAnalysisReturnedLoaders.test.ts @@ -0,0 +1,125 @@ +import { expect, it } from "vitest"; + +import { parseReferenceSourceImports } from "../referenceSourceImportParsing.js"; + +import { analyzeParsedJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { parseRecoveredJavaScript } from "./javascriptSourceParser.js"; + +const analyze = (source: string, path = "main.cjs") => { + const { file } = parseRecoveredJavaScript(source, path, {}); + return analyzeParsedJavaScriptStaticSource(source, file); +}; + +it("retains the CommonJS loader returned by an immediate arrow", () => { + const result = analyze(`const load = (() => require)(); load('./dep.cjs');`); + expect(result.parse_error_count).toBe(0); + expect(result.references).toEqual([ + expect.objectContaining({ kind: "require", specifier: "./dep.cjs" }), + ]); +}); + +it("retains a candidate for the canonical esbuild require shim", () => { + const result = analyze(` + const load = (fallback => typeof require !== 'undefined' ? require : + typeof Proxy !== 'undefined' ? new Proxy(fallback, { + get: (target, key) => (typeof require !== 'undefined' ? require : target)[key] + }) : fallback)(function (name) { + if (typeof require !== 'undefined') return require.apply(this, arguments); + throw Error('Dynamic require of "' + name + '" is not supported'); + }); + load('./dep.cjs'); + `); + expect(result.parse_error_count).toBe(0); + expect(result.references).toContainEqual( + expect.objectContaining({ + kind: "require", + specifier: null, + expression: "load('./dep.cjs')", + }), + ); + expect(result.references).not.toContainEqual( + expect.objectContaining({ specifier: "./dep.cjs" }), + ); +}); + +it.each([ + "(() => flag ? require : replacement)()", + "(() => flag ? require : () => ({}))()", + "(function () { if (flag) return require; })()", +])( + "does not promote mixed or incomplete returned loader facts: %s", + (value) => { + const result = analyze(`const load = ${value}; load('./dep.cjs');`); + expect(result.parse_error_count).toBe(0); + expect(result.references).toEqual([ + expect.objectContaining({ kind: "require", specifier: null }), + ]); + }, +); + +it("keeps an unbound module require returned by an arrow uncertain", () => { + const result = analyze( + `const load = (() => require)(); load('./dep.cjs');`, + "main.mjs", + ); + expect(result.parse_error_count).toBe(0); + expect(result.references).toEqual([ + expect.objectContaining({ kind: "require", specifier: null }), + ]); +}); + +it.each([ + "(() => () => ({}))()", + "(() => replacement)()", + "(async () => require)()", + "(function* () { return require; })()", + "(() => { const require = () => ({}); return require; })()", +])( + "does not invent a native loader for local or eventual values: %s", + (value) => { + const result = analyze(`const load = ${value}; load('./dep.cjs');`); + expect(result.parse_error_count).toBe(0); + expect(result.references).toEqual([]); + }, +); + +it("preserves direct CommonJS loads beside immediate local calls", () => { + const result = analyze( + `const local = (() => () => ({}))(); local('./unused.cjs'); require('./dep.cjs');`, + ); + expect(result.parse_error_count).toBe(0); + expect(result.references).toEqual([ + expect.objectContaining({ kind: "require", specifier: "./dep.cjs" }), + ]); +}); + +it("shares a proven immediate loader result with historical source imports", () => { + const result = parseReferenceSourceImports( + "main.cjs", + new TextEncoder().encode( + `const load = (() => require)(); load('./dep.cjs');`, + ), + "JavaScript", + ); + expect(result.parse_failures).toEqual([]); + expect(result.relationships).toEqual([ + { + from_path: "main.cjs", + to: "./dep.cjs", + kind: "requires", + resolution: "internal", + parse_state: "parsed", + resolutionStrategy: "runtime-require", + }, + ]); +}); + +it("does not prove a loader before an invocation argument replaces require", () => { + const result = analyze( + `const load = (() => require)(require = () => ({})); load('./dep.cjs');`, + ); + expect(result.parse_error_count).toBe(0); + expect(result.references).not.toContainEqual( + expect.objectContaining({ specifier: "./dep.cjs" }), + ); +}); diff --git a/tests/boundary/cli/referenceSourceReturnedLoaders.test.ts b/tests/boundary/cli/referenceSourceReturnedLoaders.test.ts new file mode 100644 index 0000000000..3e84c612fb --- /dev/null +++ b/tests/boundary/cli/referenceSourceReturnedLoaders.test.ts @@ -0,0 +1,71 @@ +import { createHash } from "node:crypto"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +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( + "imports immediate CommonJS loader results without promoting local or module values", + async ({ cli }) => { + const root = await createTestTempDirectory( + "rea-reference-returned-loaders-", + ); + const config = await createTestTempDirectory( + "rea-reference-returned-loader-config-", + ); + const source = `const load = (() => require)(); load('./dep.cjs');\n`; + const sources = { + "returned.cjs": source, + "direct.cjs": `require('./dep.cjs');\n`, + "local.cjs": `const load = (() => () => ({}))(); load('./dep.cjs');\n`, + "module.mjs": source, + "mixed.cjs": `const load = (() => flag ? require : replacement)(); load('./dep.cjs');\n`, + "dep.cjs": `throw new Error('dependency must not execute');\n`, + }; + await Promise.all( + Object.entries(sources).map(([path, text]) => + writeFile(join(root, path), text), + ), + ); + const response = await cli.run({ + arguments: ["import-reference-source", root, "--json"], + environment: { + HOME: config, + XDG_CONFIG_HOME: config, + XDG_CACHE_HOME: config, + }, + }); + expect(response.exitCode, response.stderr).toBe(0); + const graph = historicalSourceGraphSchema.parse(response.json); + expect(graph.parse_failures).toEqual([]); + expect( + graph.relationships.filter(({ kind }) => kind === "requires"), + ).toEqual([ + { + from_path: "direct.cjs", + to: "dep.cjs", + kind: "requires", + resolution: "internal", + parse_state: "parsed", + }, + { + from_path: "returned.cjs", + to: "dep.cjs", + kind: "requires", + resolution: "internal", + parse_state: "parsed", + }, + ]); + expect(graph.entries).toContainEqual( + expect.objectContaining({ + path: "returned.cjs", + content_state: "hashed", + sha256: createHash("sha256").update(source).digest("hex"), + }), + ); + }, +);