fix(javascript): derive loader references from lexical binding ownership
Use shared lexical bindings and recovered bundler identities for require and chunk references. Preserve uncertain writes as unresolved evidence; remove name-only loader aliases and repeated definition scans. Reviewed-PR: #2989 Co-authored-by: Hussnain Ahmad <ha7886899@gmail.com>
This commit is contained in:
1 parent
f2f83934d2
commit
eef4a0bfb4
8 files changed
+509
-32
No files matched your search
@@ -78,6 +78,7 @@ interface BindPatternInput {
|
||||
readonly scope: JavaScriptSemanticScopeState;
|
||||
readonly state: JavaScriptSemanticAnalysisState;
|
||||
readonly mutable: boolean;
|
||||
readonly declarationKind?: "var" | "let" | "const" | "using" | "await using";
|
||||
readonly kind?: JavaScriptSemanticDefinition["kind"];
|
||||
readonly projection?: readonly (string | number | null)[];
|
||||
readonly referenceOnly?: boolean;
|
||||
@@ -101,6 +102,7 @@ interface AddBindingInput {
|
||||
readonly name: string;
|
||||
readonly kind: JavaScriptSemanticDefinition["kind"];
|
||||
readonly mutable: boolean;
|
||||
readonly declarationKind?: "var" | "let" | "const" | "using" | "await using";
|
||||
readonly definitionNode: t.Node;
|
||||
readonly initializer: t.Node | null;
|
||||
readonly directOrigin?: JavaScriptModuleOrigin;
|
||||
@@ -976,6 +978,9 @@ const bindInnerDeclaration = (
|
||||
parent !== null && t.isVariableDeclaration(parent)
|
||||
? parent.kind !== "const"
|
||||
: true,
|
||||
...(parent !== null && t.isVariableDeclaration(parent)
|
||||
? { declarationKind: parent.kind }
|
||||
: {}),
|
||||
});
|
||||
else if (t.isClassExpression(node) && t.isIdentifier(node.id))
|
||||
addBinding({
|
||||
@@ -1258,6 +1263,7 @@ const bindPattern = (input: BindPatternInput): void => {
|
||||
scope,
|
||||
state,
|
||||
mutable,
|
||||
declarationKind,
|
||||
kind = "variable",
|
||||
projection = [],
|
||||
referenceOnly = false,
|
||||
@@ -1300,6 +1306,7 @@ const bindPattern = (input: BindPatternInput): void => {
|
||||
name: pattern.name,
|
||||
kind,
|
||||
mutable,
|
||||
...(declarationKind === undefined ? {} : { declarationKind }),
|
||||
definitionNode: pattern,
|
||||
initializer,
|
||||
projection,
|
||||
@@ -1402,6 +1409,7 @@ const addBinding = (input: AddBindingInput): void => {
|
||||
name,
|
||||
kind,
|
||||
mutable,
|
||||
declarationKind,
|
||||
definitionNode,
|
||||
initializer,
|
||||
directOrigin,
|
||||
@@ -1416,6 +1424,8 @@ const addBinding = (input: AddBindingInput): void => {
|
||||
state.bindingsById.set(binding.bindingId, binding);
|
||||
}
|
||||
binding.mutable ||= mutable;
|
||||
if (declarationKind !== undefined)
|
||||
binding.declarationKinds.add(declarationKind);
|
||||
binding.definitions.push({ kind, location: range(definitionNode) });
|
||||
if (initializer !== null)
|
||||
binding.initializers.push({ node: initializer, projection });
|
||||
@@ -1443,6 +1453,7 @@ const createBinding = (
|
||||
name,
|
||||
kind,
|
||||
mutable,
|
||||
declarationKinds: new Set(),
|
||||
memberWrites: [],
|
||||
escapedPaths: [],
|
||||
definitions: [],
|
||||
|
||||
@@ -47,6 +47,7 @@ describe("parameter references copied into function body bindings", () => {
|
||||
scopeId: "body",
|
||||
name: "arg",
|
||||
kind: "variable",
|
||||
declarationKinds: new Set(),
|
||||
mutable: true,
|
||||
memberWrites: [],
|
||||
escapedPaths: [],
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
semanticResolutionBlocked,
|
||||
semanticStableInitializer,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import {
|
||||
@@ -22,6 +23,169 @@ export interface JavaScriptRequireOrigin {
|
||||
readonly call: t.CallExpression;
|
||||
}
|
||||
|
||||
export type JavaScriptRequireLoaderFact =
|
||||
| "commonjs-loader"
|
||||
| "factory-loader"
|
||||
| "create-require"
|
||||
| "local"
|
||||
| "unknown";
|
||||
|
||||
/** Classify a bare `require(...)` using the shared lexical binding owner. */
|
||||
export const semanticRequireLoaderAt = (
|
||||
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
factoryBindingIds: ReadonlySet<string>,
|
||||
): JavaScriptRequireLoaderFact => {
|
||||
if (!t.isIdentifier(node.callee, { name: "require" })) return "local";
|
||||
const binding = resolveSemanticBindingState(state, node.callee, "require");
|
||||
if (binding === undefined)
|
||||
return semanticResolutionBlocked(state, node.callee, "require")
|
||||
? "unknown"
|
||||
: "commonjs-loader";
|
||||
if (factoryBindingIds.has(binding.bindingId))
|
||||
return binding.initializers.length === 0 ? "factory-loader" : "unknown";
|
||||
if (isWrapperRequireInitializerCall(binding, node, state))
|
||||
return "commonjs-loader";
|
||||
const scope = state.scopesById.get(binding.scopeId);
|
||||
if (
|
||||
scope?.kind === "program" &&
|
||||
binding.declarationKinds.size > 0 &&
|
||||
[...binding.declarationKinds].every((kind) => kind === "var") &&
|
||||
binding.initializers.length === 0
|
||||
)
|
||||
return "commonjs-loader";
|
||||
if (isSemanticCreateRequireBinding(binding, state)) return "create-require";
|
||||
const initializer = semanticStableInitializer(binding, state)?.node;
|
||||
if (
|
||||
(binding.kind === "function" || binding.kind === "class") &&
|
||||
initializer !== undefined
|
||||
)
|
||||
return "local";
|
||||
if (
|
||||
binding.declarationKinds.size === 1 &&
|
||||
binding.declarationKinds.has("const") &&
|
||||
(t.isFunctionExpression(initializer) ||
|
||||
t.isArrowFunctionExpression(initializer))
|
||||
)
|
||||
return "local";
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
/** Classify a call through an exact recovered factory-loader binding. */
|
||||
export const semanticFactoryRequireLoaderAt = (
|
||||
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
bindingIds: ReadonlySet<string>,
|
||||
): JavaScriptRequireLoaderFact | undefined => {
|
||||
const callee = unwrapJavaScriptExpression(node.callee).node;
|
||||
if (t.isIdentifier(callee))
|
||||
return semanticFactoryLoaderMemberAt(node, state, bindingIds, null);
|
||||
if (
|
||||
(t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
|
||||
semanticStaticPropertyKey(callee.property, callee.computed) === "bind"
|
||||
)
|
||||
return semanticFactoryLoaderMemberAt(node, state, bindingIds, "bind");
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Classify an exact loader call by binding identity and expected member. */
|
||||
export const semanticFactoryLoaderMemberAt = (
|
||||
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
bindingIds: ReadonlySet<string>,
|
||||
member: string | null,
|
||||
): JavaScriptRequireLoaderFact | undefined => {
|
||||
const callee = unwrapJavaScriptExpression(node.callee).node;
|
||||
let loader: t.Identifier | undefined;
|
||||
if (member === null) {
|
||||
if (t.isIdentifier(callee)) loader = callee;
|
||||
} else if (
|
||||
(t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
|
||||
semanticStaticPropertyKey(callee.property, callee.computed) === member
|
||||
) {
|
||||
const root = unwrapJavaScriptExpression(callee.object).node;
|
||||
if (t.isIdentifier(root)) loader = root;
|
||||
}
|
||||
if (loader === undefined) return undefined;
|
||||
const binding = resolveSemanticBindingState(state, loader, loader.name);
|
||||
if (binding === undefined || !bindingIds.has(binding.bindingId))
|
||||
return undefined;
|
||||
return binding.initializers.length === 0 ? "factory-loader" : "unknown";
|
||||
};
|
||||
|
||||
const isSemanticCreateRequireBinding = (
|
||||
binding: NonNullable<ReturnType<typeof resolveSemanticBindingState>>,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
if (binding.initializers.length !== 1) return false;
|
||||
const initializer = semanticStableInitializer(binding, state)?.node;
|
||||
if (!t.isCallExpression(initializer)) return false;
|
||||
const callee = unwrapJavaScriptExpression(initializer.callee).node;
|
||||
if (t.isIdentifier(callee)) {
|
||||
const origins = resolveSemanticBindingState(
|
||||
state,
|
||||
callee,
|
||||
callee.name,
|
||||
)?.directOrigins;
|
||||
if (origins?.length !== 1) return false;
|
||||
const [origin] = origins;
|
||||
return (
|
||||
origin?.importedPath.at(-1) === "createRequire" &&
|
||||
["module", "node:module"].includes(origin.specifier)
|
||||
);
|
||||
}
|
||||
const chain = semanticMemberChain(callee);
|
||||
if (chain === undefined || chain.members.at(-1) !== "createRequire")
|
||||
return false;
|
||||
if (!t.isNode(chain.root)) return false;
|
||||
const root = unwrapJavaScriptExpression(chain.root).node;
|
||||
if (!t.isIdentifier(root)) return false;
|
||||
const origins = resolveSemanticBindingState(
|
||||
state,
|
||||
root,
|
||||
root.name,
|
||||
)?.directOrigins;
|
||||
if (origins?.length !== 1) return false;
|
||||
const [origin] = origins;
|
||||
return (
|
||||
origin?.importedPath.length === 0 &&
|
||||
["module", "node:module"].includes(origin.specifier)
|
||||
);
|
||||
};
|
||||
|
||||
/** The RHS call runs before its assignment replaces Node's wrapper loader. */
|
||||
const isWrapperRequireInitializerCall = (
|
||||
binding: NonNullable<ReturnType<typeof resolveSemanticBindingState>>,
|
||||
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
const scope = state.scopesById.get(binding.scopeId);
|
||||
if (
|
||||
scope?.kind !== "program" ||
|
||||
binding.kind !== "variable" ||
|
||||
binding.declarationKinds.size !== 1 ||
|
||||
!binding.declarationKinds.has("var") ||
|
||||
binding.initializers.length !== 1 ||
|
||||
binding.initializers[0]?.node !== node
|
||||
)
|
||||
return false;
|
||||
const assignment = state.parentsByNode.get(node);
|
||||
if (
|
||||
!t.isAssignmentExpression(assignment) ||
|
||||
assignment.right !== node ||
|
||||
state.conditionallySkippedExpressions.has(node)
|
||||
)
|
||||
return false;
|
||||
const statement = state.parentsByNode.get(assignment);
|
||||
if (!t.isExpressionStatement(statement)) return false;
|
||||
if (state.parentsByNode.get(statement) !== state.program) return false;
|
||||
const target = unwrapJavaScriptExpression(assignment.left).node;
|
||||
return (
|
||||
t.isIdentifier(target, { name: "require" }) &&
|
||||
resolveSemanticBindingState(state, target, target.name) === binding
|
||||
);
|
||||
};
|
||||
|
||||
// Ambient indexing finishes before these derived facts are consumed. Keep only
|
||||
// replacement sites, rather than rescanning unrelated global member writes.
|
||||
const globalProcessReplacements = new WeakMap<
|
||||
|
||||
@@ -46,6 +46,8 @@ export interface JavaScriptSemanticBindingState {
|
||||
readonly scopeId: string;
|
||||
readonly name: string;
|
||||
kind: JavaScriptSemanticDefinition["kind"];
|
||||
/** Source declaration form retained internally for CommonJS wrapper bindings. */
|
||||
declarationKinds: Set<"var" | "let" | "const" | "using" | "await using">;
|
||||
mutable: boolean;
|
||||
readonly memberWrites: JavaScriptSemanticMemberEffect[];
|
||||
readonly escapedPaths: JavaScriptSemanticMemberEffect[];
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
resolveSemanticBindingState,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { semanticFactoryLoaderMemberAt } from "./javascriptSemanticRequireOrigin.js";
|
||||
|
||||
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
||||
|
||||
@@ -46,7 +47,11 @@ export const inspectBundlerRegistration = (
|
||||
if (runtime === undefined || !t.isArrayExpression(entry)) return;
|
||||
const chunkIds = entry.elements[0];
|
||||
const table = entry.elements[1];
|
||||
const runtimeValue = runtimeMetadata(entry.elements[2]);
|
||||
const runtimeValue = runtimeMetadata(
|
||||
entry.elements[2],
|
||||
bindings,
|
||||
accumulator,
|
||||
);
|
||||
if (!t.isArrayExpression(chunkIds) || !t.isObjectExpression(table)) return;
|
||||
const recovered = recoverBundlerModules(source, table, accumulator, {
|
||||
runtime,
|
||||
@@ -253,7 +258,21 @@ const recoverBundlerModule = (
|
||||
const fingerprint = fingerprintJavaScriptAst(factory);
|
||||
const exportsValue = collectJavaScriptExports(factory, bindings);
|
||||
const requireName = factoryRequireName(factory);
|
||||
const asyncChunks = collectBundlerAsyncChunkKeys(factory, requireName);
|
||||
const requireParameter = runtimeFunctionParameters(factory)[2];
|
||||
const requireBindingId = t.isIdentifier(requireParameter)
|
||||
? (resolveSemanticBindingState(
|
||||
bindings,
|
||||
requireParameter,
|
||||
requireParameter.name,
|
||||
)?.bindingId ?? null)
|
||||
: null;
|
||||
if (requireBindingId !== null)
|
||||
accumulator.factoryRequireBindingIds.add(requireBindingId);
|
||||
const asyncChunks = collectBundlerAsyncChunkKeys(
|
||||
factory,
|
||||
requireBindingId,
|
||||
bindings,
|
||||
);
|
||||
if (typeof factory.start === "number" && typeof factory.end === "number") {
|
||||
const moduleRange = {
|
||||
start: factory.start,
|
||||
@@ -297,7 +316,11 @@ type BundlerFunction =
|
||||
| t.ArrowFunctionExpression
|
||||
| t.ObjectMethod;
|
||||
|
||||
const runtimeMetadata = (node: t.Node | null | undefined): RuntimeMetadata => {
|
||||
const runtimeMetadata = (
|
||||
node: t.Node | null | undefined,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
accumulator: AnalysisAccumulator,
|
||||
): RuntimeMetadata => {
|
||||
if (!isBundlerFunction(node))
|
||||
return {
|
||||
requireName: null,
|
||||
@@ -308,6 +331,12 @@ const runtimeMetadata = (node: t.Node | null | undefined): RuntimeMetadata => {
|
||||
};
|
||||
const parameter = runtimeFunctionParameters(node)[0];
|
||||
const requireName = t.isIdentifier(parameter) ? parameter.name : null;
|
||||
const requireBindingId = t.isIdentifier(parameter)
|
||||
? (resolveSemanticBindingState(bindings, parameter, parameter.name)
|
||||
?.bindingId ?? null)
|
||||
: null;
|
||||
if (requireBindingId !== null)
|
||||
accumulator.factoryRequireBindingIds.add(requireBindingId);
|
||||
if (requireName === null)
|
||||
return {
|
||||
requireName,
|
||||
@@ -316,8 +345,18 @@ const runtimeMetadata = (node: t.Node | null | undefined): RuntimeMetadata => {
|
||||
asyncChunkKeys: [],
|
||||
unknownAsyncChunkKeys: 0,
|
||||
};
|
||||
const entries = collectBundlerCallArgumentValues(node, requireName);
|
||||
const asyncChunks = collectBundlerAsyncChunkKeys(node, requireName);
|
||||
const entries = collectBundlerCallArgumentValues(
|
||||
node,
|
||||
requireBindingId,
|
||||
bindings,
|
||||
null,
|
||||
);
|
||||
const asyncChunks = collectBundlerCallArgumentValues(
|
||||
node,
|
||||
requireBindingId,
|
||||
bindings,
|
||||
"e",
|
||||
);
|
||||
return {
|
||||
requireName,
|
||||
entryModuleKeys: entries.values,
|
||||
@@ -329,23 +368,43 @@ const runtimeMetadata = (node: t.Node | null | undefined): RuntimeMetadata => {
|
||||
|
||||
const collectBundlerAsyncChunkKeys = (
|
||||
factory: BundlerFunction,
|
||||
requireName: string | null,
|
||||
requireBindingId: string | null,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
): StaticValues =>
|
||||
requireName === null
|
||||
requireBindingId === null
|
||||
? { values: [], unknown: 0 }
|
||||
: collectBundlerCallArgumentValues(factory, `${requireName}.e`);
|
||||
: collectBundlerCallArgumentValues(
|
||||
factory,
|
||||
requireBindingId,
|
||||
bindings,
|
||||
"e",
|
||||
);
|
||||
|
||||
const collectBundlerCallArgumentValues = (
|
||||
factory: BundlerFunction,
|
||||
callee: string,
|
||||
bindingId: string | null,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
member: string | null,
|
||||
): StaticValues => {
|
||||
if (bindingId === null) return { values: [], unknown: 0 };
|
||||
const values: string[] = [];
|
||||
let unknown = 0;
|
||||
const bindingIds = new Set([bindingId]);
|
||||
traverseJavaScriptAst(factory, {
|
||||
enter: (node) => {
|
||||
if (!t.isCallExpression(node) && !t.isNewExpression(node))
|
||||
return undefined;
|
||||
if (calleeName(node.callee) !== callee) return undefined;
|
||||
const loader = semanticFactoryLoaderMemberAt(
|
||||
node,
|
||||
bindings,
|
||||
bindingIds,
|
||||
member,
|
||||
);
|
||||
if (loader === undefined) return undefined;
|
||||
if (loader === "unknown") {
|
||||
unknown += 1;
|
||||
return undefined;
|
||||
}
|
||||
const value = argumentValue(node.arguments[0]);
|
||||
if (value === undefined) unknown += 1;
|
||||
else values.push(value);
|
||||
|
||||
@@ -40,6 +40,11 @@ import {
|
||||
isUnmodifiedGlobalReference,
|
||||
isUnmodifiedImportMetaUrl,
|
||||
} from "./javascriptSemanticAmbientWrites.js";
|
||||
import {
|
||||
semanticFactoryLoaderMemberAt,
|
||||
semanticFactoryRequireLoaderAt,
|
||||
semanticRequireLoaderAt,
|
||||
} from "./javascriptSemanticRequireOrigin.js";
|
||||
|
||||
type JavaScriptCallLike =
|
||||
| t.CallExpression
|
||||
@@ -56,16 +61,14 @@ export const inspectCall = (
|
||||
const { accumulator } = context;
|
||||
const name = calleeName(node.callee);
|
||||
const first = stringValue(node.arguments[0]);
|
||||
const module = context.accumulator.moduleRangeIndex.find(node.start);
|
||||
const moduleRequireName = module?.requireName ?? null;
|
||||
const requireCall = requireCallSpecifier(node, name, moduleRequireName);
|
||||
const requireCall = requireCallSpecifier(node, bindings, context);
|
||||
if (requireCall !== null)
|
||||
addReference(context, {
|
||||
node,
|
||||
kind: "require",
|
||||
specifier: requireCall.specifier,
|
||||
});
|
||||
const chunkLoad = chunkLoadSpecifier(node, name, moduleRequireName);
|
||||
const chunkLoad = chunkLoadSpecifier(node, bindings, context);
|
||||
if (chunkLoad !== null)
|
||||
addReference(context, {
|
||||
node,
|
||||
@@ -224,31 +227,61 @@ const noteMediaElementPreloads = (
|
||||
|
||||
const requireCallSpecifier = (
|
||||
node: JavaScriptCallLike,
|
||||
name: string,
|
||||
moduleRequireName: string | null,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
context: FindingContext,
|
||||
): { readonly specifier: string | undefined } | null => {
|
||||
if (name === "require" || name.endsWith(".require"))
|
||||
return { specifier: argumentValue(node.arguments[0]) };
|
||||
if (name === "__webpack_require__" || name === moduleRequireName)
|
||||
return { specifier: argumentValue(node.arguments[0]) };
|
||||
if (
|
||||
name === "__webpack_require__.bind" ||
|
||||
(moduleRequireName !== null && name === `${moduleRequireName}.bind`)
|
||||
)
|
||||
return { specifier: argumentValue(node.arguments[1]) };
|
||||
const bareRequire = t.isIdentifier(node.callee, { name: "require" });
|
||||
if (bareRequire) {
|
||||
const loader = semanticRequireLoaderAt(
|
||||
node,
|
||||
bindings,
|
||||
context.accumulator.factoryRequireBindingIds,
|
||||
);
|
||||
if (loader === "local") return null;
|
||||
if (loader === "unknown") context.accumulator.unknownFindings += 1;
|
||||
return {
|
||||
specifier:
|
||||
loader === "unknown" ? undefined : argumentValue(node.arguments[0]),
|
||||
};
|
||||
}
|
||||
const factoryLoader = semanticFactoryRequireLoaderAt(
|
||||
node,
|
||||
bindings,
|
||||
context.accumulator.factoryRequireBindingIds,
|
||||
);
|
||||
if (factoryLoader === "factory-loader")
|
||||
return {
|
||||
specifier: argumentValue(
|
||||
node.arguments[
|
||||
t.isMemberExpression(unwrapJavaScriptExpression(node.callee).node) ||
|
||||
t.isOptionalMemberExpression(
|
||||
unwrapJavaScriptExpression(node.callee).node,
|
||||
)
|
||||
? 1
|
||||
: 0
|
||||
],
|
||||
),
|
||||
};
|
||||
if (factoryLoader === "unknown") {
|
||||
context.accumulator.unknownFindings += 1;
|
||||
return { specifier: undefined };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const chunkLoadSpecifier = (
|
||||
node: JavaScriptCallLike,
|
||||
name: string,
|
||||
moduleRequireName: string | null,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
context: FindingContext,
|
||||
): { readonly specifier: string | undefined } | null => {
|
||||
if (
|
||||
name !== "__webpack_require__.e" &&
|
||||
(moduleRequireName === null || name !== `${moduleRequireName}.e`)
|
||||
)
|
||||
return null;
|
||||
const loader = semanticFactoryLoaderMemberAt(
|
||||
node,
|
||||
bindings,
|
||||
context.accumulator.factoryRequireBindingIds,
|
||||
"e",
|
||||
);
|
||||
if (loader === undefined) return null;
|
||||
if (loader === "unknown") return { specifier: undefined };
|
||||
return { specifier: prefixedArgument("chunk:", node.arguments[0]) };
|
||||
};
|
||||
|
||||
|
||||
@@ -62,6 +62,210 @@ it("does not treat a same-named shadow as the factory exports parameter", () =>
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not resolve a parameter named require as a module dependency", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(
|
||||
`function run(require) { return require("./ghost.cjs"); }`,
|
||||
);
|
||||
expect(analysis.references).not.toContainEqual(
|
||||
expect.objectContaining({ specifier: "./ghost.cjs" }),
|
||||
);
|
||||
expect(analysis.references).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "require",
|
||||
specifier: null,
|
||||
expression: 'require("./ghost.cjs")',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a top-level wrapper require redeclaration", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(
|
||||
`var require; require("./wrapper.cjs");`,
|
||||
);
|
||||
expect(analysis.references).toContainEqual(
|
||||
expect.objectContaining({ specifier: "./wrapper.cjs" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("retains the wrapper dependency evaluated inside its replacing assignment", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(
|
||||
`var require; require = require("./before-replacement.cjs");`,
|
||||
);
|
||||
expect(analysis.references).toContainEqual(
|
||||
expect.objectContaining({ specifier: "./before-replacement.cjs" }),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"a deferred function assignment",
|
||||
`var require; function load() { require = require("./maybe.cjs"); } load(); load();`,
|
||||
],
|
||||
[
|
||||
"an assignment repeated by a loop",
|
||||
`var require; while (condition) { require = require("./maybe.cjs"); }`,
|
||||
],
|
||||
])("keeps %s unknown", (_label, source) => {
|
||||
const analysis = analyzeJavaScriptStaticSource(source);
|
||||
expect(analysis.references).not.toContainEqual(
|
||||
expect.objectContaining({ specifier: "./maybe.cjs" }),
|
||||
);
|
||||
expect(analysis.references).toContainEqual(
|
||||
expect.objectContaining({ kind: "require", specifier: null }),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"a skipped wrapper write",
|
||||
`var require; if (false) require = localLoader; require("./ghost.cjs");`,
|
||||
"./ghost.cjs",
|
||||
],
|
||||
[
|
||||
"a skipped createRequire write",
|
||||
`import { createRequire } from "node:module"; let require = () => "local"; if (false) require = createRequire(import.meta.url); require("./ghost.cjs");`,
|
||||
"./ghost.cjs",
|
||||
],
|
||||
[
|
||||
"a class static field write before its computed key in source order",
|
||||
`var require; class C { static value = (require = () => "local"); [require("./computed-key.cjs")]() {} }`,
|
||||
"./computed-key.cjs",
|
||||
],
|
||||
[
|
||||
"a loop condition write before a later call",
|
||||
`var require; while ((require = () => "local", false)) {} require("./after-loop.cjs");`,
|
||||
"./after-loop.cjs",
|
||||
],
|
||||
])("preserves uncertainty for %s", (_label, source, specifier) => {
|
||||
const analysis = analyzeJavaScriptStaticSource(source);
|
||||
expect(analysis.references).not.toContainEqual(
|
||||
expect.objectContaining({ specifier }),
|
||||
);
|
||||
expect(analysis.references).toContainEqual(
|
||||
expect.objectContaining({ kind: "require", specifier: null }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a proven createRequire binding", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
import { createRequire as makeRequire } from "node:module";
|
||||
const require = makeRequire(import.meta.url);
|
||||
require("./created.cjs");
|
||||
`);
|
||||
expect(analysis.references).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ specifier: "./created.cjs" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("recognizes only factory loader calls and supported bind references", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
globalThis.webpackChunkApp.push([[1], {
|
||||
"factory": function (module, exports, loader) {
|
||||
loader.resolve("./not-a-dependency.cjs");
|
||||
loader("./dependency.cjs");
|
||||
loader.bind(module, "./bound-dependency.cjs");
|
||||
}
|
||||
}]);
|
||||
`);
|
||||
expect(analysis.references.map(({ specifier }) => specifier)).toEqual(
|
||||
expect.arrayContaining(["./dependency.cjs", "./bound-dependency.cjs"]),
|
||||
);
|
||||
expect(analysis.references).not.toContainEqual(
|
||||
expect.objectContaining({ specifier: "./not-a-dependency.cjs" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps reassigned factory loaders unknown instead of resolving imports", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
globalThis.webpackChunkApp.push([[1], {
|
||||
"factory": function (module, exports, loader) {
|
||||
loader = userInput;
|
||||
loader("./uncertain.cjs");
|
||||
}
|
||||
}]);
|
||||
`);
|
||||
expect(analysis.references).not.toContainEqual(
|
||||
expect.objectContaining({ specifier: "./uncertain.cjs" }),
|
||||
);
|
||||
expect(analysis.references).toContainEqual(
|
||||
expect.objectContaining({ kind: "require", specifier: null }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a reassigned function named require unknown", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
function require() { return userInput; }
|
||||
require = replacement;
|
||||
require("./uncertain.cjs");
|
||||
`);
|
||||
expect(analysis.references).not.toContainEqual(
|
||||
expect.objectContaining({ specifier: "./uncertain.cjs" }),
|
||||
);
|
||||
expect(analysis.references).toContainEqual(
|
||||
expect.objectContaining({ kind: "require", specifier: null }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not treat a shadowed webpack loader name as bundler ownership", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
function local(__webpack_require__) {
|
||||
__webpack_require__("./ghost.cjs");
|
||||
__webpack_require__.e("ghost-chunk");
|
||||
}
|
||||
`);
|
||||
expect(analysis.references).not.toContainEqual(
|
||||
expect.objectContaining({ specifier: "./ghost.cjs" }),
|
||||
);
|
||||
expect(analysis.references).not.toContainEqual(
|
||||
expect.objectContaining({ specifier: "chunk:ghost-chunk" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses recovered factory and runtime loader identities for webpack imports and chunks", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
globalThis.webpackChunkApp.push([[1], {
|
||||
"factory": function (module, exports, __webpack_require__) {
|
||||
__webpack_require__("./factory-dependency.cjs");
|
||||
__webpack_require__.e("factory-chunk");
|
||||
}
|
||||
}, function (__webpack_require__) {
|
||||
__webpack_require__("factory");
|
||||
__webpack_require__.e("runtime-chunk");
|
||||
}]);
|
||||
`);
|
||||
expect(analysis.references.map(({ specifier }) => specifier)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"./factory-dependency.cjs",
|
||||
"chunk:factory-chunk",
|
||||
"chunk:runtime-chunk",
|
||||
]),
|
||||
);
|
||||
expect(analysis.bundler_registrations[0]?.async_chunk_keys).toEqual(
|
||||
expect.arrayContaining(["factory-chunk", "runtime-chunk"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not attribute a shadowed factory loader parameter to async chunks", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
globalThis.webpackChunkApp.push([[1], {
|
||||
"factory": function (module, exports, loader) {
|
||||
function local(__webpack_require__) {
|
||||
__webpack_require__.e("ghost-chunk");
|
||||
}
|
||||
loader.e("real-chunk");
|
||||
}
|
||||
}]);
|
||||
`);
|
||||
expect(analysis.bundler_registrations[0]?.async_chunk_keys).toEqual([
|
||||
"real-chunk",
|
||||
]);
|
||||
expect(analysis.references).not.toContainEqual(
|
||||
expect.objectContaining({ specifier: "chunk:ghost-chunk" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not confuse a shadowed dispatch table with the outer module table", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`(() => {
|
||||
const modules = { 17(module) { module.exports = require("electron"); } };
|
||||
|
||||
@@ -53,6 +53,8 @@ export interface JavaScriptAnalysisAccumulator {
|
||||
readonly utilityProcesses: LocatedJavaScriptFinding<ElectronUtilityProcessFinding>[];
|
||||
readonly nativeAddonBindings: LocatedJavaScriptFinding<ElectronNativeAddonBindingFinding>[];
|
||||
readonly moduleRangeIndex: JavaScriptModuleRangeIndex;
|
||||
/** Binding identities proven as third-parameter loaders of recovered factories. */
|
||||
readonly factoryRequireBindingIds: Set<string>;
|
||||
/** `preload` attributes of audio and video elements, which are not scripts. */
|
||||
readonly mediaElementPreloads: WeakSet<t.Node>;
|
||||
readonly seen: Set<string>;
|
||||
@@ -114,6 +116,7 @@ export const createJavaScriptAnalysisAccumulator = (
|
||||
utilityProcesses: [],
|
||||
nativeAddonBindings: [],
|
||||
moduleRangeIndex: new JavaScriptModuleRangeIndex(sourceLength),
|
||||
factoryRequireBindingIds: new Set(),
|
||||
mediaElementPreloads: new WeakSet(),
|
||||
seen: new Set(),
|
||||
visitedNodes: 0,
|
||||
|
||||
Reference in new issue
Block a user