* fix(javascript): resolve module-level Electron options in functions * fix(javascript): limit module option ancestry walk * fix(javascript): preserve uncertain captured argument ordering * fix(javascript): retain hoisted activation uncertainty * fix(javascript): retain function ownership through loops * fix(javascript): account for classic global activations * fix(javascript): bind first activations to lexical identities * fix(javascript): retain uncertain global activation timing * fix(javascript): isolate receiver facts and share computed reads * fix(javascript): retain projected and opaque global activations * fix(javascript): refresh activation bounds as opaque effects grow * fix(ci): format website watch directories without changing configuration * fix(javascript): share source reads across opaque-effect updates * fix(javascript): retain constructed-code global activation bounds * fix(javascript): share runtime activation and receiver facts * fix(javascript): reuse pattern reads and function positions * fix(javascript): isolate copied paths and preserve runtime reads * style(javascript): format migrated reference imports * fix(javascript): normalize constructor activation and receiver effects * fix(javascript): retain aliased receiver timing and lexical global this * fix(javascript): preserve effect-free receiver identity across activations * fix(javascript): retain outer activation and prototype effects * fix(javascript): index implicit reads without deep path rescans * fix(javascript): retain aliased prototype and parameter read uncertainty * fix(javascript): index reflected reads through intrinsic invocation facts * fix(javascript): retain unresolved global activation reads * chore: refresh PR branch after review fix publication * fix(javascript): retain projected receiver reads and cache method facts * fix(javascript): retain inherited and implicit activation reads * fix(javascript): retain caller-owned this uncertainty * fix(javascript): retain reflected reads with unresolved spread arguments * fix(javascript): preserve enclosing activation for decorators * fix(javascript): track intrinsic literal prototype effects * fix(javascript): retain catch activation from implicit reads * perf(javascript): reuse effective keys for projected reads * fix(javascript): retain possible caller-owned ambient effects
370 lines
12 KiB
TypeScript
370 lines
12 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
import * as t from "@babel/types";
|
|
|
|
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
|
|
import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js";
|
|
|
|
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
|
|
|
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
|
import { semanticStaticPropertyKey } from "./javascriptAstValues.js";
|
|
import { stringValue } from "./javascriptStaticAnalysisHelpers.js";
|
|
import {
|
|
withoutUnusedFactoryDefaults,
|
|
type JavaScriptBundlerFactoryRoles,
|
|
} from "./javascriptBundlerFactoryRoles.js";
|
|
import {
|
|
resolveSemanticBindingState,
|
|
type JavaScriptBindingIndex,
|
|
} from "./javascriptSemanticState.js";
|
|
|
|
/** Static CommonJS export names. */
|
|
export interface StaticExports {
|
|
readonly values: readonly string[];
|
|
/** Export-shaped writes whose receiver role could not be resolved. */
|
|
readonly unknown: number;
|
|
}
|
|
|
|
/** Derive a rename-resistant syntax fingerprint without code execution. */
|
|
export const fingerprintJavaScriptAst = (node: t.Node): string => {
|
|
const tokens: string[] = [];
|
|
traverseJavaScriptAst(node, {
|
|
enter: (current) => {
|
|
tokens.push(...semanticTokens(current));
|
|
return undefined;
|
|
},
|
|
});
|
|
return createHash("sha256").update(JSON.stringify(tokens)).digest("hex");
|
|
};
|
|
|
|
/** Collect statically declared CommonJS/bundler export names from one factory. */
|
|
export const collectJavaScriptExports = (
|
|
node: t.Node,
|
|
factory?: {
|
|
readonly roles: Pick<
|
|
JavaScriptBundlerFactoryRoles,
|
|
"module" | "exports" | "loader" | "unusedDefaults"
|
|
>;
|
|
readonly index: Pick<
|
|
JavaScriptBindingIndex,
|
|
"bindings" | "effectsForReferenceWithinActivationAt"
|
|
>;
|
|
},
|
|
): StaticExports => {
|
|
const roles = factory?.roles;
|
|
const index = factory?.index;
|
|
const bindings = index?.bindings;
|
|
const exports = new Set<string>();
|
|
let unknown = 0;
|
|
const bindingId = (reference: t.Identifier): string | undefined =>
|
|
bindings === undefined
|
|
? undefined
|
|
: resolveSemanticBindingState(bindings, reference, reference.name)
|
|
?.bindingId;
|
|
const identity = (
|
|
parameters: readonly (t.Identifier | undefined)[],
|
|
fallback: string,
|
|
) => {
|
|
const ids = new Set(
|
|
parameters.flatMap((parameter) => {
|
|
const id = parameter === undefined ? undefined : bindingId(parameter);
|
|
return id === undefined ? [] : [id];
|
|
}),
|
|
);
|
|
return (reference: t.Node | null): ReceiverRole => {
|
|
if (!t.isIdentifier(reference)) return undefined;
|
|
if (roles === undefined)
|
|
return reference.name === fallback ? "role" : undefined;
|
|
if (bindings === undefined) return undefined;
|
|
const binding = resolveSemanticBindingState(
|
|
bindings,
|
|
reference,
|
|
reference.name,
|
|
);
|
|
if (binding === undefined || !ids.has(binding.bindingId))
|
|
return undefined;
|
|
// The calling convention owns the parameter's entry value, not every
|
|
// later value assigned to the same lexical binding.
|
|
const read = semanticBindingAtRead(
|
|
withoutUnusedFactoryDefaults(binding, roles),
|
|
reference,
|
|
bindings,
|
|
);
|
|
if (read.status !== "ordered") return "unresolved";
|
|
return read.binding.initializers.length === 0 &&
|
|
read.binding.referenceInitializers.length === 0
|
|
? "role"
|
|
: undefined;
|
|
};
|
|
};
|
|
const isExportsAlias = identity(roles?.exports ?? [], "exports");
|
|
const isModuleAlias = identity([roles?.module], "module");
|
|
const isLoaderAlias = identity([roles?.loader], "__webpack_require__");
|
|
const receivers: ExportReceivers = {
|
|
isExportsAlias,
|
|
isModuleAlias,
|
|
unresolved: () => {
|
|
unknown += 1;
|
|
},
|
|
};
|
|
traverseJavaScriptAst(node, {
|
|
// A default the calling convention never runs declares no exports.
|
|
skip: (current) => roles?.unusedDefaults.has(current) === true,
|
|
enter: (current) => {
|
|
if (t.isAssignmentExpression(current))
|
|
collectAssignmentExports(
|
|
current.left,
|
|
current.right,
|
|
exports,
|
|
receivers,
|
|
);
|
|
if (t.isCallExpression(current)) {
|
|
const intrinsic =
|
|
bindings !== undefined &&
|
|
isUnmodifiedGlobalReference(current.callee, bindings, [
|
|
"Object",
|
|
"defineProperty",
|
|
]);
|
|
collectCallExports(current, exports, receivers, intrinsic, (root) => {
|
|
const loader = isLoaderAlias(root);
|
|
if (loader !== "role" || root === null) return loader;
|
|
if (index === undefined) return "role";
|
|
const facts = index.effectsForReferenceWithinActivationAt(
|
|
current,
|
|
root,
|
|
["d"],
|
|
);
|
|
return facts.memberEffectsIndexed &&
|
|
!facts.effects.some(
|
|
(effect) =>
|
|
effect.kind === "member-write" && effect.timing !== "after",
|
|
)
|
|
? "role"
|
|
: undefined;
|
|
});
|
|
}
|
|
return undefined;
|
|
},
|
|
});
|
|
return { values: [...exports].sort(compareUnicodeCodePoints), unknown };
|
|
};
|
|
|
|
/**
|
|
* `role` holds the parameter's entry value; `unresolved` reads its binding
|
|
* where write order is unknown, so the write may or may not be an export.
|
|
*/
|
|
type ReceiverRole = "role" | "unresolved" | undefined;
|
|
|
|
interface ExportReceivers {
|
|
readonly isExportsAlias: (root: t.Node | null) => ReceiverRole;
|
|
readonly isModuleAlias: (root: t.Node | null) => ReceiverRole;
|
|
/** Record an export-shaped write whose receiver could not be resolved. */
|
|
readonly unresolved: () => void;
|
|
}
|
|
|
|
const semanticTokens = (node: t.Node): string[] => {
|
|
const tokens = [`node:${node.type}`];
|
|
if (t.isStringLiteral(node)) tokens.push(`string:${node.value}`);
|
|
else if (t.isBigIntLiteral(node)) tokens.push(`bigint:${String(node.value)}`);
|
|
else if (t.isDirectiveLiteral(node)) tokens.push(`directive:${node.value}`);
|
|
else if (t.isNumericLiteral(node))
|
|
tokens.push(`number:${String(node.value)}`);
|
|
else if (t.isBooleanLiteral(node))
|
|
tokens.push(`boolean:${String(node.value)}`);
|
|
else if (t.isRegExpLiteral(node))
|
|
tokens.push(`regexp:${node.pattern}/${node.flags}`);
|
|
else if (t.isTemplateElement(node))
|
|
tokens.push(`template:${JSON.stringify(node.value)}`);
|
|
else if (
|
|
t.isBinaryExpression(node) ||
|
|
t.isLogicalExpression(node) ||
|
|
t.isUnaryExpression(node) ||
|
|
t.isUpdateExpression(node) ||
|
|
t.isAssignmentExpression(node)
|
|
)
|
|
tokens.push(`operator:${node.operator}`);
|
|
if (t.isUpdateExpression(node)) tokens.push(`prefix:${String(node.prefix)}`);
|
|
if (t.isVariableDeclaration(node)) tokens.push(`kind:${node.kind}`);
|
|
if (t.isFunction(node)) {
|
|
tokens.push(`async:${String(node.async)}`);
|
|
tokens.push(`generator:${String(node.generator)}`);
|
|
}
|
|
if (t.isObjectMethod(node)) tokens.push(`kind:${node.kind}`);
|
|
if (t.isClassMethod(node) || t.isClassPrivateMethod(node)) {
|
|
tokens.push(`kind:${node.kind}`);
|
|
tokens.push(`static:${String(node.static)}`);
|
|
}
|
|
if (
|
|
t.isClassProperty(node) ||
|
|
t.isClassPrivateProperty(node) ||
|
|
t.isClassAccessorProperty(node)
|
|
)
|
|
tokens.push(`static:${String(node.static)}`);
|
|
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
|
// A computed identifier names a local binding, not a fixed member.
|
|
tokens.push(`computed:${String(node.computed)}`);
|
|
if (!node.computed) {
|
|
const property = semanticStaticPropertyKey(node.property, false);
|
|
if (property !== null) tokens.push(`property:${property}`);
|
|
}
|
|
}
|
|
if (
|
|
t.isObjectProperty(node) ||
|
|
t.isObjectMethod(node) ||
|
|
t.isClassMethod(node) ||
|
|
t.isClassProperty(node) ||
|
|
t.isClassAccessorProperty(node)
|
|
) {
|
|
tokens.push(`computed:${String(node.computed)}`);
|
|
if (!node.computed) {
|
|
const key = semanticStaticPropertyKey(node.key, false);
|
|
if (key !== null) tokens.push(`key:${key}`);
|
|
}
|
|
}
|
|
return tokens;
|
|
};
|
|
|
|
const collectAssignmentExports = (
|
|
left: t.LVal | t.OptionalMemberExpression,
|
|
right: t.Expression,
|
|
output: Set<string>,
|
|
{ isExportsAlias, isModuleAlias, unresolved }: ExportReceivers,
|
|
): void => {
|
|
const path = memberPath(left);
|
|
if (path === null) return;
|
|
const root = memberRoot(left);
|
|
// Rebinding the CommonJS alias does not replace module.exports.
|
|
if (path.length === 1) return;
|
|
const exportsAlias = isExportsAlias(root);
|
|
const moduleAlias =
|
|
exportsAlias === undefined && path[1] === "exports"
|
|
? isModuleAlias(root)
|
|
: undefined;
|
|
if (exportsAlias === "unresolved" || moduleAlias === "unresolved") {
|
|
unresolved();
|
|
return;
|
|
}
|
|
const exportOffset =
|
|
exportsAlias === "role" ? 1 : moduleAlias === "role" ? 2 : null;
|
|
if (exportOffset === null) return;
|
|
if (path.length === exportOffset) {
|
|
if (t.isObjectExpression(right)) collectObjectKeys(right, output);
|
|
else addExport(output, "default");
|
|
return;
|
|
}
|
|
// A CommonJS export assignment exposes the first property below the
|
|
// exports object. Deeper writes mutate that exported value, rather than
|
|
// creating an export whose name is the dotted member path.
|
|
addExport(output, path[exportOffset]!);
|
|
};
|
|
|
|
const collectCallExports = (
|
|
call: t.CallExpression,
|
|
output: Set<string>,
|
|
{ isExportsAlias, isModuleAlias, unresolved }: ExportReceivers,
|
|
intrinsic: boolean,
|
|
isLoaderAlias: (root: t.Node | null) => ReceiverRole,
|
|
): void => {
|
|
const callee = memberPath(call.callee);
|
|
if (callee === null) return;
|
|
const definePropertyCall = intrinsic;
|
|
const declarationCall =
|
|
!definePropertyCall && callee.length === 2 && callee[1] === "d"
|
|
? isLoaderAlias(memberRoot(call.callee))
|
|
: undefined;
|
|
if (!definePropertyCall && declarationCall === undefined) return;
|
|
const targetNode = call.arguments[0];
|
|
const target = memberPath(targetNode);
|
|
const exportsAlias = isExportsAlias(targetNode ?? null);
|
|
const moduleAlias =
|
|
exportsAlias === undefined &&
|
|
target !== null &&
|
|
target.length === 2 &&
|
|
target[1] === "exports"
|
|
? isModuleAlias(memberRoot(targetNode))
|
|
: undefined;
|
|
if (exportsAlias === undefined && moduleAlias === undefined) return;
|
|
if (
|
|
exportsAlias === "unresolved" ||
|
|
moduleAlias === "unresolved" ||
|
|
declarationCall === "unresolved"
|
|
) {
|
|
unresolved();
|
|
return;
|
|
}
|
|
if (definePropertyCall) {
|
|
const name = stringValue(call.arguments[1]);
|
|
if (name !== undefined) addExport(output, name);
|
|
return;
|
|
}
|
|
const declarations = call.arguments[1];
|
|
if (t.isObjectExpression(declarations))
|
|
collectObjectKeys(declarations, output);
|
|
};
|
|
|
|
const memberRoot = (node: t.Node | null | undefined): t.Node | null => {
|
|
let current = node;
|
|
while (
|
|
current !== null &&
|
|
current !== undefined &&
|
|
(t.isMemberExpression(current) || t.isOptionalMemberExpression(current))
|
|
)
|
|
current = current.object;
|
|
return current ?? null;
|
|
};
|
|
|
|
const collectObjectKeys = (
|
|
object: t.ObjectExpression,
|
|
output: Set<string>,
|
|
): void => {
|
|
for (const property of object.properties) {
|
|
if (!t.isObjectProperty(property) && !t.isObjectMethod(property)) continue;
|
|
const name = semanticStaticPropertyKey(property.key, property.computed);
|
|
// In an object literal, a non-computed `__proto__: value` sets the
|
|
// prototype instead of creating an own property. Shorthand, computed,
|
|
// and method forms do create own properties.
|
|
if (
|
|
name !== null &&
|
|
!(
|
|
t.isObjectProperty(property) &&
|
|
!property.computed &&
|
|
!property.shorthand &&
|
|
name === "__proto__"
|
|
)
|
|
)
|
|
addExport(output, name);
|
|
}
|
|
};
|
|
|
|
const addExport = (output: Set<string>, name: string): void => {
|
|
output.add(name);
|
|
};
|
|
|
|
const memberPath = (
|
|
node: t.Node | null | undefined,
|
|
): readonly string[] | null => {
|
|
const properties: string[] = [];
|
|
let current = node;
|
|
while (
|
|
current !== undefined &&
|
|
current !== null &&
|
|
(t.isMemberExpression(current) || t.isOptionalMemberExpression(current))
|
|
) {
|
|
const property = semanticStaticPropertyKey(
|
|
current.property,
|
|
current.computed,
|
|
);
|
|
if (property === null) return null;
|
|
properties.push(property);
|
|
current = current.object;
|
|
}
|
|
if (current === undefined || current === null) return null;
|
|
const root = t.isIdentifier(current)
|
|
? current.name
|
|
: t.isThisExpression(current)
|
|
? "this"
|
|
: null;
|
|
return root === null ? null : [root, ...properties.reverse()];
|
|
};
|