mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(javascript): unify loader binding and mutation ownership
Model the CommonJS wrapper binding and query shared lexical identity, mutation, and evaluation timing at each call. Remove duplicate importer interpretation and preserve uncertainty for with scopes, prototypes, repeated writes, and opaque object escapes. Validate generated fixtures against bounded Node oracles; no analyzed target execution is added. Refs: #3982, #4072 Co-authored-by: Geronimo <varungore1@gmail.com>
This commit is contained in:
1 parent
b67baede2e
commit
1b9b725c4c
8 files changed
+1089
-295
No files matched your search
@@ -1,6 +1,9 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import { semanticBranchMayBeSkipped } from "./javascriptSemanticEvaluationOrder.js";
|
||||
import {
|
||||
semanticBranchMayBeSkipped,
|
||||
semanticIterationCardinality,
|
||||
} from "./javascriptSemanticEvaluationOrder.js";
|
||||
|
||||
import {
|
||||
failedJavaScriptSemanticIr,
|
||||
@@ -16,12 +19,14 @@ import {
|
||||
collectSemanticReferencesSteps,
|
||||
immutableSemanticBindings,
|
||||
immutableSemanticScopes,
|
||||
semanticReferenceRole,
|
||||
} from "./javascriptSemanticProjection.js";
|
||||
import {
|
||||
semanticObjectPatternKeys,
|
||||
runtimeFunctionParameters,
|
||||
semanticStaticPropertyKey,
|
||||
semanticWriteTargets,
|
||||
semanticWriteSiteTarget,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
import type {
|
||||
@@ -29,12 +34,15 @@ import type {
|
||||
JavaScriptSemanticBindingState,
|
||||
JavaScriptSemanticScopeState,
|
||||
JavaScriptBindingIndex,
|
||||
JavaScriptSemanticReferenceEffectsAtSite,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import {
|
||||
currentSemanticScope,
|
||||
resolveSemanticBindingState,
|
||||
semanticResolutionBlocked,
|
||||
resolveSemanticBindingFromScope,
|
||||
semanticBindingLookupFromScope,
|
||||
semanticBindingLookupAt,
|
||||
semanticBindingId,
|
||||
semanticScopeId,
|
||||
semanticStableInitializer,
|
||||
@@ -47,7 +55,11 @@ import {
|
||||
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
|
||||
import { semanticBundledRequireOrigin } from "./javascriptBundledRequireOrigin.js";
|
||||
import { collectSemanticMemberMutationsSteps } from "./javascriptSemanticMemberMutations.js";
|
||||
import { indexJavaScriptAmbientWritesSteps } from "./javascriptSemanticAmbientWrites.js";
|
||||
import { semanticEffectTiming } from "./javascriptSemanticMutationInitializers.js";
|
||||
import {
|
||||
indexJavaScriptAmbientWritesSteps,
|
||||
isUnmodifiedGlobalReference,
|
||||
} from "./javascriptSemanticAmbientWrites.js";
|
||||
import {
|
||||
hasUncertainTypeOnlyModuleSideEffect,
|
||||
semanticImportEqualsSpecifier,
|
||||
@@ -74,7 +86,11 @@ import {
|
||||
|
||||
import { isJavaScriptRpcClientFactory } from "./javascriptRpcEndpoints.js";
|
||||
import { createSemanticModuleInterop } from "./javascriptModuleInterop.js";
|
||||
import { SemanticPropertyPathCoverage } from "./javascriptSemanticPropertyPaths.js";
|
||||
import {
|
||||
SemanticPropertyPathCoverage,
|
||||
semanticPropertyPathKeyMatches,
|
||||
type JavaScriptSemanticPropertyPath,
|
||||
} from "./javascriptSemanticPropertyPaths.js";
|
||||
import { indexCommonJsExportReceiversSteps } from "./javascriptCommonJsExportReceivers.js";
|
||||
import type { JavaScriptModuleResolutionMode } from "./javascriptModuleResolutionMode.js";
|
||||
|
||||
@@ -136,13 +152,131 @@ export const analyzeParsedJavaScriptReferences = (
|
||||
return collectSemanticReferences(file.program, state, name);
|
||||
};
|
||||
|
||||
const indexedAmbientWriteStates =
|
||||
new WeakSet<JavaScriptSemanticAnalysisState>();
|
||||
const indexedMemberMutationStates =
|
||||
new WeakSet<JavaScriptSemanticAnalysisState>();
|
||||
|
||||
function* indexSemanticAmbientWritesSteps(
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): Generator<void, void> {
|
||||
if (indexedAmbientWriteStates.has(state)) return;
|
||||
yield* indexJavaScriptAmbientWritesSteps(state.program, state);
|
||||
indexedAmbientWriteStates.add(state);
|
||||
}
|
||||
|
||||
/** Index alias-aware mutation facts only for consumers that request them. */
|
||||
function* indexSemanticMemberMutationFactsSteps(
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): Generator<void, void> {
|
||||
if (indexedMemberMutationStates.has(state)) return;
|
||||
yield* indexSemanticAmbientWritesSteps(state);
|
||||
yield* collectSemanticMemberMutationsSteps(state.program, state);
|
||||
indexedMemberMutationStates.add(state);
|
||||
}
|
||||
|
||||
/** Resolve one identifier or member use and summarize its source-site effects. */
|
||||
const semanticReferenceEffectsAtSite = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
site: t.Node,
|
||||
referenceNode: t.Node,
|
||||
requestedPath?: JavaScriptSemanticPropertyPath,
|
||||
): JavaScriptSemanticReferenceEffectsAtSite => {
|
||||
const { root, path: inferredPath } = semanticReferenceRoot(referenceNode);
|
||||
const memberEffectsIndexed = indexedMemberMutationStates.has(state);
|
||||
if (!t.isIdentifier(root))
|
||||
return {
|
||||
bindingId: null,
|
||||
resolution: "unknown",
|
||||
memberEffectsIndexed,
|
||||
effects: [],
|
||||
};
|
||||
const lookup = semanticBindingLookupAt(state, root, root.name);
|
||||
const binding = "binding" in lookup ? lookup.binding : undefined;
|
||||
const blocked = lookup.kind !== "binding" && lookup.kind !== "ambient";
|
||||
const resolution =
|
||||
lookup.kind === "binding" ? "resolved" : blocked ? "unknown" : "unbound";
|
||||
const path = requestedPath ?? inferredPath;
|
||||
const effects: JavaScriptSemanticReferenceEffectsAtSite["effects"][number][] =
|
||||
[];
|
||||
const retain = (
|
||||
kind: JavaScriptSemanticReferenceEffectsAtSite["effects"][number]["kind"],
|
||||
node: t.Node | undefined,
|
||||
effectPath?: JavaScriptSemanticPropertyPath,
|
||||
): void => {
|
||||
if (effectPath !== undefined && !semanticPathsOverlap(path, effectPath))
|
||||
return;
|
||||
effects.push({
|
||||
kind,
|
||||
...(effectPath === undefined ? {} : { path: effectPath }),
|
||||
timing: semanticEffectTiming(site, node, state),
|
||||
});
|
||||
};
|
||||
if (binding !== undefined) {
|
||||
for (const node of state.bindingWrites.get(binding.bindingId) ?? [])
|
||||
retain("binding-write", node);
|
||||
if (indexedMemberMutationStates.has(state)) {
|
||||
for (const effect of binding.memberWrites)
|
||||
retain("member-write", effect.node, effect.path);
|
||||
for (const effect of binding.escapedPaths)
|
||||
retain("escape", effect.node, effect.path);
|
||||
}
|
||||
} else if (lookup.kind === "ambient" || lookup.kind === "uncertain-ambient") {
|
||||
for (const node of state.ambientBindingWrites.get(root.name) ?? [])
|
||||
retain("ambient-write", node);
|
||||
}
|
||||
return {
|
||||
bindingId: binding?.bindingId ?? null,
|
||||
resolution,
|
||||
memberEffectsIndexed,
|
||||
effects,
|
||||
};
|
||||
};
|
||||
|
||||
const semanticReferenceRoot = (
|
||||
node: t.Node,
|
||||
): { readonly root: t.Node; readonly path: JavaScriptSemanticPropertyPath } => {
|
||||
const path: (string | number | null)[] = [];
|
||||
let current = unwrapJavaScriptExpression(node).node;
|
||||
while (
|
||||
t.isMemberExpression(current) ||
|
||||
t.isOptionalMemberExpression(current)
|
||||
) {
|
||||
const key = semanticStaticPropertyKey(current.property, current.computed);
|
||||
path.unshift(key ?? null);
|
||||
current = unwrapJavaScriptExpression(current.object).node;
|
||||
}
|
||||
return { root: current, path };
|
||||
};
|
||||
|
||||
const semanticPathsOverlap = (
|
||||
left: JavaScriptSemanticPropertyPath,
|
||||
right: JavaScriptSemanticPropertyPath,
|
||||
): boolean => {
|
||||
for (let index = 0; index < Math.min(left.length, right.length); index++) {
|
||||
const leftKey = left[index];
|
||||
const rightKey = right[index];
|
||||
if (leftKey === undefined || rightKey === undefined) return true;
|
||||
const leftMatchesRight =
|
||||
typeof rightKey === "string" || typeof rightKey === "number"
|
||||
? semanticPropertyPathKeyMatches(leftKey, String(rightKey))
|
||||
: true;
|
||||
const rightMatchesLeft =
|
||||
typeof leftKey === "string" || typeof leftKey === "number"
|
||||
? semanticPropertyPathKeyMatches(rightKey, String(leftKey))
|
||||
: true;
|
||||
if (!leftMatchesRight && !rightMatchesLeft) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/** Share the lexical index with bounded, demand-driven boundary analyses. */
|
||||
export function* indexJavaScriptBindingsSteps(
|
||||
program: t.Program,
|
||||
): Generator<void, JavaScriptSemanticAnalysisState> {
|
||||
const state = createState(program);
|
||||
yield* collectDefinitionsSteps(program, state);
|
||||
yield* indexJavaScriptAmbientWritesSteps(program, state);
|
||||
yield* indexSemanticAmbientWritesSteps(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -167,8 +301,8 @@ export function* analyzeParsedJavaScriptSemanticsSteps(
|
||||
): Generator<void, JavaScriptSemanticIr> {
|
||||
const state = createState(file.program, staticImportResolutionMode);
|
||||
yield* collectDefinitionsSteps(file.program, state);
|
||||
yield* indexJavaScriptAmbientWritesSteps(file.program, state);
|
||||
yield* collectSemanticMemberMutationsSteps(file.program, state);
|
||||
yield* indexSemanticAmbientWritesSteps(state);
|
||||
yield* indexSemanticMemberMutationFactsSteps(state);
|
||||
const commonJsReceivers = yield* indexCommonJsExportReceiversSteps(state);
|
||||
yield* traverseJavaScriptAstSteps(file.program, {
|
||||
enter: (node) => collectSemanticModuleLink(node, state, commonJsReceivers),
|
||||
@@ -262,20 +396,38 @@ export interface JavaScriptReceiverIndex extends JavaScriptBindingIndex {
|
||||
*/
|
||||
export function* indexJavaScriptBindingsWithReceiversSteps(
|
||||
program: t.Program,
|
||||
hostContext?: "commonjs-wrapper",
|
||||
): Generator<void, JavaScriptReceiverIndex> {
|
||||
const state = createState(program);
|
||||
if (hostContext === "commonjs-wrapper") {
|
||||
const scope = currentSemanticScope(state.scopes);
|
||||
const binding: JavaScriptSemanticBindingState = {
|
||||
...createBinding(scope, "require", "parameter", false),
|
||||
hostOrigin: "commonjs-wrapper",
|
||||
};
|
||||
scope.bindings.set(binding.name, binding);
|
||||
state.bindingsById.set(binding.bindingId, binding);
|
||||
}
|
||||
yield* collectDefinitionsSteps(program, state);
|
||||
const receivers = yield* classifyDefinitionReceiversSteps(program, state);
|
||||
let ambientIndexed = false;
|
||||
let references: readonly JavaScriptSemanticReference[] | undefined;
|
||||
return {
|
||||
sourceType: program.sourceType,
|
||||
bindings: state,
|
||||
receivers,
|
||||
*indexAmbientWritesSteps() {
|
||||
if (ambientIndexed) return;
|
||||
yield* indexJavaScriptAmbientWritesSteps(program, state);
|
||||
ambientIndexed = true;
|
||||
yield* indexSemanticAmbientWritesSteps(state);
|
||||
},
|
||||
*indexMemberMutationFactsSteps() {
|
||||
yield* indexSemanticMemberMutationFactsSteps(state);
|
||||
},
|
||||
effectsForReferenceAt(site, referenceNode, propertyPath) {
|
||||
return semanticReferenceEffectsAtSite(
|
||||
state,
|
||||
site,
|
||||
referenceNode,
|
||||
propertyPath,
|
||||
);
|
||||
},
|
||||
*referencesSteps() {
|
||||
references ??= yield* collectSemanticReferencesSteps(program, state);
|
||||
@@ -713,6 +865,9 @@ const createState = (
|
||||
scopesById: new Map([[root.scopeId, root]]),
|
||||
scopeByNode: new WeakMap([[program, root]]),
|
||||
bindingsById: new Map(),
|
||||
bindingWrites: new Map(),
|
||||
prototypeRequireMutationObserved: false,
|
||||
prototypeEnumerationMayAffect: false,
|
||||
callables: [],
|
||||
callableNodesById: new Map(),
|
||||
moduleLinks: [],
|
||||
@@ -740,6 +895,7 @@ function* collectDefinitionsSteps(
|
||||
readonly scope: JavaScriptSemanticScopeState;
|
||||
readonly conditionallySkipped: boolean;
|
||||
}[] = [];
|
||||
let needsPrototypeFacts = false;
|
||||
const functionBodies: {
|
||||
readonly body: t.BlockStatement;
|
||||
readonly scope: JavaScriptSemanticScopeState;
|
||||
@@ -748,6 +904,12 @@ function* collectDefinitionsSteps(
|
||||
yield* traverseJavaScriptAstSteps(program, {
|
||||
enter: (node, parent, readAncestors) => {
|
||||
if (parent !== null) state.parentsByNode.set(node, parent);
|
||||
if (
|
||||
t.isWithStatement(node) ||
|
||||
t.isForInStatement(node) ||
|
||||
t.isForOfStatement(node)
|
||||
)
|
||||
needsPrototypeFacts = true;
|
||||
let parentScope = currentSemanticScope(stack);
|
||||
if (
|
||||
parent !== null &&
|
||||
@@ -777,6 +939,7 @@ function* collectDefinitionsSteps(
|
||||
location: range(parent),
|
||||
bindingsComplete: false,
|
||||
bindings: new Map(),
|
||||
dynamicWithStatement: parent,
|
||||
};
|
||||
state.scopes.push(dynamicScope);
|
||||
state.scopesById.set(dynamicScope.scopeId, dynamicScope);
|
||||
@@ -837,6 +1000,8 @@ function* collectDefinitionsSteps(
|
||||
stack.pop();
|
||||
},
|
||||
});
|
||||
if (needsPrototypeFacts)
|
||||
yield* indexPrototypeUncertaintySteps(program, state);
|
||||
// All declarations must exist before resolving writes, including hoisted
|
||||
// functions and local declarations that shadow an outer binding.
|
||||
for (const [
|
||||
@@ -857,6 +1022,112 @@ function* collectDefinitionsSteps(
|
||||
}
|
||||
}
|
||||
|
||||
type PrototypeOwner = "Object" | "Array" | "String";
|
||||
|
||||
const prototypeOwners = ["Object", "Array", "String"] as const;
|
||||
|
||||
/** Preserve uncertainty when source can change or expose a built-in prototype. */
|
||||
function* indexPrototypeUncertaintySteps(
|
||||
program: t.Program,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): Generator<void, void> {
|
||||
const taint = (owner: PrototypeOwner): void => {
|
||||
if (owner === "Object") state.prototypeRequireMutationObserved = undefined;
|
||||
state.prototypeEnumerationMayAffect = undefined;
|
||||
};
|
||||
const globalOwner = (node: t.Node): PrototypeOwner | undefined => {
|
||||
const value = unwrapJavaScriptExpression(node).node;
|
||||
for (const owner of prototypeOwners) {
|
||||
if (isUnmodifiedGlobalReference(value, state, owner)) return owner;
|
||||
if (
|
||||
t.isIdentifier(value, { name: owner }) &&
|
||||
resolveSemanticBindingState(state, value, owner) === undefined
|
||||
)
|
||||
return owner;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const prototypeOwner = (node: t.Node): PrototypeOwner | undefined => {
|
||||
const value = unwrapJavaScriptExpression(node).node;
|
||||
return (t.isMemberExpression(value) ||
|
||||
t.isOptionalMemberExpression(value)) &&
|
||||
semanticStaticPropertyKey(value.property, value.computed) === "prototype"
|
||||
? globalOwner(value.object)
|
||||
: undefined;
|
||||
};
|
||||
const objectMethod = (node: t.Node): string | undefined => {
|
||||
const callee = unwrapJavaScriptExpression(node).node;
|
||||
return (t.isMemberExpression(callee) ||
|
||||
t.isOptionalMemberExpression(callee)) &&
|
||||
globalOwner(callee.object) === "Object"
|
||||
? (semanticStaticPropertyKey(callee.property, callee.computed) ??
|
||||
undefined)
|
||||
: undefined;
|
||||
};
|
||||
const mutators = new Set([
|
||||
"defineProperty",
|
||||
"defineProperties",
|
||||
"setPrototypeOf",
|
||||
]);
|
||||
yield* traverseJavaScriptAstSteps(program, {
|
||||
enter: (node, parent) => {
|
||||
const write = semanticWriteSiteTarget(node);
|
||||
if (write !== null) {
|
||||
for (const target of semanticWriteTargets(write)) {
|
||||
const value = unwrapJavaScriptExpression(target).node;
|
||||
if (
|
||||
t.isMemberExpression(value) ||
|
||||
t.isOptionalMemberExpression(value)
|
||||
) {
|
||||
const owner =
|
||||
prototypeOwner(value.object) ?? globalOwner(value.object);
|
||||
if (owner !== undefined) taint(owner);
|
||||
} else {
|
||||
const owner = globalOwner(value);
|
||||
if (owner !== undefined) taint(owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
|
||||
const method = objectMethod(node.callee);
|
||||
const callee = unwrapJavaScriptExpression(node.callee).node;
|
||||
if (
|
||||
t.isMemberExpression(callee) ||
|
||||
t.isOptionalMemberExpression(callee)
|
||||
) {
|
||||
const owner = prototypeOwner(callee.object);
|
||||
if (owner !== undefined) taint(owner);
|
||||
}
|
||||
for (const [index, argument] of node.arguments.entries()) {
|
||||
const owner = prototypeOwner(argument) ?? globalOwner(argument);
|
||||
if (owner === undefined) continue;
|
||||
// These intrinsics mutate only their first argument. Linking another
|
||||
// object's prototype to this one does not mutate this prototype.
|
||||
if (method !== undefined && mutators.has(method) && index !== 0)
|
||||
continue;
|
||||
taint(owner);
|
||||
}
|
||||
}
|
||||
const owner = prototypeOwner(node) ?? globalOwner(node);
|
||||
if (owner === undefined || parent === null) return;
|
||||
if (
|
||||
(t.isMemberExpression(parent) ||
|
||||
t.isOptionalMemberExpression(parent)) &&
|
||||
parent.object === node
|
||||
)
|
||||
return;
|
||||
if (
|
||||
(t.isCallExpression(parent) || t.isOptionalCallExpression(parent)) &&
|
||||
parent.arguments.includes(node as t.Expression)
|
||||
)
|
||||
return;
|
||||
if (t.isIdentifier(node) && semanticReferenceRole(node, parent) === null)
|
||||
return;
|
||||
taint(owner);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const copyParameterToBody = (input: {
|
||||
readonly identifier: t.Identifier;
|
||||
readonly body: t.BlockStatement;
|
||||
@@ -1043,22 +1314,44 @@ const bindAssignment = (
|
||||
scope,
|
||||
state,
|
||||
conditionallySkipped,
|
||||
node,
|
||||
);
|
||||
else
|
||||
for (const identifier of semanticWriteTargets(node.left))
|
||||
if (t.isIdentifier(identifier))
|
||||
addAssignment(identifier, node, scope, state, conditionallySkipped);
|
||||
addAssignment(
|
||||
identifier,
|
||||
node,
|
||||
scope,
|
||||
state,
|
||||
conditionallySkipped,
|
||||
node,
|
||||
);
|
||||
} else if (t.isUpdateExpression(node)) {
|
||||
const argument = unwrapJavaScriptExpression(node.argument).node;
|
||||
if (t.isIdentifier(argument))
|
||||
addAssignment(argument, node, scope, state, conditionallySkipped);
|
||||
} else if (t.isForOfStatement(node) || t.isForInStatement(node))
|
||||
} else if (t.isForOfStatement(node) || t.isForInStatement(node)) {
|
||||
if (
|
||||
semanticIterationCardinality(
|
||||
node,
|
||||
state.prototypeEnumerationMayAffect,
|
||||
) === "empty"
|
||||
)
|
||||
return;
|
||||
for (const pattern of t.isVariableDeclaration(node.left)
|
||||
? node.left.declarations.map(({ id }) => id)
|
||||
: [node.left]) {
|
||||
for (const identifier of semanticWriteTargets(pattern))
|
||||
if (t.isIdentifier(identifier))
|
||||
addAssignment(identifier, node, scope, state, conditionallySkipped);
|
||||
addAssignment(
|
||||
identifier,
|
||||
node,
|
||||
scope,
|
||||
state,
|
||||
conditionallySkipped,
|
||||
node,
|
||||
);
|
||||
if (t.isForOfStatement(node))
|
||||
// Keep the loop value unknown, but preserve references yielded into
|
||||
// each bound slot so writes/escapes can reach their iterable origins.
|
||||
@@ -1071,6 +1364,7 @@ const bindAssignment = (
|
||||
referenceOnly: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const nestedScope = (
|
||||
@@ -1455,20 +1749,24 @@ const addAssignment = (
|
||||
scope: JavaScriptSemanticScopeState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
conditionallySkipped: boolean,
|
||||
writeSite?: t.Node,
|
||||
): void => {
|
||||
const binding = resolveSemanticBindingFromScope(
|
||||
scope,
|
||||
identifier.name,
|
||||
state,
|
||||
);
|
||||
const lookup = semanticBindingLookupFromScope(scope, identifier.name, state);
|
||||
if (lookup.kind === "captured") return;
|
||||
const binding = "binding" in lookup ? lookup.binding : undefined;
|
||||
conditionallySkipped ||=
|
||||
lookup.kind !== "binding" && lookup.kind !== "ambient";
|
||||
if (binding === undefined) {
|
||||
// Unbound identifiers can denote host wrapper bindings (such as require),
|
||||
// which are separate from properties written through the global object.
|
||||
const writes = state.ambientBindingWrites.get(identifier.name) ?? [];
|
||||
writes.push(identifier);
|
||||
writes.push(writeSite ?? initializer);
|
||||
state.ambientBindingWrites.set(identifier.name, writes);
|
||||
return;
|
||||
}
|
||||
const writes = state.bindingWrites.get(binding.bindingId) ?? [];
|
||||
writes.push(writeSite ?? initializer);
|
||||
state.bindingWrites.set(binding.bindingId, writes);
|
||||
binding.mutable = true;
|
||||
binding.definitions.push({ kind: "assignment", location: range(identifier) });
|
||||
binding.initializers.push({ node: initializer, projection: [] });
|
||||
|
||||
@@ -1,9 +1,70 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import { unwrapJavaScriptExpression } from "./javascriptAstValues.js";
|
||||
import {
|
||||
readExactJavaScriptLiteral,
|
||||
semanticStaticPropertyKey,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
|
||||
const preserveGrouping = { stopAtParentheses: true } as const;
|
||||
|
||||
export type JavaScriptSemanticIterationCardinality =
|
||||
| "empty"
|
||||
| "nonempty"
|
||||
| "unknown";
|
||||
|
||||
/** Exact source-only cardinality for literal loop heads. */
|
||||
export const semanticIterationCardinality = (
|
||||
loop: t.ForInStatement | t.ForOfStatement,
|
||||
prototypeMayAffectEnumeration: boolean | undefined,
|
||||
): JavaScriptSemanticIterationCardinality => {
|
||||
const iterable = unwrapJavaScriptExpression(loop.right).node;
|
||||
if (t.isForOfStatement(loop)) {
|
||||
if (prototypeMayAffectEnumeration !== false) return "unknown";
|
||||
if (t.isArrayExpression(iterable)) {
|
||||
if (iterable.elements.length === 0) return "empty";
|
||||
if (
|
||||
iterable.elements.some(
|
||||
(element) => element === null || !t.isSpreadElement(element),
|
||||
)
|
||||
)
|
||||
return "nonempty";
|
||||
return "unknown";
|
||||
}
|
||||
if (t.isStringLiteral(iterable))
|
||||
return iterable.value.length === 0 ? "empty" : "nonempty";
|
||||
return "unknown";
|
||||
}
|
||||
if (t.isObjectExpression(iterable)) {
|
||||
let prototype: t.Node | undefined;
|
||||
for (const property of iterable.properties) {
|
||||
if (t.isSpreadElement(property)) return "unknown";
|
||||
const key = semanticStaticPropertyKey(property.key, property.computed);
|
||||
if (
|
||||
t.isObjectProperty(property) &&
|
||||
!property.computed &&
|
||||
!property.shorthand &&
|
||||
key === "__proto__"
|
||||
) {
|
||||
prototype = unwrapJavaScriptExpression(property.value).node;
|
||||
continue;
|
||||
}
|
||||
if (key === null) return "unknown";
|
||||
return "nonempty";
|
||||
}
|
||||
if (t.isNullLiteral(prototype)) return "empty";
|
||||
if (prototype !== undefined && !readExactJavaScriptLiteral(prototype).found)
|
||||
return "unknown";
|
||||
return prototypeMayAffectEnumeration === false ? "empty" : "unknown";
|
||||
}
|
||||
if (t.isNullLiteral(iterable)) return "empty";
|
||||
if (t.isStringLiteral(iterable) && iterable.value.length > 0)
|
||||
return "nonempty";
|
||||
if (readExactJavaScriptLiteral(iterable).found)
|
||||
return prototypeMayAffectEnumeration === false ? "empty" : "unknown";
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
/** Instance values run on construction; their computed keys run at definition. */
|
||||
export const delayedInstanceFieldValue = (
|
||||
parent: t.Node | undefined,
|
||||
|
||||
@@ -1146,7 +1146,11 @@ const observableMutationPaths = (
|
||||
const hasParsedRequireOrigin = binding.initializers.some(
|
||||
({ node }) => semanticRequireOrigin(node, state) !== undefined,
|
||||
);
|
||||
if (binding.directOrigins.length > 0 || hasParsedRequireOrigin)
|
||||
if (
|
||||
binding.directOrigins.length > 0 ||
|
||||
hasParsedRequireOrigin ||
|
||||
binding.hostOrigin !== undefined
|
||||
)
|
||||
(observer.origins ??= new Set()).add("opaque");
|
||||
for (const initializer of binding.initializers) {
|
||||
link(
|
||||
@@ -1176,6 +1180,11 @@ const observableMutationPaths = (
|
||||
// Keep class/getter projections distinct so cycle pruning does not
|
||||
// discard observable escapes through a shared getter result.
|
||||
if (
|
||||
t.isNewExpression(expression) &&
|
||||
semanticClassCandidates(expression.callee, state).length === 0
|
||||
)
|
||||
(observer.origins ??= new Set()).add("opaque");
|
||||
else if (
|
||||
t.isObjectExpression(expression) ||
|
||||
t.isArrayExpression(expression) ||
|
||||
t.isClass(expression) ||
|
||||
|
||||
@@ -3,13 +3,18 @@ import * as t from "@babel/types";
|
||||
import {
|
||||
delayedInstanceFieldValue,
|
||||
semanticBranchMayBeSkipped,
|
||||
semanticIterationCardinality,
|
||||
} from "./javascriptSemanticEvaluationOrder.js";
|
||||
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
import type { JavaScriptSemanticBindingState } from "./javascriptSemanticState.js";
|
||||
import type {
|
||||
JavaScriptSemanticBindingState,
|
||||
JavaScriptSemanticAnalysisState,
|
||||
JavaScriptSemanticEffectTiming,
|
||||
} from "./javascriptSemanticState.js";
|
||||
|
||||
interface StatementPosition {
|
||||
readonly body: readonly t.Statement[];
|
||||
@@ -120,8 +125,10 @@ export const semanticTopLevelEffectFollowsCapture = (
|
||||
if (t.isFunction(node) || t.isLoop(node)) return null;
|
||||
if (delayedInstanceFieldValue(node, child)) return null;
|
||||
if (t.isVariableDeclarator(node)) declarator = node;
|
||||
if (selected === null && t.isStatement(node))
|
||||
selected = statementPosition(node, parents, declarator);
|
||||
if (t.isStatement(node)) {
|
||||
const enclosing = statementPosition(node, parents, declarator);
|
||||
if (enclosing !== null) selected = enclosing;
|
||||
}
|
||||
if (t.isProgram(node)) return selected;
|
||||
child = node;
|
||||
node = parents.get(node);
|
||||
@@ -141,6 +148,118 @@ export const semanticTopLevelEffectFollowsCapture = (
|
||||
return expressionEvaluationOrder(capture, effect, parents) === "before";
|
||||
};
|
||||
|
||||
/** Include expression order when a write operation contains the read site. */
|
||||
export const semanticEffectFollowsReference = (
|
||||
reference: t.Node,
|
||||
effect: t.Node | undefined,
|
||||
parents: WeakMap<t.Node, t.Node>,
|
||||
): boolean => {
|
||||
if (reference === effect) return false;
|
||||
let current: t.Node | undefined = reference;
|
||||
let child: t.Node | undefined;
|
||||
while (current !== undefined && current !== effect) {
|
||||
if (t.isFunction(current)) break;
|
||||
child = current;
|
||||
current = parents.get(current);
|
||||
}
|
||||
const loopInput =
|
||||
(t.isForInStatement(effect) || t.isForOfStatement(effect)) &&
|
||||
child === effect.right;
|
||||
let once = current;
|
||||
child = undefined;
|
||||
while (once !== undefined) {
|
||||
const initialLoopInput =
|
||||
(t.isForStatement(once) && child === once.init) ||
|
||||
((t.isForInStatement(once) || t.isForOfStatement(once)) &&
|
||||
child === once.right) ||
|
||||
(once === effect && loopInput);
|
||||
if (
|
||||
t.isFunction(once) ||
|
||||
(t.isLoop(once) && !initialLoopInput) ||
|
||||
delayedInstanceFieldValue(once, child)
|
||||
)
|
||||
break;
|
||||
child = once;
|
||||
once = parents.get(once);
|
||||
}
|
||||
return (
|
||||
(current === effect &&
|
||||
once === undefined &&
|
||||
(t.isAssignmentExpression(effect) ||
|
||||
t.isUpdateExpression(effect) ||
|
||||
t.isVariableDeclarator(effect) ||
|
||||
t.isCallExpression(effect) ||
|
||||
t.isOptionalCallExpression(effect) ||
|
||||
t.isNewExpression(effect))) ||
|
||||
(current === effect && once === undefined && loopInput) ||
|
||||
semanticTopLevelEffectFollowsCapture(reference, effect, parents)
|
||||
);
|
||||
};
|
||||
|
||||
export const semanticEffectTiming = (
|
||||
site: t.Node,
|
||||
effect: t.Node | undefined,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptSemanticEffectTiming => {
|
||||
if (effect === undefined) return "unknown";
|
||||
// Invocation receives arguments after their values have been selected.
|
||||
if (
|
||||
site === effect &&
|
||||
(t.isCallExpression(site) ||
|
||||
t.isOptionalCallExpression(site) ||
|
||||
t.isNewExpression(site))
|
||||
)
|
||||
return "after";
|
||||
const loop = semanticEnclosingIteration(effect, state);
|
||||
if (loop !== undefined) {
|
||||
const cardinality = semanticIterationCardinality(
|
||||
loop,
|
||||
state.prototypeEnumerationMayAffect,
|
||||
);
|
||||
if (cardinality === "empty") return "after";
|
||||
if (cardinality === "nonempty") {
|
||||
const loopParent = state.parentsByNode.get(loop);
|
||||
const crossesFunction = (node: t.Node): boolean => {
|
||||
let current: t.Node | undefined = node;
|
||||
while (current !== undefined) {
|
||||
if (t.isFunction(current)) return true;
|
||||
current = state.parentsByNode.get(current);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (
|
||||
loopParent === state.program &&
|
||||
!crossesFunction(site) &&
|
||||
typeof loop.start === "number" &&
|
||||
typeof loop.end === "number" &&
|
||||
typeof site.start === "number"
|
||||
) {
|
||||
if (site.start > loop.end) return "before";
|
||||
if (site.start < loop.start) return "after";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (semanticEffectFollowsReference(site, effect, state.parentsByNode))
|
||||
return "after";
|
||||
if (semanticEffectFollowsReference(effect, site, state.parentsByNode))
|
||||
return "before";
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
const semanticEnclosingIteration = (
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): t.ForInStatement | t.ForOfStatement | undefined => {
|
||||
let current: t.Node | undefined = node;
|
||||
while (current !== undefined) {
|
||||
if (t.isForInStatement(current) || t.isForOfStatement(current))
|
||||
return current;
|
||||
if (t.isFunction(current)) return undefined;
|
||||
current = state.parentsByNode.get(current);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Memoize descendant paths toward the currently compared source expression. */
|
||||
const expressionSiblings = (
|
||||
earlier: t.Node,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
isUnmodifiedCommonJsWrapperBinding,
|
||||
commonJsWrapperBindingDeclaration,
|
||||
semanticResolutionBlocked,
|
||||
semanticStableInitializer,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
@@ -16,7 +17,10 @@ import {
|
||||
} from "./javascriptAstValues.js";
|
||||
import { semanticPropertyPathKeyMatches } from "./javascriptSemanticPropertyPaths.js";
|
||||
import { stringValue } from "./javascriptStaticAnalysisHelpers.js";
|
||||
import { semanticTopLevelEffectFollowsCapture } from "./javascriptSemanticMutationInitializers.js";
|
||||
import {
|
||||
semanticEffectTiming,
|
||||
semanticTopLevelEffectFollowsCapture,
|
||||
} from "./javascriptSemanticMutationInitializers.js";
|
||||
import { createSemanticModuleInterop } from "./javascriptModuleInterop.js";
|
||||
|
||||
export interface JavaScriptRequireOrigin {
|
||||
@@ -37,17 +41,49 @@ export const semanticRequireLoaderAt = (
|
||||
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")
|
||||
const callee = unwrapJavaScriptExpression(node.callee).node;
|
||||
const loader = t.isIdentifier(callee, { name: "require" })
|
||||
? callee
|
||||
: (t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
|
||||
["resolve", "main"].includes(
|
||||
semanticStaticPropertyKey(callee.property, callee.computed) ?? "",
|
||||
) &&
|
||||
t.isIdentifier(callee.object, { name: "require" })
|
||||
? callee.object
|
||||
: undefined;
|
||||
if (loader === undefined) return "local";
|
||||
const binding = resolveSemanticBindingState(state, loader, "require");
|
||||
if (binding === undefined) {
|
||||
if (semanticResolutionBlocked(state, loader, "require")) return "unknown";
|
||||
return (state.ambientBindingWrites.get("require") ?? []).some(
|
||||
(write) => semanticEffectTiming(node, write, state) !== "after",
|
||||
)
|
||||
? "unknown"
|
||||
: "commonjs-loader";
|
||||
}
|
||||
if (factoryBindingIds.has(binding.bindingId))
|
||||
return binding.initializers.length === 0 ? "factory-loader" : "unknown";
|
||||
if (
|
||||
binding.hostOrigin === "commonjs-wrapper" &&
|
||||
[...binding.declarationKinds].every((kind) => kind === "var")
|
||||
) {
|
||||
if (binding.definitions.some(({ kind }) => kind === "function"))
|
||||
return "local";
|
||||
return commonJsWrapperBindingHasPriorWrite(node, binding, state)
|
||||
? "unknown"
|
||||
: "commonjs-loader";
|
||||
}
|
||||
if (isWrapperRequireInitializerCall(binding, node, state))
|
||||
return "commonjs-loader";
|
||||
if (isUnmodifiedCommonJsWrapperBinding(node.callee, "require", state))
|
||||
const wrapperBinding = commonJsWrapperBindingDeclaration(
|
||||
loader,
|
||||
"require",
|
||||
state,
|
||||
);
|
||||
if (
|
||||
wrapperBinding !== undefined &&
|
||||
!commonJsWrapperBindingHasPriorWrite(node, wrapperBinding, state)
|
||||
)
|
||||
return "commonjs-loader";
|
||||
if (isSemanticCreateRequireBinding(binding, state)) return "create-require";
|
||||
const initializer = semanticStableInitializer(binding, state)?.node;
|
||||
@@ -66,6 +102,42 @@ export const semanticRequireLoaderAt = (
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
const commonJsWrapperBindingHasPriorWrite = (
|
||||
site: t.Node,
|
||||
binding: NonNullable<ReturnType<typeof resolveSemanticBindingState>>,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
const writes = state.bindingWrites.get(binding.bindingId) ?? [];
|
||||
const declarationInitializers = binding.initializers
|
||||
.map(({ node }) => node)
|
||||
.filter(
|
||||
(initializer) =>
|
||||
!writes.some((write) => semanticNodeWithin(initializer, write, state)),
|
||||
);
|
||||
return (
|
||||
writes.some(
|
||||
(write) => semanticEffectTiming(site, write, state) !== "after",
|
||||
) ||
|
||||
declarationInitializers.some(
|
||||
(initializer) =>
|
||||
semanticEffectTiming(site, initializer, state) !== "after",
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const semanticNodeWithin = (
|
||||
node: t.Node,
|
||||
ancestor: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
let current: t.Node | undefined = node;
|
||||
while (current !== undefined) {
|
||||
if (current === ancestor) return true;
|
||||
current = state.parentsByNode.get(current);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/** Classify a call through an exact recovered factory-loader binding. */
|
||||
export const semanticFactoryRequireLoaderAt = (
|
||||
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import * as t from "@babel/types";
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
|
||||
import type {
|
||||
JavaScriptSemanticPropertyPath,
|
||||
@@ -42,11 +46,29 @@ export interface JavaScriptSemanticMemberEffect {
|
||||
readonly node: t.Node | undefined;
|
||||
}
|
||||
|
||||
export type JavaScriptSemanticEffectTiming = "before" | "after" | "unknown";
|
||||
|
||||
export interface JavaScriptSemanticReferenceEffect {
|
||||
readonly kind: "binding-write" | "member-write" | "escape" | "ambient-write";
|
||||
readonly path?: JavaScriptSemanticPropertyPath;
|
||||
readonly timing: JavaScriptSemanticEffectTiming;
|
||||
}
|
||||
|
||||
/** Source-ordered effects relevant to one binding or projected member use. */
|
||||
export interface JavaScriptSemanticReferenceEffectsAtSite {
|
||||
readonly bindingId: string | null;
|
||||
readonly resolution: JavaScriptSemanticReference["resolution"];
|
||||
/** Whether alias-aware member and escape effects were collected for this source. */
|
||||
readonly memberEffectsIndexed: boolean;
|
||||
readonly effects: readonly JavaScriptSemanticReferenceEffect[];
|
||||
}
|
||||
|
||||
export interface JavaScriptSemanticRequiredModuleEffect extends JavaScriptSemanticMemberEffect {
|
||||
readonly kind: "write" | "escape";
|
||||
}
|
||||
|
||||
export interface JavaScriptSemanticBindingState {
|
||||
readonly hostOrigin?: "commonjs-wrapper";
|
||||
readonly bindingId: string;
|
||||
readonly scopeId: string;
|
||||
readonly name: string;
|
||||
@@ -72,6 +94,8 @@ export interface JavaScriptSemanticScopeState {
|
||||
readonly location: JavaScriptSemanticScope["location"];
|
||||
bindingsComplete: boolean;
|
||||
readonly bindings: Map<string, JavaScriptSemanticBindingState>;
|
||||
/** Object environment introduced by `with`, active only for its body. */
|
||||
readonly dynamicWithStatement?: t.WithStatement;
|
||||
}
|
||||
|
||||
/** Shared state for semantic collection and evaluation. */
|
||||
@@ -89,6 +113,12 @@ export interface JavaScriptSemanticAnalysisState {
|
||||
/** Scope boundaries and descendant scopes resolved on demand. */
|
||||
readonly scopeByNode: WeakMap<t.Node, JavaScriptSemanticScopeState>;
|
||||
readonly bindingsById: Map<string, JavaScriptSemanticBindingState>;
|
||||
/** Resolved identifier writes retained at their exact source sites. */
|
||||
readonly bindingWrites: Map<string, t.Node[]>;
|
||||
/** Source evidence that Object.prototype may supply a `require` property. */
|
||||
prototypeRequireMutationObserved: boolean | undefined;
|
||||
/** Source evidence that Object.prototype may add inherited enumerable keys. */
|
||||
prototypeEnumerationMayAffect: boolean | undefined;
|
||||
/** Effects on a cached require exports object, independent of local projections. */
|
||||
readonly requiredModuleEffects: Map<
|
||||
string,
|
||||
@@ -132,6 +162,12 @@ export interface JavaScriptBindingIndex {
|
||||
readonly sourceType: t.Program["sourceType"];
|
||||
readonly bindings: JavaScriptSemanticAnalysisState;
|
||||
indexAmbientWritesSteps(): Generator<void, void>;
|
||||
indexMemberMutationFactsSteps(): Generator<void, void>;
|
||||
effectsForReferenceAt(
|
||||
site: t.Node,
|
||||
referenceNode: t.Node,
|
||||
propertyPath?: JavaScriptSemanticPropertyPath,
|
||||
): JavaScriptSemanticReferenceEffectsAtSite;
|
||||
referencesSteps(): Generator<void, readonly JavaScriptSemanticReference[]>;
|
||||
}
|
||||
|
||||
@@ -185,35 +221,18 @@ export const resolveSemanticBindingState = (
|
||||
node: t.Node,
|
||||
name: string,
|
||||
): JavaScriptSemanticBindingState | undefined => {
|
||||
let scope = semanticScopeForNode(state, node);
|
||||
while (scope !== undefined) {
|
||||
const binding = scope.bindings.get(name);
|
||||
if (binding !== undefined) return binding;
|
||||
if (!scope.bindingsComplete) return undefined;
|
||||
scope =
|
||||
scope.parentScopeId === null
|
||||
? undefined
|
||||
: state.scopesById.get(scope.parentScopeId);
|
||||
}
|
||||
return undefined;
|
||||
const lookup = semanticBindingLookupAt(state, node, name);
|
||||
return lookup.kind === "binding" ? lookup.binding : undefined;
|
||||
};
|
||||
|
||||
/** Report whether an omitted binding scope blocks a trustworthy miss. */
|
||||
/** Report whether an omitted or dynamic binding blocks a trustworthy miss. */
|
||||
export const semanticResolutionBlocked = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
node: t.Node,
|
||||
name: string,
|
||||
): boolean => {
|
||||
let scope = semanticScopeForNode(state, node);
|
||||
while (scope !== undefined) {
|
||||
if (scope.bindings.has(name)) return false;
|
||||
if (!scope.bindingsComplete) return true;
|
||||
scope =
|
||||
scope.parentScopeId === null
|
||||
? undefined
|
||||
: state.scopesById.get(scope.parentScopeId);
|
||||
}
|
||||
return false;
|
||||
const lookup = semanticBindingLookupAt(state, node, name);
|
||||
return lookup.kind !== "binding" && lookup.kind !== "ambient";
|
||||
};
|
||||
|
||||
const semanticScopeForNode = (
|
||||
@@ -238,25 +257,232 @@ const semanticScopeForNode = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Resolve one name starting from an explicitly selected lexical scope. */
|
||||
type SemanticBindingLookup =
|
||||
| {
|
||||
readonly kind: "binding" | "uncertain-binding";
|
||||
readonly binding: JavaScriptSemanticBindingState;
|
||||
}
|
||||
| { readonly kind: "ambient" | "uncertain-ambient" | "captured" | "unknown" };
|
||||
|
||||
/** Retain both a dynamic lookup's uncertainty and its possible outer write owner. */
|
||||
export const semanticBindingLookupFromScope = (
|
||||
scope: JavaScriptSemanticScopeState | undefined,
|
||||
name: string,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): SemanticBindingLookup => {
|
||||
let uncertain = false;
|
||||
while (scope !== undefined) {
|
||||
const binding = scope.bindings.get(name);
|
||||
if (binding !== undefined)
|
||||
return { kind: uncertain ? "uncertain-binding" : "binding", binding };
|
||||
if (!scope.bindingsComplete) {
|
||||
if (scope.dynamicWithStatement === undefined) return { kind: "unknown" };
|
||||
const lookup = semanticWithRequireLookup(
|
||||
state,
|
||||
scope.dynamicWithStatement,
|
||||
name,
|
||||
);
|
||||
if (lookup === "captures") return { kind: "captured" };
|
||||
if (lookup === "unknown") uncertain = true;
|
||||
}
|
||||
scope =
|
||||
scope.parentScopeId === null
|
||||
? undefined
|
||||
: state.scopesById.get(scope.parentScopeId);
|
||||
}
|
||||
return { kind: uncertain ? "uncertain-ambient" : "ambient" };
|
||||
};
|
||||
|
||||
/** Return certain and possible binding ownership without discarding uncertainty. */
|
||||
export const semanticBindingLookupAt = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
node: t.Node,
|
||||
name: string,
|
||||
): SemanticBindingLookup =>
|
||||
semanticBindingLookupFromScope(
|
||||
semanticScopeForNode(state, node),
|
||||
name,
|
||||
state,
|
||||
);
|
||||
|
||||
/** Resolve a certain lexical binding from an explicitly selected scope. */
|
||||
export const resolveSemanticBindingFromScope = (
|
||||
scope: JavaScriptSemanticScopeState,
|
||||
name: string,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptSemanticBindingState | undefined => {
|
||||
let candidate: JavaScriptSemanticScopeState | undefined = scope;
|
||||
while (candidate !== undefined) {
|
||||
const binding = candidate.bindings.get(name);
|
||||
if (binding !== undefined) return binding;
|
||||
if (!candidate.bindingsComplete) return undefined;
|
||||
candidate =
|
||||
candidate.parentScopeId === null
|
||||
? undefined
|
||||
: state.scopesById.get(candidate.parentScopeId);
|
||||
const lookup = semanticBindingLookupFromScope(scope, name, state);
|
||||
return lookup.kind === "binding" ? lookup.binding : undefined;
|
||||
};
|
||||
|
||||
type SemanticWithRequireLookup = "captures" | "skips" | "unknown";
|
||||
const semanticUnscopablesKey = Symbol("semantic-unscopables-key");
|
||||
|
||||
/** Classify only source-visible own `require` entries in a literal with object. */
|
||||
const semanticWithRequireLookup = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
statement: t.WithStatement,
|
||||
name: string,
|
||||
): SemanticWithRequireLookup => {
|
||||
if (name !== "require") return "unknown";
|
||||
const object = unwrapJavaScriptExpression(statement.object).node;
|
||||
if (!t.isObjectExpression(object)) return "unknown";
|
||||
let ownRequire = false;
|
||||
let hasUnknownOwnProperties = false;
|
||||
let prototypeIsNull = false;
|
||||
let unknownPrototype = false;
|
||||
for (const property of object.properties) {
|
||||
if (t.isSpreadElement(property)) {
|
||||
hasUnknownOwnProperties = true;
|
||||
continue;
|
||||
}
|
||||
const key = semanticWithPropertyName(
|
||||
property.key,
|
||||
property.computed,
|
||||
state,
|
||||
);
|
||||
if (key === "require") ownRequire = true;
|
||||
else if (
|
||||
key === "__proto__" &&
|
||||
t.isObjectProperty(property) &&
|
||||
!property.computed &&
|
||||
!property.shorthand
|
||||
) {
|
||||
const prototype = unwrapJavaScriptExpression(property.value).node;
|
||||
if (t.isNullLiteral(prototype)) prototypeIsNull = true;
|
||||
else unknownPrototype = true;
|
||||
} else if (key === null && property.computed) {
|
||||
hasUnknownOwnProperties = true;
|
||||
}
|
||||
}
|
||||
if (ownRequire) {
|
||||
const unscopablesValue = finalSemanticWithSymbolProperty(object, state);
|
||||
if (unscopablesValue === "unknown") return "unknown";
|
||||
if (unscopablesValue === undefined)
|
||||
return prototypeIsNull ||
|
||||
(!unknownPrototype && state.prototypeRequireMutationObserved === false)
|
||||
? "captures"
|
||||
: "unknown";
|
||||
const unscopables = unwrapJavaScriptExpression(unscopablesValue).node;
|
||||
if (!t.isObjectExpression(unscopables))
|
||||
return isSemanticWithPrimitive(unscopables) ? "captures" : "unknown";
|
||||
const entry = finalSemanticWithStringProperty(unscopables, "require");
|
||||
if (entry === "unknown") return "unknown";
|
||||
if (entry === undefined) {
|
||||
const prototype = semanticWithPrototypeKind(unscopables);
|
||||
return prototype === "null" ||
|
||||
(prototype === "ordinary" &&
|
||||
state.prototypeRequireMutationObserved === false)
|
||||
? "captures"
|
||||
: "unknown";
|
||||
}
|
||||
const truthiness = semanticWithStaticTruthiness(entry);
|
||||
return truthiness === true
|
||||
? "skips"
|
||||
: truthiness === false
|
||||
? "captures"
|
||||
: "unknown";
|
||||
}
|
||||
if (hasUnknownOwnProperties || unknownPrototype) return "unknown";
|
||||
if (prototypeIsNull) return "skips";
|
||||
return state.prototypeRequireMutationObserved === false ? "skips" : "unknown";
|
||||
};
|
||||
|
||||
const semanticWithPropertyName = (
|
||||
key: t.Node,
|
||||
computed: boolean,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): string | typeof semanticUnscopablesKey | null => {
|
||||
if (
|
||||
computed &&
|
||||
t.isMemberExpression(key) &&
|
||||
!key.computed &&
|
||||
t.isIdentifier(key.object, { name: "Symbol" }) &&
|
||||
semanticStaticPropertyKey(key.property, false) === "unscopables" &&
|
||||
resolveSemanticBindingState(state, key.object, "Symbol") === undefined &&
|
||||
!semanticResolutionBlocked(state, key.object, "Symbol")
|
||||
)
|
||||
return semanticUnscopablesKey;
|
||||
return semanticStaticPropertyKey(key, computed);
|
||||
};
|
||||
|
||||
const finalSemanticWithSymbolProperty = (
|
||||
object: t.ObjectExpression,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): t.Node | undefined | "unknown" => {
|
||||
for (let index = object.properties.length - 1; index >= 0; index -= 1) {
|
||||
const property = object.properties[index];
|
||||
if (property === undefined || t.isSpreadElement(property)) return "unknown";
|
||||
const name = semanticWithPropertyName(
|
||||
property.key,
|
||||
property.computed,
|
||||
state,
|
||||
);
|
||||
if (name === semanticUnscopablesKey)
|
||||
return t.isObjectProperty(property) ? property.value : "unknown";
|
||||
if (name === null && property.computed) return "unknown";
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const semanticWithPrototypeKind = (
|
||||
object: t.ObjectExpression,
|
||||
): "null" | "ordinary" | "other" => {
|
||||
for (const property of object.properties) {
|
||||
if (
|
||||
t.isObjectProperty(property) &&
|
||||
!property.computed &&
|
||||
!property.shorthand &&
|
||||
t.isIdentifier(property.key, { name: "__proto__" })
|
||||
)
|
||||
return t.isNullLiteral(unwrapJavaScriptExpression(property.value).node)
|
||||
? "null"
|
||||
: "other";
|
||||
}
|
||||
return "ordinary";
|
||||
};
|
||||
|
||||
const finalSemanticWithStringProperty = (
|
||||
object: t.ObjectExpression,
|
||||
sought: string,
|
||||
): t.Node | undefined | "unknown" => {
|
||||
for (let index = object.properties.length - 1; index >= 0; index -= 1) {
|
||||
const property = object.properties[index];
|
||||
if (property === undefined || t.isSpreadElement(property)) return "unknown";
|
||||
const name = semanticStaticPropertyKey(property.key, property.computed);
|
||||
if (name === sought)
|
||||
return t.isObjectProperty(property) ? property.value : "unknown";
|
||||
if (name === null && property.computed) return "unknown";
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const semanticWithStaticTruthiness = (node: t.Node): boolean | undefined => {
|
||||
const value = unwrapJavaScriptExpression(node).node;
|
||||
if (t.isBooleanLiteral(value)) return value.value;
|
||||
if (t.isNullLiteral(value)) return false;
|
||||
if (t.isStringLiteral(value)) return value.value.length > 0;
|
||||
if (t.isNumericLiteral(value))
|
||||
return value.value !== 0 && !Number.isNaN(value.value);
|
||||
if (
|
||||
t.isObjectExpression(value) ||
|
||||
t.isArrayExpression(value) ||
|
||||
t.isFunctionExpression(value) ||
|
||||
t.isArrowFunctionExpression(value)
|
||||
)
|
||||
return true;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const isSemanticWithPrimitive = (node: t.Node): boolean =>
|
||||
t.isNullLiteral(node) ||
|
||||
t.isBooleanLiteral(node) ||
|
||||
t.isStringLiteral(node) ||
|
||||
t.isNumericLiteral(node) ||
|
||||
t.isBigIntLiteral(node) ||
|
||||
(t.isUnaryExpression(node, { operator: "-" }) &&
|
||||
(t.isNumericLiteral(node.argument) || t.isBigIntLiteral(node.argument)));
|
||||
|
||||
/** Select the function/program owner for a var declaration. */
|
||||
export const semanticVariableScope = (
|
||||
scope: JavaScriptSemanticScopeState,
|
||||
@@ -294,6 +520,16 @@ export const isUnmodifiedCommonJsWrapperBinding = (
|
||||
name: string,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
const binding = commonJsWrapperBindingDeclaration(node, name, state);
|
||||
return binding !== undefined && binding.initializers.length === 0;
|
||||
};
|
||||
|
||||
/** Program `var` redeclarations retain wrapper identity until a source write. */
|
||||
export const commonJsWrapperBindingDeclaration = (
|
||||
node: t.Node,
|
||||
name: string,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptSemanticBindingState | undefined => {
|
||||
// Parser sourceType separates explicit modules from script/CommonJS source;
|
||||
// specifier resolution mode does not establish that distinction.
|
||||
if (
|
||||
@@ -301,13 +537,13 @@ export const isUnmodifiedCommonJsWrapperBinding = (
|
||||
!["module", "exports", "require"].includes(name) ||
|
||||
!t.isIdentifier(node, { name })
|
||||
)
|
||||
return false;
|
||||
return undefined;
|
||||
const binding = resolveSemanticBindingState(state, node, name);
|
||||
if (binding === undefined || binding.initializers.length !== 0) return false;
|
||||
if (binding === undefined) return undefined;
|
||||
const scope = state.scopesById.get(binding.scopeId);
|
||||
return (
|
||||
scope?.kind === "program" &&
|
||||
return scope?.kind === "program" &&
|
||||
binding.declarationKinds.size > 0 &&
|
||||
[...binding.declarationKinds].every((kind) => kind === "var")
|
||||
);
|
||||
? binding
|
||||
: undefined;
|
||||
};
|
||||
@@ -1,256 +1,99 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import { semanticStaticPropertyKey } from "./javascript/javascriptAstValues.js";
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
semanticWriteSiteTarget,
|
||||
semanticWriteTargets,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascript/javascriptAstValues.js";
|
||||
import { analyzeParsedJavaScriptReferences } from "./javascript/javascriptSemanticAnalysis.js";
|
||||
indexJavaScriptBindingsWithReceiversSteps,
|
||||
type JavaScriptReceiverIndex,
|
||||
} from "./javascript/javascriptSemanticAnalysis.js";
|
||||
import { semanticRequireLoaderAt } from "./javascript/javascriptSemanticRequireOrigin.js";
|
||||
import {
|
||||
semanticBindingId,
|
||||
semanticScopeId,
|
||||
} from "./javascript/javascriptSemanticState.js";
|
||||
import { traverseJavaScriptAst } from "./javascript/javascriptSemanticTraversal.js";
|
||||
import type { JavaScriptSemanticReference } from "./javascript/javascriptSemanticIr.js";
|
||||
completeSemanticSteps,
|
||||
traverseJavaScriptAst,
|
||||
} from "./javascript/javascriptSemanticTraversal.js";
|
||||
import {
|
||||
isExplicitModuleSourcePath,
|
||||
type ParsedJavaScriptSource,
|
||||
} from "./javascript/javascriptSourceParser.js";
|
||||
|
||||
/**
|
||||
* Keep calls that resolve either to Node's CommonJS wrapper loader or to a
|
||||
* direct ESM `createRequire(import.meta.url)` binding. The shared lexical
|
||||
* analyzer supplies binding ownership; this boundary accounts for the two
|
||||
* Node loader bindings without evaluating arbitrary values.
|
||||
*/
|
||||
/** Recover source dependencies using the shared loader and mutation owners. */
|
||||
export const unshadowedReferenceSourceRequireCalls = (
|
||||
file: Pick<ParsedJavaScriptSource, "program">,
|
||||
path: string,
|
||||
): ReadonlySet<t.CallExpression> => {
|
||||
const references = new Map(
|
||||
analyzeParsedJavaScriptReferences(file).map((reference) => [
|
||||
`${reference.location.start.line}:${reference.location.start.column}`,
|
||||
reference,
|
||||
]),
|
||||
// Unmarked paths depend on package scope or compiler options. Preserve their
|
||||
// candidate CommonJS edges; explicit module paths have no wrapper loader.
|
||||
const wrapperMode = commonJsWrapperMode(path);
|
||||
const index = completeSemanticSteps(
|
||||
indexJavaScriptBindingsWithReceiversSteps(
|
||||
file.program,
|
||||
wrapperMode === "absent" ? undefined : "commonjs-wrapper",
|
||||
),
|
||||
);
|
||||
const programRequireBinding = semanticBindingId(
|
||||
semanticScopeId("program", file.program),
|
||||
"require",
|
||||
);
|
||||
// Syntax alone cannot establish Node's runtime mode: `.js` depends on its
|
||||
// package scope, and `.ts` may be compiled with a CommonJS module target.
|
||||
// Keep unmarked paths runtime-unknown and preserve candidate require edges.
|
||||
const allowsUnboundRequire = !isExplicitModuleSourcePath(path);
|
||||
const hasTopLevelVarRequire =
|
||||
allowsUnboundRequire && hasUninitializedTopLevelVarRequire(file.program);
|
||||
const createRequireBinding = directCreateRequireBinding(
|
||||
file.program,
|
||||
references,
|
||||
);
|
||||
const requireLoaderWasWritten = [...references.values()].some(
|
||||
(reference) =>
|
||||
reference.name === "require" &&
|
||||
reference.role === "write" &&
|
||||
(reference.resolution === "unbound" ||
|
||||
reference.bindingId === programRequireBinding),
|
||||
);
|
||||
const isLoaderReference = (identifier: t.Identifier): boolean => {
|
||||
const start = identifier.loc?.start;
|
||||
const reference =
|
||||
start === undefined
|
||||
? undefined
|
||||
: references.get(`${start.line}:${start.column}`);
|
||||
return (
|
||||
(allowsUnboundRequire && reference?.resolution === "unbound") ||
|
||||
(reference?.resolution === "resolved" &&
|
||||
reference.bindingId === programRequireBinding)
|
||||
);
|
||||
};
|
||||
const loaderMembersIntact = requireLoaderMembersIntact(
|
||||
file.program,
|
||||
isLoaderReference,
|
||||
);
|
||||
const calls = new Set<t.CallExpression>();
|
||||
|
||||
const factoryBindings: ReadonlySet<string> = new Set();
|
||||
const candidates: {
|
||||
readonly call: t.CallExpression;
|
||||
readonly identifier: t.Identifier;
|
||||
}[] = [];
|
||||
traverseJavaScriptAst(file.program, {
|
||||
enter: (node, _parent, readAncestors) => {
|
||||
enter: (node) => {
|
||||
if (!t.isCallExpression(node)) return;
|
||||
const identifier = requireCalleeIdentifier(node.callee);
|
||||
const start = identifier?.loc?.start;
|
||||
if (start === undefined) return;
|
||||
if (identifier === undefined) return;
|
||||
const fact = semanticRequireLoaderAt(
|
||||
node,
|
||||
index.bindings,
|
||||
factoryBindings,
|
||||
);
|
||||
if (
|
||||
readAncestors().some(
|
||||
(ancestor) =>
|
||||
t.isWithStatement(ancestor) && contains(ancestor.body, node),
|
||||
)
|
||||
fact === "create-require" ||
|
||||
(fact === "commonjs-loader" && wrapperMode !== "absent")
|
||||
)
|
||||
return;
|
||||
// `require.resolve` and `require.main` denote Node's members only
|
||||
// while no visible write or escape can have replaced them.
|
||||
if (!t.isIdentifier(node.callee) && !loaderMembersIntact) return;
|
||||
const reference = references.get(`${start.line}:${start.column}`);
|
||||
if (
|
||||
(allowsUnboundRequire &&
|
||||
!requireLoaderWasWritten &&
|
||||
reference?.resolution === "unbound") ||
|
||||
(((hasTopLevelVarRequire && !requireLoaderWasWritten) ||
|
||||
createRequireBinding) &&
|
||||
reference?.resolution === "resolved" &&
|
||||
reference.bindingId === programRequireBinding)
|
||||
)
|
||||
calls.add(node);
|
||||
candidates.push({ call: node, identifier });
|
||||
},
|
||||
});
|
||||
return calls;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the loader's `resolve` and `main` members keep Node's identity. A
|
||||
* write to either member or to a computed member, or `require` used as a
|
||||
* value other than a call or member read (where a mutator or alias can
|
||||
* replace members), leaves that identity unknown.
|
||||
*/
|
||||
const requireLoaderMembersIntact = (
|
||||
program: t.Program,
|
||||
isLoaderReference: (identifier: t.Identifier) => boolean,
|
||||
): boolean => {
|
||||
let intact = true;
|
||||
const isLoaderMemberTarget = (target: t.Node): boolean => {
|
||||
if (!t.isMemberExpression(target) && !t.isOptionalMemberExpression(target))
|
||||
return false;
|
||||
const object = unwrapJavaScriptExpression(target.object).node;
|
||||
if (!t.isIdentifier(object, { name: "require" })) return false;
|
||||
if (!isLoaderReference(object)) return false;
|
||||
const key = semanticStaticPropertyKey(target.property, target.computed);
|
||||
return key === null || key === "resolve" || key === "main";
|
||||
};
|
||||
traverseJavaScriptAst(program, {
|
||||
enter: (node, parent, readAncestors) => {
|
||||
if (!intact) return;
|
||||
const site = semanticWriteSiteTarget(node);
|
||||
if (site !== null) {
|
||||
for (const target of semanticWriteTargets(site))
|
||||
if (isLoaderMemberTarget(target)) intact = false;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!t.isIdentifier(node, { name: "require" }) ||
|
||||
parent === null ||
|
||||
!isLoaderReference(node)
|
||||
if (candidates.some(({ call }) => !t.isIdentifier(call.callee)))
|
||||
completeSemanticSteps(index.indexMemberMutationFactsSteps());
|
||||
return new Set(
|
||||
candidates
|
||||
.filter(
|
||||
({ call, identifier }) =>
|
||||
t.isIdentifier(call.callee) ||
|
||||
!hasPriorMemberMutation(index, call, identifier),
|
||||
)
|
||||
return;
|
||||
// Inert parentheses and TypeScript wrappers do not change how the
|
||||
// loader value is used.
|
||||
let used: t.Node = node;
|
||||
let user: t.Node | undefined = parent;
|
||||
const read = readAncestors();
|
||||
const ancestors = read.at(-1) === parent ? read : [...read, parent];
|
||||
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
||||
user = ancestors[index];
|
||||
if (
|
||||
user === undefined ||
|
||||
unwrapJavaScriptExpression(user).node !== node
|
||||
)
|
||||
break;
|
||||
used = user;
|
||||
}
|
||||
const usedAsLoader =
|
||||
((t.isCallExpression(user) || t.isOptionalCallExpression(user)) &&
|
||||
user.callee === used) ||
|
||||
((t.isMemberExpression(user) || t.isOptionalMemberExpression(user)) &&
|
||||
user.object === used) ||
|
||||
t.isUnaryExpression(user, { operator: "typeof" });
|
||||
if (!usedAsLoader) intact = false;
|
||||
},
|
||||
});
|
||||
return intact;
|
||||
};
|
||||
|
||||
const contains = (container: t.Node, node: t.Node): boolean =>
|
||||
typeof container.start === "number" &&
|
||||
typeof container.end === "number" &&
|
||||
typeof node.start === "number" &&
|
||||
typeof node.end === "number" &&
|
||||
container.start <= node.start &&
|
||||
node.end <= container.end;
|
||||
|
||||
const hasUninitializedTopLevelVarRequire = (program: t.Program): boolean => {
|
||||
let found = false;
|
||||
let initialized = false;
|
||||
traverseJavaScriptAst(program, {
|
||||
enter: (node, _parent, readAncestors) => {
|
||||
if (!t.isVariableDeclaration(node, { kind: "var" })) return;
|
||||
if (
|
||||
readAncestors().some(
|
||||
(ancestor) => t.isFunction(ancestor) || t.isStaticBlock(ancestor),
|
||||
)
|
||||
)
|
||||
return;
|
||||
for (const declaration of node.declarations) {
|
||||
if (!Object.hasOwn(t.getBindingIdentifiers(declaration.id), "require"))
|
||||
continue;
|
||||
found = true;
|
||||
initialized ||= declaration.init !== null;
|
||||
}
|
||||
},
|
||||
});
|
||||
return found && !initialized;
|
||||
};
|
||||
|
||||
const directCreateRequireBinding = (
|
||||
program: t.Program,
|
||||
references: ReadonlyMap<string, JavaScriptSemanticReference>,
|
||||
): boolean => {
|
||||
const programScope = semanticScopeId("program", program);
|
||||
const importedNames = new Set<string>();
|
||||
for (const declaration of program.body) {
|
||||
if (
|
||||
!t.isImportDeclaration(declaration) ||
|
||||
(declaration.source.value !== "node:module" &&
|
||||
declaration.source.value !== "module")
|
||||
)
|
||||
continue;
|
||||
for (const specifier of declaration.specifiers) {
|
||||
if (
|
||||
t.isImportSpecifier(specifier) &&
|
||||
t.isIdentifier(specifier.imported, { name: "createRequire" })
|
||||
)
|
||||
importedNames.add(specifier.local.name);
|
||||
}
|
||||
}
|
||||
|
||||
return program.body.some(
|
||||
(statement) =>
|
||||
t.isVariableDeclaration(statement, { kind: "const" }) &&
|
||||
statement.declarations.some((declaration) => {
|
||||
if (
|
||||
!t.isIdentifier(declaration.id, { name: "require" }) ||
|
||||
!t.isCallExpression(declaration.init) ||
|
||||
!t.isIdentifier(declaration.init.callee) ||
|
||||
!importedNames.has(declaration.init.callee.name) ||
|
||||
!isImportMetaUrl(declaration.init.arguments[0])
|
||||
)
|
||||
return false;
|
||||
const start = declaration.init.callee.loc?.start;
|
||||
if (start === undefined) return false;
|
||||
const reference = references.get(`${start.line}:${start.column}`);
|
||||
return (
|
||||
reference?.resolution === "resolved" &&
|
||||
reference.bindingId ===
|
||||
semanticBindingId(programScope, declaration.init.callee.name)
|
||||
);
|
||||
}),
|
||||
.map(({ call }) => call),
|
||||
);
|
||||
};
|
||||
|
||||
const isImportMetaUrl = (node: t.Node | undefined): boolean =>
|
||||
t.isMemberExpression(node) &&
|
||||
!node.computed &&
|
||||
t.isIdentifier(node.property, { name: "url" }) &&
|
||||
t.isMetaProperty(node.object) &&
|
||||
t.isIdentifier(node.object.meta, { name: "import" }) &&
|
||||
t.isIdentifier(node.object.property, { name: "meta" });
|
||||
type CommonJsWrapperMode = "present" | "absent" | "unknown";
|
||||
|
||||
const commonJsWrapperMode = (path: string): CommonJsWrapperMode =>
|
||||
path.endsWith(".cjs")
|
||||
? "present"
|
||||
: path.endsWith(".mjs")
|
||||
? "absent"
|
||||
: isExplicitModuleSourcePath(path)
|
||||
? "absent"
|
||||
: "unknown";
|
||||
|
||||
const hasPriorMemberMutation = (
|
||||
index: JavaScriptReceiverIndex,
|
||||
site: t.CallExpression,
|
||||
identifier: t.Identifier,
|
||||
): boolean => {
|
||||
const callee = site.callee;
|
||||
const property = t.isMemberExpression(callee)
|
||||
? semanticStaticPropertyKey(callee.property, callee.computed)
|
||||
: undefined;
|
||||
if (property === undefined || property === null) return true;
|
||||
const facts = index.effectsForReferenceAt(site, identifier, [property]);
|
||||
return (
|
||||
!facts.memberEffectsIndexed ||
|
||||
facts.effects.some(
|
||||
({ kind, timing }) =>
|
||||
(kind === "member-write" || kind === "escape") && timing !== "after",
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const requireCalleeIdentifier = (
|
||||
callee: t.Node | null | undefined,
|
||||
|
||||
@@ -120,6 +120,162 @@ it("resolves require calls by lexical binding and source mode", async () => {
|
||||
]);
|
||||
});
|
||||
|
||||
const dynamicLoaderSources: Record<string, string> = {
|
||||
"active-with.cjs": [
|
||||
"with ({ require: null }) { require = () => 'local'; }",
|
||||
"console.log(require('./loaded.cjs'));",
|
||||
].join("\n"),
|
||||
"block-local-with.cjs": [
|
||||
"function patch() {",
|
||||
" { let require = () => 'before'; with ({}) { require = () => 'local'; } }",
|
||||
"}",
|
||||
"patch();",
|
||||
"console.log(require('./loaded.cjs'));",
|
||||
].join("\n"),
|
||||
"for-in-empty-primitive.cjs": [
|
||||
"for (var require in { __proto__: 0 }) {}",
|
||||
"console.log(require('./loaded.cjs'));",
|
||||
].join("\n"),
|
||||
"for-in-nonempty.cjs": [
|
||||
"for (var require in { local: true }) {}",
|
||||
"try { require('./throwing.cjs'); }",
|
||||
"catch (error) {",
|
||||
" if (!(error instanceof TypeError)) throw error;",
|
||||
" console.log('replaced');",
|
||||
"}",
|
||||
].join("\n"),
|
||||
"for-of-empty.cjs": [
|
||||
"for (var require of []) {}",
|
||||
"console.log(require('./loaded.cjs'));",
|
||||
].join("\n"),
|
||||
"for-of-nonempty.cjs": [
|
||||
"for (var require of [() => 'local']) {}",
|
||||
"console.log(require('./throwing.cjs'));",
|
||||
].join("\n"),
|
||||
"function-parameter-with.cjs": [
|
||||
"function patch(require) { with ({}) { require = () => 'local'; } }",
|
||||
"patch(() => 'before');",
|
||||
"console.log(require('./loaded.cjs'));",
|
||||
].join("\n"),
|
||||
"inactive-with-expression.cjs": [
|
||||
"with ({ require: (() => {",
|
||||
" with ({}) { require = () => 'local'; }",
|
||||
" return null;",
|
||||
"})() }) {}",
|
||||
"console.log(require('./throwing.cjs'));",
|
||||
].join("\n"),
|
||||
"ordered-write.cjs": [
|
||||
"require('./ordered-before.cjs');",
|
||||
"with ({}) { require = () => 'local'; }",
|
||||
"console.log(require('./ordered-after.cjs'));",
|
||||
].join("\n"),
|
||||
"primitive-unscopables.cjs": [
|
||||
"with ({ require: null, [Symbol.unscopables]: null }) {",
|
||||
" require = () => 'local';",
|
||||
"}",
|
||||
"console.log(require('./loaded.cjs'));",
|
||||
].join("\n"),
|
||||
"object-unscopables.cjs": [
|
||||
"with ({ require: null, [Symbol.unscopables]: { require: true } }) {",
|
||||
" require = () => 'local';",
|
||||
"}",
|
||||
"console.log(require('./throwing.cjs'));",
|
||||
].join("\n"),
|
||||
"with-write.cjs": [
|
||||
"with ({}) { require = () => 'local'; }",
|
||||
"console.log(require('./throwing.cjs'));",
|
||||
].join("\n"),
|
||||
"assignment-rhs.cjs":
|
||||
"var require; require = require('./loaded.cjs'); console.log(require);",
|
||||
"for-of-sparse.cjs":
|
||||
"for (var require of [,]) {} try { require('./throwing.cjs'); } catch (error) { if (!(error instanceof TypeError)) throw error; console.log('replaced'); }",
|
||||
"for-in-inherited.cjs":
|
||||
"for (var require in { __proto__: { item: true } }) {} try { require('./throwing.cjs'); } catch (error) { if (!(error instanceof TypeError)) throw error; console.log('replaced'); }",
|
||||
"prototype-read-only.cjs":
|
||||
"Object.setPrototypeOf({}, Object.prototype); for (var require in { __proto__: 0 }) {} console.log(require('./loaded.cjs'));",
|
||||
"prototype-alias.cjs":
|
||||
"const prototype = Object.prototype; prototype.item = true; for (var require in {}) {} try { require('./throwing.cjs'); } catch (error) { if (!(error instanceof TypeError)) throw error; console.log('replaced'); }",
|
||||
"array-iterator.cjs":
|
||||
"Array.prototype[Symbol.iterator] = function* () { yield () => 'local'; }; for (var require of []) {} console.log(require('./throwing.cjs'));",
|
||||
"string-unscopables.cjs":
|
||||
"with ({ require: null, ['@@unscopables']: { require: true } }) { require = () => 'local'; } console.log(require('./loaded.cjs'));",
|
||||
"inherited-unscopables.cjs":
|
||||
"with ({ require: null, [Symbol.unscopables]: { __proto__: { require: true } } }) { require = () => 'local'; } console.log(require('./throwing.cjs'));",
|
||||
"prototype-unscopables.cjs":
|
||||
"Object.prototype[Symbol.unscopables] = { require: true }; with ({ require: null }) { require = () => 'local'; } console.log(require('./throwing.cjs'));",
|
||||
"loaded.cjs": "console.log('loaded');\nmodule.exports = 'real';\n",
|
||||
"throwing.cjs": "throw new Error('A shadowed require must not load me');\n",
|
||||
"ordered-before.cjs": "console.log('before-loaded');\n",
|
||||
"ordered-after.cjs":
|
||||
"throw new Error('The replaced loader must not load me');\n",
|
||||
};
|
||||
|
||||
it("keeps require edges according to the loader at each source site", async () => {
|
||||
const root = await createTestTempDirectory("rea-reference-require-dynamic-");
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(dynamicLoaderSources).map(([path, source]) =>
|
||||
writeFile(join(root, path), source),
|
||||
),
|
||||
);
|
||||
|
||||
const runNode = promisify(execFile);
|
||||
for (const [path, stdout] of [
|
||||
["assignment-rhs.cjs", "loaded\nreal\n"],
|
||||
["for-of-sparse.cjs", "replaced\n"],
|
||||
["for-in-inherited.cjs", "replaced\n"],
|
||||
["prototype-read-only.cjs", "loaded\nreal\n"],
|
||||
["prototype-alias.cjs", "replaced\n"],
|
||||
["array-iterator.cjs", "local\n"],
|
||||
["string-unscopables.cjs", "loaded\nreal\n"],
|
||||
["inherited-unscopables.cjs", "local\n"],
|
||||
["prototype-unscopables.cjs", "local\n"],
|
||||
["active-with.cjs", "loaded\nreal\n"],
|
||||
["block-local-with.cjs", "loaded\nreal\n"],
|
||||
["for-in-empty-primitive.cjs", "loaded\nreal\n"],
|
||||
["for-in-nonempty.cjs", "replaced\n"],
|
||||
["for-of-empty.cjs", "loaded\nreal\n"],
|
||||
["for-of-nonempty.cjs", "local\n"],
|
||||
["function-parameter-with.cjs", "loaded\nreal\n"],
|
||||
["inactive-with-expression.cjs", "local\n"],
|
||||
["ordered-write.cjs", "before-loaded\nlocal\n"],
|
||||
["primitive-unscopables.cjs", "loaded\nreal\n"],
|
||||
["object-unscopables.cjs", "local\n"],
|
||||
["with-write.cjs", "local\n"],
|
||||
] as const) {
|
||||
const executed = await runNode(process.execPath, [join(root, path)], {
|
||||
cwd: root,
|
||||
timeout: 5_000,
|
||||
});
|
||||
expect(executed.stdout).toBe(stdout);
|
||||
expect(executed.stderr).toBe("");
|
||||
}
|
||||
|
||||
const result = await importReferenceSource({
|
||||
root,
|
||||
caller: "reference-require-dynamic-test",
|
||||
policy: { secretPatterns: [] },
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
expect(
|
||||
result.value.relationships
|
||||
.filter(({ kind }) => kind === "requires")
|
||||
.map(({ from_path, to }) => [from_path, to])
|
||||
.sort((left, right) => (left[0] ?? "").localeCompare(right[0] ?? "")),
|
||||
).toEqual([
|
||||
["active-with.cjs", "loaded.cjs"],
|
||||
["assignment-rhs.cjs", "loaded.cjs"],
|
||||
["block-local-with.cjs", "loaded.cjs"],
|
||||
["for-in-empty-primitive.cjs", "loaded.cjs"],
|
||||
["for-of-empty.cjs", "loaded.cjs"],
|
||||
["function-parameter-with.cjs", "loaded.cjs"],
|
||||
["ordered-write.cjs", "ordered-before.cjs"],
|
||||
["primitive-unscopables.cjs", "loaded.cjs"],
|
||||
["prototype-read-only.cjs", "loaded.cjs"],
|
||||
["string-unscopables.cjs", "loaded.cjs"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not attribute overwritten require members to Node's resolver", async () => {
|
||||
const root = await createTestTempDirectory("rea-reference-require-member-");
|
||||
const sources: Record<string, string> = {
|
||||
|
||||
Reference in new issue
Block a user