fix(javascript): deduplicate shared projected source facts

This commit is contained in:
morluto
2026-10-24 08:42:58 +08:00
parent d81d6b6707
commit 446333cb7f
3 changed files with 98 additions and 9 deletions
@@ -363,6 +363,27 @@ const projectedSourceFactsAtRead = (
)
return undefined;
const sources: ProjectedSource[] = [];
const sourceKeys = new WeakMap<t.Node, Set<string>>();
const captureIds = new WeakMap<t.Node, number>();
let nextCaptureId = 0;
const appendSource = (source: ProjectedSource): void => {
const captures = semanticNormalizeCapturedSelections(
source.capturedSelections,
)?.map(({ at, depth, ownEnumerable }) => {
let id = captureIds.get(at);
if (id === undefined) {
id = nextCaptureId++;
captureIds.set(at, id);
}
return [id, depth, ownEnumerable];
});
const key = JSON.stringify([source.path, captures]);
const keys = sourceKeys.get(source.node) ?? new Set<string>();
if (keys.has(key)) return;
keys.add(key);
sourceKeys.set(source.node, keys);
sources.push(source);
};
for (const effect of writes) {
if (!retainEligibility && superseded(effect)) continue;
if (
@@ -389,7 +410,7 @@ const projectedSourceFactsAtRead = (
choice === undefined ? [] : [choice],
);
if (selection !== null)
sources.push({
appendSource({
node: source.node,
path: [...selection.projection, ...suffix],
...(selection.capturedSelectionDepth === undefined
@@ -415,7 +436,7 @@ const projectedSourceFactsAtRead = (
for (const initializer of read.binding.initializers) {
if (semanticReferenceIterationUnknown(initializer, state)) return undefined;
if (semanticReferenceSourcesInactive(initializer, state)) continue;
sources.push({
appendSource({
node: initializer.node,
path: [...initializer.projection, ...path],
});
@@ -433,7 +454,7 @@ const projectedSourceFactsAtRead = (
"identity",
);
if (projection !== null)
sources.push({
appendSource({
node: initializer.node,
path: [...projection, ...suffix],
capturedSelections:
@@ -1,3 +1,4 @@
import { createContext, runInContext, runInNewContext } from "node:vm";
import { expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
@@ -120,15 +121,34 @@ it.each(
.replaceAll("SLOT", value.slot)
.replaceAll("READ", value.read),
expression: shape.read.replaceAll("READ", value.read),
// The public module/delegate owner retains unknown after the copy's child
// escapes into the new parent. Factory result selection can exclude it.
expected: shared ? !value.host || value.slot !== "make" : value.host,
// The new local parent shares the same descendant. Its own slot write
// replaces the host value; an independent descendant keeps the old one.
expected: shared ? !value.host : value.host,
localCall: value.slot === "load",
})),
),
),
)(
"distinguishes shared and newly allocated descendant receivers: $source",
({ source, expression, expected }) => {
({ source, expression, expected, localCall }) => {
const hostCalls: string[] = [];
const module = {
require: (specifier: string) => {
hostCalls.push(specifier);
return {};
},
};
const context = createContext({ module });
expect(source.endsWith(expression + ";")).toBe(true);
runInContext(source.slice(0, -expression.length - 1), context);
if (expected || localCall) {
runInContext(expression, context);
} else {
expect(() => runInContext(expression, context)).toThrow(
expect.objectContaining({ name: "TypeError" }),
);
}
expect(hostCalls).toEqual(expected ? ["./shared.cjs"] : []);
const result = analyzeJavaScriptStaticSource(source);
expect(result.parse_error_count).toBe(0);
expect(
@@ -143,6 +163,54 @@ it.each(
},
);
it.each([
{
kind: "external call",
iterator: false,
source:
"const child={loader:module};const box={child};external(child);box.child.loader.require('./uncertain.cjs');",
},
{
kind: "custom iterator",
iterator: true,
source:
"const child={loader:module};const box={child};const values=[child];values[Symbol.iterator]=external;for(const selected of values)selected.loader={};box.child.loader.require('./uncertain.cjs');",
},
])(
"retains host eligibility across an opaque shared receiver: $kind",
({ source, iterator }) => {
const analysis = analyzeJavaScriptStaticSource(source);
expect(analysis.references).toContainEqual(
expect.objectContaining({ kind: "require", specifier: null }),
);
// Both executions fit the same unknown input: one retains the host loader,
// the other replaces it. Neither outcome can prove the static alternative absent.
for (const preserveHost of [true, false]) {
const calls: string[] = [];
const module = {
require: (specifier: string) => {
calls.push(specifier);
return {};
},
};
const external = iterator
? function* (this: readonly { loader: unknown }[]) {
yield preserveHost ? { loader: {} } : this[0]!;
}
: (child: { loader: unknown }) => {
if (!preserveHost) child.loader = {};
};
try {
runInNewContext(source, { module, external });
} catch (error) {
expect(error).toMatchObject({ name: "TypeError" });
expect(preserveHost).toBe(false);
}
expect(calls).toEqual(preserveHost ? ["./uncertain.cjs"] : []);
}
},
);
it.each(["module", "{}"])(
"selects the later write to a shared projected receiver: %s",
(next) => {
@@ -53,7 +53,7 @@ it("preserves earlier and sibling effects without inventing replaced cached-modu
require('node:fs').readFileSync('wrong-direct-loader.json');`,
);
await writeFile(
join(root, "imports.ts"),
join(root, "imports.cts"),
`import fs = require('node:fs'); const other = require('fs');
const read = other.readFileSync; observe(read); read('escaped.json');
fs.readFileSync = custom; other.readFileSync('wrong-import-equals.json');`,
@@ -65,7 +65,7 @@ it("preserves earlier and sibling effects without inventing replaced cached-modu
require('fs').readFileSync('wrong-late-loader.json');`,
);
await writeFile(
join(root, "replacement.ts"),
join(root, "replacement.cts"),
`import fs = require('fs'); const other=require('fs'); fs={}; fs.readFileSync=custom; other.readFileSync('replacement-kept.json');`,
);
await writeFile(