mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 01:45:32 +00:00
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
This commit is contained in:
1 parent
9d7779d365
commit
36f528efcb
4 files changed
+265
-12
No files matched your search
@@ -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
|
||||
|
||||
@@ -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<typeof createSemanticReturnedReferences> => {
|
||||
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,
|
||||
);
|
||||
|
||||
@@ -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" }),
|
||||
);
|
||||
});
|
||||
@@ -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"),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user