From 0f3a15de0dbaf85240b28b95c15ca4e071baf3b1 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:42:32 +0800 Subject: [PATCH] fix(javascript): preserve factory export helper identity --- docs/javascript-artifact-reconstruction.md | 20 ++-- .../javascript/javascriptAstFingerprint.ts | 37 +++++-- .../javascriptMemberMutationCoverage.test.ts | 102 +++++++++++++++++- .../javascript/javascriptSemanticAnalysis.ts | 19 +++- ...ticAnalysis.typed_array_references.test.ts | 20 +++- .../javascriptSemanticMemberMutations.ts | 6 +- .../javascriptSemanticMutationInitializers.ts | 19 ++++ .../javascript/javascriptSemanticState.ts | 6 ++ .../javascript/javascriptStaticAnalysis.ts | 18 +++- .../javascriptStaticAnalysisBundler.ts | 23 +++- .../javascriptStaticAnalysisState.ts | 9 ++ .../webpackRenamedExportsCli.test.ts | 27 +++++ 12 files changed, 277 insertions(+), 29 deletions(-) diff --git a/docs/javascript-artifact-reconstruction.md b/docs/javascript-artifact-reconstruction.md index 413dd696e6..ebbc665981 100644 --- a/docs/javascript-artifact-reconstruction.md +++ b/docs/javascript-artifact-reconstruction.md @@ -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. diff --git a/src/domain/javascript/javascriptAstFingerprint.ts b/src/domain/javascript/javascriptAstFingerprint.ts index 9efcc803a7..ecf4b7405a 100644 --- a/src/domain/javascript/javascriptAstFingerprint.ts +++ b/src/domain/javascript/javascriptAstFingerprint.ts @@ -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(); 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; }, diff --git a/src/domain/javascript/javascriptMemberMutationCoverage.test.ts b/src/domain/javascript/javascriptMemberMutationCoverage.test.ts index 2311416559..0f695da9ca 100644 --- a/src/domain/javascript/javascriptMemberMutationCoverage.test.ts +++ b/src/domain/javascript/javascriptMemberMutationCoverage.test.ts @@ -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" }), + }), + ]); +}); diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts index 51c6b3fe78..1fbce52664 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.ts @@ -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 => { 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( diff --git a/src/domain/javascript/javascriptSemanticAnalysis.typed_array_references.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.typed_array_references.test.ts index c0e7849476..6f6455db46 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.typed_array_references.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.typed_array_references.test.ts @@ -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); diff --git a/src/domain/javascript/javascriptSemanticMemberMutations.ts b/src/domain/javascript/javascriptSemanticMemberMutations.ts index 341e800a0f..531da49942 100644 --- a/src/domain/javascript/javascriptSemanticMemberMutations.ts +++ b/src/domain/javascript/javascriptSemanticMemberMutations.ts @@ -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 diff --git a/src/domain/javascript/javascriptSemanticMutationInitializers.ts b/src/domain/javascript/javascriptSemanticMutationInitializers.ts index 6c6768a14b..d4f5c16dc9 100644 --- a/src/domain/javascript/javascriptSemanticMutationInitializers.ts +++ b/src/domain/javascript/javascriptSemanticMutationInitializers.ts @@ -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, diff --git a/src/domain/javascript/javascriptSemanticState.ts b/src/domain/javascript/javascriptSemanticState.ts index 1541b9bc46..b9eaf40491 100644 --- a/src/domain/javascript/javascriptSemanticState.ts +++ b/src/domain/javascript/javascriptSemanticState.ts @@ -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, diff --git a/src/domain/javascript/javascriptStaticAnalysis.ts b/src/domain/javascript/javascriptStaticAnalysis.ts index 16c72077a9..5fbf0ac127 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.ts @@ -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); diff --git a/src/domain/javascript/javascriptStaticAnalysisBundler.ts b/src/domain/javascript/javascriptStaticAnalysisBundler.ts index 688f3eefd3..b0b1bf37ea 100644 --- a/src/domain/javascript/javascriptStaticAnalysisBundler.ts +++ b/src/domain/javascript/javascriptStaticAnalysisBundler.ts @@ -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); + } +}; diff --git a/src/domain/javascript/javascriptStaticAnalysisState.ts b/src/domain/javascript/javascriptStaticAnalysisState.ts index eda2728580..505258501a 100644 --- a/src/domain/javascript/javascriptStaticAnalysisState.ts +++ b/src/domain/javascript/javascriptStaticAnalysisState.ts @@ -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 { /** 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; readonly seen: Set; + 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, }); diff --git a/tests/acceptance/applications/webpackRenamedExportsCli.test.ts b/tests/acceptance/applications/webpackRenamedExportsCli.test.ts index 6b5693ea99..55ae5087b7 100644 --- a/tests/acceptance/applications/webpackRenamedExportsCli.test.ts +++ b/tests/acceptance/applications/webpackRenamedExportsCli.test.ts @@ -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");