mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(javascript): preserve factory export helper identity
This commit is contained in:
@@ -399,13 +399,19 @@ Bundler adapters own callback roles: Webpack supplies module, exports, then
|
||||
loader; esbuild CommonJS supplies exports, then module. Export extraction uses
|
||||
those binding identities for assignments and declarations, so renaming either
|
||||
parameter does not change the recovered names. Global property-definition
|
||||
intrinsics require lexical and visible-write proof. Computed writes through a
|
||||
standalone loader's exposed module/cache table use the shared value owner; an
|
||||
unknown key invalidates the original-slot proof rather than implying a fresh
|
||||
chunk installation. A function stored on a standalone loader member does not
|
||||
write the table merely by being defined: the bootstrap owner excludes its
|
||||
deferred writes only when that member is unread and the loader namespace stays
|
||||
confined. Calls, member aliases, dynamic member reads, namespace escapes, or
|
||||
intrinsics require lexical and visible-write proof. Factory export declarations
|
||||
also require the loader's `d` member to have no prior or unordered visible
|
||||
replacement, including writes through lexical aliases. Unrelated loader members
|
||||
do not invalidate declarations. Ordering describes declarations within one factory
|
||||
activation; helper integrity on entry, effects of earlier invocations, and
|
||||
arbitrary call side effects remain unproven.
|
||||
Computed writes through a standalone loader's exposed module/cache table use
|
||||
the shared value owner; an unknown key invalidates the original-slot proof
|
||||
rather than implying a fresh chunk installation. A function stored on a
|
||||
standalone loader member does not write the table merely by being defined:
|
||||
the bootstrap owner excludes its deferred writes only when that member is
|
||||
unread and the loader namespace stays confined. Calls, member aliases, dynamic
|
||||
member reads, namespace escapes, or
|
||||
receiver-based access retain uncertainty about those writes. Export names
|
||||
describe recognized declarations, not a proof
|
||||
of final runtime property lifetime or arbitrary helper behavior.
|
||||
|
||||
@@ -12,7 +12,7 @@ import { semanticStaticPropertyKey } from "./javascriptAstValues.js";
|
||||
import { stringValue } from "./javascriptStaticAnalysisHelpers.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptBindingIndex,
|
||||
} from "./javascriptSemanticState.js";
|
||||
|
||||
/** Static CommonJS export names. */
|
||||
@@ -42,9 +42,17 @@ export interface JavaScriptFactoryExportBindings {
|
||||
/** Collect statically declared CommonJS/bundler export names from one factory. */
|
||||
export const collectJavaScriptExports = (
|
||||
node: t.Node,
|
||||
bindings?: JavaScriptSemanticAnalysisState,
|
||||
roles?: JavaScriptFactoryExportBindings,
|
||||
factory?: {
|
||||
readonly roles: JavaScriptFactoryExportBindings;
|
||||
readonly index: Pick<
|
||||
JavaScriptBindingIndex,
|
||||
"bindings" | "effectsForReferenceWithinActivationAt"
|
||||
>;
|
||||
},
|
||||
): StaticExports => {
|
||||
const roles = factory?.roles;
|
||||
const index = factory?.index;
|
||||
const bindings = index?.bindings;
|
||||
const exports = new Set<string>();
|
||||
const bindingId = (
|
||||
reference: t.Node | null | undefined,
|
||||
@@ -102,13 +110,22 @@ export const collectJavaScriptExports = (
|
||||
"Object",
|
||||
"defineProperty",
|
||||
]);
|
||||
collectCallExports(
|
||||
current,
|
||||
exports,
|
||||
receivers,
|
||||
intrinsic,
|
||||
isLoaderAlias,
|
||||
);
|
||||
collectCallExports(current, exports, receivers, intrinsic, (root) => {
|
||||
if (root === null || !isLoaderAlias(root)) return false;
|
||||
if (index === undefined) return true;
|
||||
const facts = index.effectsForReferenceWithinActivationAt(
|
||||
current,
|
||||
root,
|
||||
["d"],
|
||||
);
|
||||
return (
|
||||
facts.memberEffectsIndexed &&
|
||||
!facts.effects.some(
|
||||
(effect) =>
|
||||
effect.kind === "member-write" && effect.timing !== "after",
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect, it } from "vitest";
|
||||
import * as t from "@babel/types";
|
||||
import {
|
||||
analyzeJavaScriptSemantics,
|
||||
indexJavaScriptBindingsWithReceiversSteps,
|
||||
@@ -7,7 +8,10 @@ import { onlyCallable } from "./javascriptSemanticAnalysis.fixture.js";
|
||||
import { semanticResourceLimitReason } from "./javascriptSemanticResourceLimits.js";
|
||||
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
||||
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
|
||||
import { completeSemanticSteps } from "./javascriptSemanticTraversal.js";
|
||||
import {
|
||||
completeSemanticSteps,
|
||||
traverseJavaScriptAst,
|
||||
} from "./javascriptSemanticTraversal.js";
|
||||
|
||||
import {
|
||||
joinSemanticPropertyPaths,
|
||||
@@ -171,3 +175,99 @@ it("keeps a preceding global self-property replacement unresolved", () => {
|
||||
facts.references.filter(({ kind }) => kind === "worker"),
|
||||
).toMatchObject([{ specifier: null }]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"load",
|
||||
"inspect(load); load.d = replacement; inspect(load);",
|
||||
["after", "before"],
|
||||
],
|
||||
[
|
||||
"load",
|
||||
"inspect(load); function later() { load.d = replacement; }",
|
||||
["unknown"],
|
||||
],
|
||||
["load", "inspect(load); if (flag) { load.d = replacement; }", ["unknown"]],
|
||||
[
|
||||
"load",
|
||||
"inspect(load); while (flag) { load.d = replacement; }",
|
||||
["unknown"],
|
||||
],
|
||||
[
|
||||
"load",
|
||||
"while (flag) { inspect(load); load.d = replacement; }",
|
||||
["unknown"],
|
||||
],
|
||||
["load, value = (load.d = replacement)", "inspect(load);", ["unknown"]],
|
||||
] as const)(
|
||||
"preserves parameter member provenance and activation ordering: (%s) %s",
|
||||
(parameters, body, timing) => {
|
||||
const parsed = parseJavaScriptSource(
|
||||
`function factory(${parameters}) { ${body} }`,
|
||||
);
|
||||
if (parsed === null) throw new Error("Expected parsed source");
|
||||
const index = completeSemanticSteps(
|
||||
indexJavaScriptBindingsWithReceiversSteps(parsed.program),
|
||||
);
|
||||
completeSemanticSteps(index.indexMemberMutationFactsSteps());
|
||||
const sites: t.CallExpression[] = [];
|
||||
traverseJavaScriptAst(parsed.program, {
|
||||
enter: (node) => {
|
||||
if (
|
||||
t.isCallExpression(node) &&
|
||||
t.isIdentifier(node.callee, { name: "inspect" })
|
||||
)
|
||||
sites.push(node);
|
||||
},
|
||||
});
|
||||
const originalTiming: string[] = [];
|
||||
const activationTiming: string[] = [];
|
||||
for (const site of sites) {
|
||||
const reference = site.arguments[0];
|
||||
if (!t.isIdentifier(reference))
|
||||
throw new Error("Expected parameter reference");
|
||||
const binding = index.bindingForReferenceAt(reference);
|
||||
expect(binding?.memberWrites).toEqual([
|
||||
expect.objectContaining({
|
||||
path: ["d"],
|
||||
node: expect.objectContaining({ type: "AssignmentExpression" }),
|
||||
}),
|
||||
]);
|
||||
for (const [facts, output] of [
|
||||
[index.effectsForReferenceAt(site, reference, ["d"]), originalTiming],
|
||||
[
|
||||
index.effectsForReferenceWithinActivationAt(site, reference, ["d"]),
|
||||
activationTiming,
|
||||
],
|
||||
] as const) {
|
||||
const writes = facts.effects.filter(
|
||||
({ kind }) => kind === "member-write",
|
||||
);
|
||||
expect(writes).toHaveLength(1);
|
||||
output.push(writes[0]!.timing);
|
||||
}
|
||||
}
|
||||
expect(originalTiming).toEqual(timing.map(() => "unknown"));
|
||||
expect(activationTiming).toEqual(timing);
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves exact member writes through an externally supplied caught value", () => {
|
||||
const parsed = parseJavaScriptSource(`
|
||||
try { throw external; } catch (caught) { caught.d = replacement; }
|
||||
`);
|
||||
if (parsed === null) throw new Error("Expected parsed source");
|
||||
const index = completeSemanticSteps(
|
||||
indexJavaScriptBindingsWithReceiversSteps(parsed.program),
|
||||
);
|
||||
completeSemanticSteps(index.indexMemberMutationFactsSteps());
|
||||
const caught = [...index.bindings.bindingsById.values()].find(
|
||||
({ name }) => name === "caught",
|
||||
);
|
||||
expect(caught?.memberWrites).toEqual([
|
||||
expect.objectContaining({
|
||||
path: ["d"],
|
||||
node: expect.objectContaining({ type: "AssignmentExpression" }),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -67,7 +67,10 @@ import {
|
||||
} from "./javascriptSemanticRequireOrigin.js";
|
||||
import { semanticBundledRequireOrigin } from "./javascriptBundledRequireOrigin.js";
|
||||
import { collectSemanticMemberMutationsSteps } from "./javascriptSemanticMemberMutations.js";
|
||||
import { semanticEffectTiming } from "./javascriptSemanticMutationInitializers.js";
|
||||
import {
|
||||
semanticEffectTiming,
|
||||
semanticEffectTimingWithinActivation,
|
||||
} from "./javascriptSemanticMutationInitializers.js";
|
||||
import {
|
||||
createAmbientPathResolver,
|
||||
indexJavaScriptAmbientWritesSteps,
|
||||
@@ -229,6 +232,7 @@ const visitSemanticReferenceEffectsAtSite = (
|
||||
effect: JavaScriptSemanticReferenceEffectsAtSite["effects"][number],
|
||||
) => boolean,
|
||||
requestedPath?: JavaScriptSemanticPropertyPath,
|
||||
effectTiming: typeof semanticEffectTiming = semanticEffectTiming,
|
||||
): Omit<JavaScriptSemanticReferenceEffectsAtSite, "effects"> => {
|
||||
const { root, path: inferredPath } = semanticReferenceRoot(referenceNode);
|
||||
const memberEffectsIndexed = indexedMemberMutationStates.has(state);
|
||||
@@ -259,7 +263,7 @@ const visitSemanticReferenceEffectsAtSite = (
|
||||
return visit({
|
||||
kind,
|
||||
...(effectPath === undefined ? {} : { path: effectPath }),
|
||||
timing: semanticEffectTiming(site, node, state),
|
||||
timing: effectTiming(site, node, state),
|
||||
});
|
||||
};
|
||||
if (binding !== undefined) {
|
||||
@@ -328,6 +332,7 @@ const semanticReferenceEffectsAtSite = (
|
||||
site: t.Node,
|
||||
referenceNode: t.Node,
|
||||
requestedPath?: JavaScriptSemanticPropertyPath,
|
||||
effectTiming: typeof semanticEffectTiming = semanticEffectTiming,
|
||||
): JavaScriptSemanticReferenceEffectsAtSite => {
|
||||
const effects: JavaScriptSemanticReferenceEffectsAtSite["effects"][number][] =
|
||||
[];
|
||||
@@ -340,6 +345,7 @@ const semanticReferenceEffectsAtSite = (
|
||||
return true;
|
||||
},
|
||||
requestedPath,
|
||||
effectTiming,
|
||||
);
|
||||
return { ...metadata, effects };
|
||||
};
|
||||
@@ -554,6 +560,15 @@ export function* indexJavaScriptBindingsWithReceiversSteps(
|
||||
propertyPath,
|
||||
);
|
||||
},
|
||||
effectsForReferenceWithinActivationAt(site, referenceNode, propertyPath) {
|
||||
return semanticReferenceEffectsAtSite(
|
||||
state,
|
||||
site,
|
||||
referenceNode,
|
||||
propertyPath,
|
||||
semanticEffectTimingWithinActivation,
|
||||
);
|
||||
},
|
||||
referenceMayHaveChangedAt(site, referenceNode, propertyPath) {
|
||||
if (!indexedMemberMutationStates.has(state)) return true;
|
||||
return referenceEffectsMayHaveChanged(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import {
|
||||
analyzeJavaScriptSemantics,
|
||||
@@ -270,8 +271,9 @@ describe("typed-array reference dependencies", () => {
|
||||
});
|
||||
|
||||
it("does not retain the cross-product of scalar assignments and store sites", () => {
|
||||
const storeCount = 48;
|
||||
const stores = Array.from(
|
||||
{ length: 48 },
|
||||
{ length: storeCount },
|
||||
(_, i) =>
|
||||
`value = heap[index + ${i} >> 2]; heap[index + ${i + 1} >> 2] = value;`,
|
||||
).join("\n");
|
||||
@@ -279,9 +281,21 @@ describe("typed-array reference dependencies", () => {
|
||||
const heap = new Int32Array(buffer);
|
||||
function copy(index) { let value = 0; ${stores} }
|
||||
`);
|
||||
for (const name of ["heap", "value"])
|
||||
expect(
|
||||
bindings.find((binding) => binding.name === name)!.escapedPaths,
|
||||
).toEqual([]);
|
||||
// An unknown parameter need not be numeric: '+' may invoke conversion methods.
|
||||
const indexEscapes = bindings.find(
|
||||
(binding) => binding.name === "index",
|
||||
)!.escapedPaths;
|
||||
expect(indexEscapes.length).toBeGreaterThan(0);
|
||||
expect(indexEscapes.length).toBeLessThanOrEqual(2 * storeCount);
|
||||
expect(
|
||||
bindings.reduce((n, binding) => n + binding.escapedPaths.length, 0),
|
||||
).toBe(0);
|
||||
indexEscapes.every(({ node }) =>
|
||||
t.isBinaryExpression(node, { operator: "+" }),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
bindings.find((binding) => binding.name === "heap")!.memberWrites.length,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
@@ -2008,8 +2008,8 @@ const observableMutationPaths = (
|
||||
for (const binding of state.bindingsById.values()) {
|
||||
const observer = bindingNodes.get(binding.bindingId);
|
||||
if (observer === undefined) continue;
|
||||
// Imported values are external objects with observable member slots even
|
||||
// when their syntax has no local object-allocation node (for example a
|
||||
// Parameters, caught values and imports can have observable member slots
|
||||
// without a local object-allocation node (for example a
|
||||
// CommonJS namespace from `require("node:fs")`). Keep those writes at their
|
||||
// exact member paths instead of widening the first path to a wildcard.
|
||||
const hasParsedRequireOrigin = binding.initializers.some(
|
||||
@@ -2018,6 +2018,8 @@ const observableMutationPaths = (
|
||||
hasSemanticCreateRequireFactoryOrigin(node, state),
|
||||
);
|
||||
if (
|
||||
binding.kind === "parameter" ||
|
||||
binding.kind === "catch" ||
|
||||
binding.directOrigins.length > 0 ||
|
||||
hasParsedRequireOrigin ||
|
||||
binding.hostOrigin !== undefined
|
||||
|
||||
@@ -556,6 +556,25 @@ export const semanticDefiniteEffectPrecedesReference = (
|
||||
);
|
||||
};
|
||||
|
||||
/** Order straight-line source effects in one activation, preserving deferred/repeating unknowns. */
|
||||
export const semanticEffectTimingWithinActivation = (
|
||||
site: t.Node,
|
||||
effect: t.Node | undefined,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptSemanticEffectTiming => {
|
||||
if (effect !== undefined) {
|
||||
if (
|
||||
semanticDefiniteEffectPrecedesReference(site, effect, state.parentsByNode)
|
||||
)
|
||||
return "after";
|
||||
if (
|
||||
semanticDefiniteEffectPrecedesReference(effect, site, state.parentsByNode)
|
||||
)
|
||||
return "before";
|
||||
}
|
||||
return semanticEffectTiming(site, effect, state);
|
||||
};
|
||||
|
||||
const computeReferenceInitializerPosition = (
|
||||
node: t.Node,
|
||||
parents: WeakMap<t.Node, t.Node>,
|
||||
|
||||
@@ -207,6 +207,12 @@ export interface JavaScriptBindingIndex {
|
||||
referenceNode: t.Node,
|
||||
propertyPath?: JavaScriptSemanticPropertyPath,
|
||||
): JavaScriptSemanticReferenceEffectsAtSite;
|
||||
/** Order declarations within one activation; deferred and repeating effects stay unknown. */
|
||||
effectsForReferenceWithinActivationAt(
|
||||
site: t.Node,
|
||||
referenceNode: t.Node,
|
||||
propertyPath?: JavaScriptSemanticPropertyPath,
|
||||
): JavaScriptSemanticReferenceEffectsAtSite;
|
||||
/** Stop at the first relevant prior/uncertain effect without retaining a report. */
|
||||
referenceMayHaveChangedAt(
|
||||
site: t.Node,
|
||||
|
||||
@@ -17,7 +17,10 @@ import {
|
||||
import { isElectronRendererLoad } from "./electronStaticRenderer.js";
|
||||
import { finishElectronNativeBindingsSteps } from "./electronStaticAnalysisNative.js";
|
||||
import { inspectElectronStaticNode } from "./electronStaticAnalysis.js";
|
||||
import { hasPossibleCommonJsDelegateWrite } from "./javascriptSemanticRequireOrigin.js";
|
||||
import {
|
||||
hasPossibleCommonJsDelegateWrite,
|
||||
semanticFactoryLoaderMemberAt,
|
||||
} from "./javascriptSemanticRequireOrigin.js";
|
||||
import { semanticResourceLimitReason } from "./javascriptSemanticResourceLimits.js";
|
||||
import { createStaticPathIntrinsicQuery } from "./javascriptStaticPathIntrinsics.js";
|
||||
import { createSemanticExpressionShapeResolver } from "./javascriptSemanticExpressionShapes.js";
|
||||
@@ -41,6 +44,7 @@ import {
|
||||
indexVitePreloadDependencyTable,
|
||||
} from "./javascriptStaticAnalysisCalls.js";
|
||||
import {
|
||||
finishBundlerExports,
|
||||
inspectBundlerRegistration,
|
||||
inspectBundlerBootstrap,
|
||||
inspectEsbuildWrapper,
|
||||
@@ -125,6 +129,9 @@ export function* analyzeParsedJavaScriptStaticSourceSteps(
|
||||
);
|
||||
yield* traverseStaticSourceSteps(file, findings, bindings, routeScan);
|
||||
const routes = yield* routeScan.finishSteps();
|
||||
if (accumulator.bundlerExportHelpersObserved)
|
||||
yield* index.indexMemberMutationFactsSteps();
|
||||
finishBundlerExports(accumulator, index);
|
||||
yield* finishElectronNativeBindingsSteps(findings, index);
|
||||
yield* finishElectronBrowserWindowsSteps(findings, index);
|
||||
if (accumulator.pendingModuleRequireCalls.length > 0) {
|
||||
@@ -299,6 +306,15 @@ const inspectNode = (
|
||||
inspectBundlerBootstrap(source, node, accumulator, bindings);
|
||||
inspectBundlerRegistration(source, node, accumulator, bindings);
|
||||
inspectEsbuildWrapper(source, node, accumulator, bindings);
|
||||
if (
|
||||
semanticFactoryLoaderMemberAt(
|
||||
node,
|
||||
bindings,
|
||||
accumulator.factoryRequireBindingIds,
|
||||
"d",
|
||||
) === "factory-loader"
|
||||
)
|
||||
accumulator.bundlerExportHelpersObserved = true;
|
||||
inspectCall(source, node, findings, bindings);
|
||||
} else if (t.isOptionalCallExpression(node)) {
|
||||
inspectCall(source, node, findings, bindings);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { javascriptBootstrapAt } from "./javascriptBootstrap.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptBindingIndex,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { semanticFactoryLoaderMemberAt } from "./javascriptSemanticRequireOrigin.js";
|
||||
|
||||
@@ -262,7 +263,7 @@ const recoverBundlerModule = (
|
||||
const parameter = parameters[index];
|
||||
return t.isIdentifier(parameter) ? parameter : undefined;
|
||||
};
|
||||
const exportsValue = collectJavaScriptExports(factory, bindings, {
|
||||
const roles = {
|
||||
exports:
|
||||
runtime === "esbuild-commonjs"
|
||||
? identifierAt(0)
|
||||
@@ -276,7 +277,9 @@ const recoverBundlerModule = (
|
||||
? undefined
|
||||
: identifierAt(0),
|
||||
loader: esbuild ? undefined : identifierAt(2),
|
||||
});
|
||||
};
|
||||
const exports: string[] = [];
|
||||
accumulator.pendingBundlerExports.push({ factory, roles, exports });
|
||||
const requireName = esbuild ? null : factoryRequireName(factory);
|
||||
const requireParameter = esbuild ? undefined : parameters[2];
|
||||
const requireBindingId = t.isIdentifier(requireParameter)
|
||||
@@ -310,7 +313,7 @@ const recoverBundlerModule = (
|
||||
source_sha256: sha256Text(sourceSlice(source, factory)),
|
||||
structural_fingerprint_sha256: fingerprint,
|
||||
structural_fingerprint_algorithm: JAVASCRIPT_AST_FINGERPRINT_ALGORITHM,
|
||||
exports: exportsValue.values,
|
||||
exports,
|
||||
location: range(factory),
|
||||
},
|
||||
asyncChunkKeys: asyncChunks.values,
|
||||
@@ -448,3 +451,17 @@ const uniqueValues = (values: readonly string[], unknown = 0): StaticValues => {
|
||||
unknown,
|
||||
};
|
||||
};
|
||||
|
||||
/** Complete factory exports once the shared alias-aware write index is ready. */
|
||||
export const finishBundlerExports = (
|
||||
accumulator: AnalysisAccumulator,
|
||||
index: JavaScriptBindingIndex,
|
||||
): void => {
|
||||
for (const pending of accumulator.pendingBundlerExports) {
|
||||
const collected = collectJavaScriptExports(pending.factory, {
|
||||
roles: pending.roles,
|
||||
index,
|
||||
});
|
||||
for (const value of collected.values) pending.exports.push(value);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type * as t from "@babel/types";
|
||||
import type { JavaScriptFactoryExportBindings } from "./javascriptAstFingerprint.js";
|
||||
import type { JavaScriptOpenReceiverFact } from "./javascriptSemanticAnalysis.js";
|
||||
import type { JavaScriptRpcScan } from "./javascriptRpcEndpoints.js";
|
||||
import type { JavaScriptStaticPathIntrinsicQuery } from "./javascriptStaticAnalysisHelpers.js";
|
||||
@@ -41,6 +42,11 @@ export interface LocatedJavaScriptFinding<Value> {
|
||||
|
||||
/** Mutable collections used during one inert AST traversal. */
|
||||
export interface JavaScriptAnalysisAccumulator {
|
||||
readonly pendingBundlerExports: {
|
||||
readonly factory: t.Node;
|
||||
readonly roles: JavaScriptFactoryExportBindings;
|
||||
readonly exports: string[];
|
||||
}[];
|
||||
readonly pendingModuleRequireCalls: (
|
||||
| t.CallExpression
|
||||
| t.OptionalCallExpression
|
||||
@@ -85,6 +91,7 @@ export interface JavaScriptAnalysisAccumulator {
|
||||
/** Binding identities proven as third-parameter loaders of recovered factories. */
|
||||
readonly factoryRequireBindingIds: Set<string>;
|
||||
readonly seen: Set<string>;
|
||||
bundlerExportHelpersObserved: boolean;
|
||||
visitedNodes: number;
|
||||
unknownFindings: number;
|
||||
}
|
||||
@@ -145,6 +152,7 @@ export interface LocatedJavaScriptFindingInput<
|
||||
export const createJavaScriptAnalysisAccumulator = (
|
||||
sourceLength: number,
|
||||
): JavaScriptAnalysisAccumulator => ({
|
||||
pendingBundlerExports: [],
|
||||
pendingModuleRequireCalls: [],
|
||||
appPathChanges: [],
|
||||
references: [],
|
||||
@@ -166,6 +174,7 @@ export const createJavaScriptAnalysisAccumulator = (
|
||||
moduleRangeIndex: new JavaScriptModuleRangeIndex(sourceLength),
|
||||
factoryRequireBindingIds: new Set(),
|
||||
seen: new Set(),
|
||||
bundlerExportHelpersObserved: false,
|
||||
visitedNodes: 0,
|
||||
unknownFindings: 0,
|
||||
});
|
||||
|
||||
@@ -34,6 +34,21 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
r = {d() {}};
|
||||
r.d(e, { loaderGhost: () => 2 });
|
||||
e.afterLoaderReplacement = 3;
|
||||
},
|
||||
31(mod, api, load) {
|
||||
load.d(api, { beforeMethodReplacement: () => 1 });
|
||||
load.other = () => {};
|
||||
load.d(api, { afterUnrelatedWrite: () => 2 });
|
||||
load.d = () => {};
|
||||
load.d(api, { methodGhost: () => 3 });
|
||||
api.afterMethodReplacement = 4;
|
||||
},
|
||||
37(mod, api, load) {
|
||||
load.d(api, { beforeAliasedReplacement: () => 1 });
|
||||
const alias = load;
|
||||
alias.d = () => {};
|
||||
load.d(api, { aliasedMethodGhost: () => 2 });
|
||||
api.afterAliasedReplacement = 3;
|
||||
}
|
||||
}]);`,
|
||||
);
|
||||
@@ -51,6 +66,18 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
module_key: "23",
|
||||
exports: ["afterLoaderReplacement", "alpha"],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
module_key: "31",
|
||||
exports: [
|
||||
"afterMethodReplacement",
|
||||
"afterUnrelatedWrite",
|
||||
"beforeMethodReplacement",
|
||||
],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
module_key: "37",
|
||||
exports: ["afterAliasedReplacement", "beforeAliasedReplacement"],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(result.graph.coverage.status).toBe("complete");
|
||||
|
||||
Reference in New Issue
Block a user