fix(javascript): preserve repeated native resolver calls (#7824)

This commit is contained in:
morluto authored and GitHub committed 2026-10-29 01:29:48 +08:00
1 parent 477f63ae21
commit aba158506b
3 files changed
+97 -1

No files matched your search

@@ -30,6 +30,7 @@ import {
semanticPropertyPathKeyMatches,
} from "./javascriptSemanticPropertyPaths.js";
import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js";
import { semanticRequireLoaderAt } from "./javascriptSemanticRequireOrigin.js";
import {
semanticCopiedReferencePath,
semanticReferenceIterationUnknown,
@@ -772,7 +773,45 @@ export function* indexJavaScriptAmbientWritesSteps(
retainSemanticAmbientEffect(state, "write", resolved, node);
}
}
for (const { node, target, browserReceiver, projection } of escapes) {
// Settle other ambient effects first. A syntactic resolver call is safe only
// if no write or opaque escape can replace the native member anywhere in
// this analysis; otherwise retain its receiver escape conservatively.
const resolverReceivers: typeof escapes = [];
for (const effect of escapes) {
const { node, target } = effect;
const callee =
t.isCallExpression(node) || t.isOptionalCallExpression(node)
? unwrapJavaScriptExpression(node.callee).node
: undefined;
if (
(t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
callee.object === target &&
t.isIdentifier(target, { name: "require" }) &&
isUnshadowedGlobal(target, state, "require") &&
semanticStaticPropertyKey(callee.property, callee.computed) === "resolve"
) {
resolverReceivers.push(effect);
continue;
}
retainEscapeEffect(effect);
}
for (const effect of resolverReceivers) {
const { node } = effect;
if (
(t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
!state.ambientWrites.covers(["global", "require", "resolve"]) &&
!state.ambientEscapes.covers(["global", "require", "resolve"]) &&
semanticRequireLoaderAt(node, state, new Set()) === "commonjs-loader"
)
continue;
retainEscapeEffect(effect);
}
function retainEscapeEffect({
node,
target,
browserReceiver,
projection,
}: (typeof escapes)[number]): void {
const resolved = ambientWrites(target, node, true, projection);
if (
browserReceiver &&
@@ -70,6 +70,30 @@ it.each([
);
const imported = "import {createRequire as make} from 'node:module';";
it("retains consecutive native resolver calls while keeping replaced or escaped loaders uncertain", () => {
const references = (source: string) =>
analyzeJavaScriptStaticSource(source).references.map(
({ operation, specifier }) => ({ operation, specifier }),
);
expect(
references('require.resolve("./first.js");require.resolve("./second.js");'),
).toEqual([
{ operation: "resolve", specifier: "./first.js" },
{ operation: "resolve", specifier: "./second.js" },
]);
for (const prefix of [
"require.resolve=custom;",
"opaque(require);",
"const box={r:require};box.r.resolve=custom;",
'Object.defineProperty(require,"resolve",{value:custom});',
])
expect(
references(`${prefix}require.resolve("./second.js");`).at(-1),
).toEqual({
operation: "load",
specifier: null,
});
});
it.each([
[
"proven factory",
@@ -0,0 +1,33 @@
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
cliTest.for(["CLI", "stdio MCP"] as const)(
"retains consecutive native resolver relationships through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-repeat-resolve-");
await writeFixtureFiles(root, {
"main.cjs":
'require.resolve("./first.js");require.resolve("./second.js");',
"first.js": "module.exports=1;",
"second.js": "module.exports=2;",
});
const result = await analyzeJavaScriptDirectory(surface, cli, root);
const edges = result.graph.edges.filter(
(edge) => edge.properties.operation === "resolve",
);
expect(
edges.map((edge) => [
edge.properties.specifier,
edge.properties.resolved_path,
]),
).toEqual(
expect.arrayContaining([
["./first.js", "first.js"],
["./second.js", "second.js"],
]),
);
},
);