mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
e9fc8fb30c
* fix(javascript): preserve literal percent runtime paths * fix(javascript): ignore resolve prefixes before the final anchor * fix(electron): retain application roots for loadFile declarations * fix(electron): resolve loadFile from enclosing application package * fix(electron): retain outer application root across package scopes * fix(electron): derive application ownership from inventoried packages * fix(electron): retain resolved package entrypoint ownership * fix(electron): preserve competing application ownership * fix(electron): retain invalid and omitted runtime ownership * fix(electron): preserve omitted module ownership and runtime paths * fix(electron): preserve proven roots before confining page paths * fix(electron): retain possible owners behind missing imports * fix(electron): distinguish absent imports and owned page candidates * fix(electron): keep unavailable JSON out of owner gaps * fix(electron): retain exact loader formats and default entry status * fix(electron): retain unavailable entrypoint ownership candidates * test(ghidra): preserve Windows Node probe prerequisites * test(ghidra): assert cancellation during profile shutdown * fix(electron): separate examined imports from frontend ownership * fix(electron): retain unavailable directory entrypoint owners * test(jeb): retain Linux ownership prerequisite in isolation * fix(javascript): retain unchecked loader ownership and literal main paths * fix(javascript): preserve runtime paths and call-site module aliases * fix(javascript): bound module alias traversal and preserve projections * fix(javascript): retain host module and nested artifact path boundaries * fix(javascript): preserve ESM and archive module loader provenance * test(javascript): distinguish unmapped containers from missing exports * fix: retain default module aliases and Electron builtin imports * fix: preserve host file URL and inward ASAR ownership * fix: preserve CommonJS file URL classification in CI fixtures * fix: retain bound module loaders and Windows entrypoint uncertainty * fix: retain ASAR uncertainty and invalidate changed Electron app roots * fix(javascript): retain ownership uncertainty across loader boundaries * fix(javascript): preserve loader sources and bound alias queries * fix(javascript): retain destructured loader sources * fix(javascript): preserve loader uncertainty at resolution boundaries * fix(javascript): preserve callable and package launch ownership * fix(javascript): preserve captured values across member reads * test(process): assert tagged filesystem cancellation failures * fix(javascript): preserve inventory bases and invocation read timing * fix(javascript): preserve ownership across indirect and unread loads * test(javascript): order fixture directory creation on Windows * test(javascript): share ordered filesystem fixture writer * fix(javascript): unify loader provenance and package validation * fix(javascript): preserve captured Node require loader values * fix(javascript): preserve loader invocation authority * fix(javascript): verify generic loader invocation authority * fix(javascript): preserve conditional loader method alternatives * fix(javascript): require proven factories for source association * fix(javascript): honor replaced Electron app path setters * fix(javascript): prove path and assignment source ownership * fix(javascript): retain native delegate eligibility through shared writes * fix(process): settle stale baseline errors after confirmed absence * fix(javascript): preserve assignment and path boundary evidence * fix(javascript): prove source-directory resolve anchors * fix: preserve application path provenance and portable schemas * fix(javascript): retain CommonJS exported setter effects * fix: isolate Linux identity observations and retain native module exports * fix(javascript): preserve published app and helper provenance * fix(javascript): normalize typed stores and native path fixtures
698 lines
25 KiB
TypeScript
698 lines
25 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import {
|
|
semanticStaticPropertyKey,
|
|
unwrapJavaScriptExpression,
|
|
} from "./javascriptAstValues.js";
|
|
import {
|
|
collectJavaScriptIdentityWritesSteps,
|
|
electronMemberPath,
|
|
} from "./javascriptElectronMemberWrites.js";
|
|
import { createSemanticModuleInterop } from "./javascriptModuleInterop.js";
|
|
import {
|
|
isUnshadowedGlobal,
|
|
resolveSemanticBindingState,
|
|
semanticStableInitializer,
|
|
type JavaScriptSemanticAnalysisState,
|
|
} from "./javascriptSemanticState.js";
|
|
import { isImportMetaUrl } from "./javascriptStaticAnalysisHelpers.js";
|
|
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
|
|
import {
|
|
semanticIterationSources,
|
|
type SemanticIterationSource,
|
|
} from "./javascriptSemanticIterationSources.js";
|
|
import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js";
|
|
import {
|
|
semanticCopiedReferencePath,
|
|
semanticReferenceIterationUnknown,
|
|
semanticReferenceSourcesInactive,
|
|
} from "./javascriptSemanticReferenceSources.js";
|
|
|
|
const GLOBAL_OBJECT_NAMES = ["globalThis", "global", "window", "self"];
|
|
|
|
const globalObjectName = (
|
|
node: t.Node,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): string | undefined =>
|
|
GLOBAL_OBJECT_NAMES.find((name) => isUnshadowedGlobal(node, state, name));
|
|
|
|
/** Global self references retain object identity, rather than adding a property. */
|
|
const canonicalAmbientPath = (
|
|
path: readonly (string | null)[],
|
|
): readonly (string | null)[] => {
|
|
if (path[0] !== "global") return path;
|
|
let firstProperty = 1;
|
|
while (GLOBAL_OBJECT_NAMES.includes(path[firstProperty] ?? ""))
|
|
firstProperty += 1;
|
|
return ["global", ...path.slice(firstProperty)];
|
|
};
|
|
|
|
type AmbientPath = readonly (string | null)[];
|
|
|
|
/** Undefined means a cyclic or conflicting projection can select any depth. */
|
|
type AmbientProjection = AmbientPath | undefined;
|
|
|
|
const joinAmbientProjection = (
|
|
left: AmbientProjection,
|
|
right: AmbientProjection,
|
|
): AmbientProjection =>
|
|
left === undefined || right === undefined || left.length !== right.length
|
|
? undefined
|
|
: left.map((key, index) => (key === right[index] ? key : null));
|
|
|
|
interface AmbientReferenceFacts {
|
|
readonly stores: WeakMap<
|
|
t.Node,
|
|
Map<SemanticIterationSource, AmbientProjection>
|
|
>;
|
|
readonly prototypeTarget: (node: t.Node) => t.Node | undefined;
|
|
readonly freshObject: (node: t.Node) => boolean;
|
|
}
|
|
|
|
/**
|
|
* Follow local object identities without equating shallow copies with their
|
|
* sources. Writes select aliases at their capture; uncertain execution order
|
|
* retains possible initializers. Intrinsic reads stay strict.
|
|
*/
|
|
export const createAmbientPathResolver = (
|
|
state: JavaScriptSemanticAnalysisState,
|
|
possibleWrites: boolean,
|
|
facts?: AmbientReferenceFacts,
|
|
) => {
|
|
const cached = new WeakMap<t.Node, readonly AmbientPath[]>();
|
|
const returnedReferences = createSemanticReturnedReferences(state);
|
|
return (
|
|
node: t.Node,
|
|
capture?: t.Node,
|
|
escapedContents = false,
|
|
projection: AmbientProjection = [],
|
|
localIdentity?: (node: t.Node, path: AmbientProjection) => void,
|
|
): readonly AmbientPath[] => {
|
|
const prior = possibleWrites ? undefined : cached.get(node);
|
|
if (prior !== undefined) return prior;
|
|
const result = new Map<string, AmbientPath>();
|
|
// The caller's write site selects initializers for the written target.
|
|
// Reads reached through an initializer occur at their own position.
|
|
const pending: {
|
|
node: t.Node;
|
|
suffix: AmbientProjection;
|
|
readsAtSelf?: true;
|
|
}[] = [{ node, suffix: projection }];
|
|
// Join projections at their producing expression, rather than enumerating
|
|
// alias histories. Each key can only widen to a wildcard, and conflicting
|
|
// depths widen to unknown, so diamonds and cycles have finite state.
|
|
const expanded = new WeakMap<t.Node, AmbientProjection>();
|
|
const retain = (path: AmbientPath) => {
|
|
let canonical = canonicalAmbientPath(path);
|
|
for (;;) {
|
|
result.set(JSON.stringify(canonical), canonical);
|
|
if (
|
|
possibleWrites &&
|
|
canonical[0] === "global" &&
|
|
canonical[2] === "prototype" &&
|
|
canonical[3] === "constructor"
|
|
) {
|
|
canonical = ["global", canonical[1] ?? null, ...canonical.slice(4)];
|
|
continue;
|
|
}
|
|
// An unknown key may select a global self reference as well as a
|
|
// distinct property. Retain both possible object identities for writes.
|
|
if (
|
|
!possibleWrites ||
|
|
canonical[0] !== "global" ||
|
|
canonical[1] !== null
|
|
)
|
|
break;
|
|
canonical = canonicalAmbientPath(["global", ...canonical.slice(2)]);
|
|
}
|
|
};
|
|
while (pending.length > 0) {
|
|
const current = pending.pop();
|
|
if (current === undefined) break;
|
|
const { root, members } = electronMemberPath(current.node);
|
|
let suffix: AmbientProjection =
|
|
current.suffix === undefined
|
|
? undefined
|
|
: [...members, ...current.suffix];
|
|
if (expanded.has(root)) {
|
|
const previous = expanded.get(root);
|
|
if (previous === undefined) continue;
|
|
if (suffix !== undefined && previous.length === suffix.length) {
|
|
const joined: AmbientPath = previous.map((key, index) =>
|
|
key === suffix?.[index] ? key : null,
|
|
);
|
|
if (joined.every((key, index) => key === previous[index])) continue;
|
|
suffix = joined;
|
|
} else suffix = undefined;
|
|
if (!possibleWrites) {
|
|
cached.set(node, []);
|
|
return [];
|
|
}
|
|
}
|
|
expanded.set(root, suffix);
|
|
// A widened projection is a possible write, never proof of an intrinsic.
|
|
if (suffix === undefined && !possibleWrites) continue;
|
|
const continueWith = (node: t.Node, path: AmbientProjection = suffix) =>
|
|
pending.push({
|
|
node,
|
|
suffix: path,
|
|
...(current.readsAtSelf && { readsAtSelf: true }),
|
|
});
|
|
const continueReturned = (callee: t.Node, path: AmbientProjection) => {
|
|
for (const source of returnedReferences(callee).references)
|
|
continueWith(
|
|
source.node,
|
|
path === undefined
|
|
? undefined
|
|
: [
|
|
...source.projection.map((key) =>
|
|
key === null || typeof key === "object"
|
|
? null
|
|
: String(key),
|
|
),
|
|
...path,
|
|
],
|
|
);
|
|
};
|
|
if (!t.isIdentifier(root) && facts?.freshObject(root)) {
|
|
localIdentity?.(root, suffix);
|
|
for (const [source, path] of facts?.stores.get(root) ?? []) {
|
|
const selected =
|
|
suffix === undefined ||
|
|
path === undefined ||
|
|
path.every(
|
|
(key, index) =>
|
|
index >= suffix.length ||
|
|
key === null ||
|
|
suffix[index] === null ||
|
|
key === suffix[index],
|
|
);
|
|
if (
|
|
!selected ||
|
|
(suffix !== undefined &&
|
|
path !== undefined &&
|
|
suffix.length < path.length &&
|
|
!escapedContents)
|
|
)
|
|
continue;
|
|
const remaining =
|
|
suffix === undefined || path === undefined
|
|
? undefined
|
|
: suffix.slice(path.length);
|
|
pending.push({
|
|
node: source.node,
|
|
readsAtSelf: true,
|
|
suffix:
|
|
remaining === undefined
|
|
? undefined
|
|
: [
|
|
...source.projection.map((key) =>
|
|
key === null || typeof key === "object"
|
|
? null
|
|
: String(key),
|
|
),
|
|
...remaining,
|
|
],
|
|
});
|
|
}
|
|
}
|
|
if (
|
|
t.isMetaProperty(root) &&
|
|
root.meta.name === "import" &&
|
|
root.property.name === "meta"
|
|
) {
|
|
retain(["import-meta", ...(suffix ?? [])]);
|
|
continue;
|
|
}
|
|
if (t.isIdentifier(root)) {
|
|
// The wrapper module object is distinct from globalThis.module. Keep
|
|
// its identity through the same alias/projection owner used for writes.
|
|
if (isUnshadowedGlobal(root, state, "module")) {
|
|
retain(["commonjs-module", ...(suffix ?? [])]);
|
|
continue;
|
|
}
|
|
if (globalObjectName(root, state) !== undefined) {
|
|
retain(["global", ...(suffix ?? [])]);
|
|
continue;
|
|
}
|
|
if (isUnshadowedGlobal(root, state, root.name)) {
|
|
retain(["global", root.name, ...(suffix ?? [])]);
|
|
continue;
|
|
}
|
|
const binding = resolveSemanticBindingState(state, root, root.name);
|
|
if (binding === undefined) continue;
|
|
const stable = possibleWrites
|
|
? undefined
|
|
: semanticStableInitializer(binding, state);
|
|
const readAt = current.readsAtSelf ? root : capture;
|
|
const selected =
|
|
!possibleWrites || readAt === undefined
|
|
? binding
|
|
: semanticBindingAtRead(binding, readAt, state.parentsByNode)
|
|
.binding;
|
|
const initializers = possibleWrites
|
|
? [...selected.initializers, ...selected.referenceInitializers]
|
|
: stable === undefined
|
|
? []
|
|
: [stable];
|
|
for (const initializer of initializers) {
|
|
if (semanticReferenceSourcesInactive(initializer, state)) continue;
|
|
// Pattern assignments share the source selection owner with member
|
|
// effects: copying a container preserves its children, not itself.
|
|
const path = semanticCopiedReferencePath(
|
|
initializer,
|
|
suffix ?? [null],
|
|
escapedContents ? "escape" : "identity",
|
|
);
|
|
if (path === null) continue;
|
|
pending.push({
|
|
node: initializer.node,
|
|
readsAtSelf: true,
|
|
suffix:
|
|
suffix === undefined ||
|
|
semanticReferenceIterationUnknown(initializer, state)
|
|
? undefined
|
|
: path.map((key) =>
|
|
key === null || typeof key === "object"
|
|
? null
|
|
: String(key),
|
|
),
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
if (
|
|
possibleWrites &&
|
|
t.isNewExpression(root) &&
|
|
(suffix?.[0] === "__proto__" || suffix?.[0] === "constructor")
|
|
) {
|
|
continueWith(
|
|
root.callee,
|
|
suffix[0] === "__proto__"
|
|
? ["prototype", ...suffix.slice(1)]
|
|
: suffix.slice(1),
|
|
);
|
|
continue;
|
|
}
|
|
const prototypeTarget = possibleWrites
|
|
? facts?.prototypeTarget(root)
|
|
: undefined;
|
|
if (prototypeTarget !== undefined) {
|
|
continueWith(
|
|
prototypeTarget,
|
|
suffix === undefined ? undefined : ["__proto__", ...suffix],
|
|
);
|
|
continue;
|
|
}
|
|
if (
|
|
possibleWrites &&
|
|
(t.isCallExpression(root) ||
|
|
t.isOptionalCallExpression(root) ||
|
|
t.isNewExpression(root) ||
|
|
t.isTaggedTemplateExpression(root) ||
|
|
(escapedContents && (t.isFunction(root) || t.isClass(root))))
|
|
) {
|
|
continueReturned(
|
|
t.isFunction(root) || t.isClass(root)
|
|
? root
|
|
: t.isTaggedTemplateExpression(root)
|
|
? root.tag
|
|
: root.callee,
|
|
suffix,
|
|
);
|
|
continue;
|
|
}
|
|
if (possibleWrites && t.isConditionalExpression(root)) {
|
|
continueWith(root.consequent);
|
|
continueWith(root.alternate);
|
|
continue;
|
|
}
|
|
if (possibleWrites && t.isLogicalExpression(root)) {
|
|
continueWith(root.left);
|
|
continueWith(root.right);
|
|
continue;
|
|
}
|
|
if (possibleWrites && t.isSequenceExpression(root)) {
|
|
const value = root.expressions.at(-1);
|
|
if (value !== undefined) continueWith(value);
|
|
continue;
|
|
}
|
|
if (
|
|
possibleWrites &&
|
|
t.isAssignmentExpression(root) &&
|
|
root.operator === "="
|
|
) {
|
|
continueWith(root.right);
|
|
continue;
|
|
}
|
|
const key = suffix?.[0];
|
|
// Container escapes reuse the finite worklist, including alias cycles.
|
|
if (escapedContents && suffix?.length === 0) {
|
|
if (t.isObjectExpression(root)) {
|
|
for (const property of root.properties)
|
|
if (t.isSpreadElement(property))
|
|
// A fresh copy exposes selected children, not its source container.
|
|
continueWith(property.argument, [null]);
|
|
else if (
|
|
t.isObjectProperty(property) &&
|
|
t.isExpression(property.value)
|
|
)
|
|
continueWith(property.value, []);
|
|
else if (t.isObjectMethod(property) && property.kind === "get")
|
|
continueReturned(property, []);
|
|
continue;
|
|
}
|
|
if (t.isArrayExpression(root)) {
|
|
for (const element of root.elements)
|
|
if (t.isSpreadElement(element)) continueWith(element.argument, []);
|
|
else if (t.isExpression(element)) continueWith(element, []);
|
|
continue;
|
|
}
|
|
}
|
|
if (suffix !== undefined && key === undefined) continue;
|
|
if (t.isObjectExpression(root)) {
|
|
if (possibleWrites) {
|
|
for (const property of root.properties) {
|
|
if (t.isSpreadElement(property)) continueWith(property.argument);
|
|
else if (
|
|
(t.isObjectProperty(property) &&
|
|
t.isExpression(property.value)) ||
|
|
(t.isObjectMethod(property) && property.kind === "get")
|
|
) {
|
|
const ownKey = semanticStaticPropertyKey(
|
|
property.key,
|
|
property.computed,
|
|
);
|
|
if (
|
|
suffix === undefined ||
|
|
key === null ||
|
|
ownKey === null ||
|
|
ownKey === key
|
|
)
|
|
if (t.isObjectMethod(property))
|
|
continueReturned(property, suffix?.slice(1));
|
|
else if (t.isExpression(property.value))
|
|
continueWith(property.value, suffix?.slice(1));
|
|
}
|
|
}
|
|
} else if (
|
|
suffix !== undefined &&
|
|
key !== null &&
|
|
root.properties.every(
|
|
(property) =>
|
|
t.isObjectProperty(property) &&
|
|
semanticStaticPropertyKey(property.key, property.computed) !==
|
|
null,
|
|
)
|
|
) {
|
|
const property = root.properties.findLast(
|
|
(property) =>
|
|
t.isObjectProperty(property) &&
|
|
semanticStaticPropertyKey(property.key, property.computed) ===
|
|
key,
|
|
);
|
|
if (t.isObjectProperty(property) && t.isExpression(property.value))
|
|
continueWith(property.value, suffix.slice(1));
|
|
}
|
|
} else if (t.isArrayExpression(root)) {
|
|
if (suffix === undefined) {
|
|
for (const element of root.elements)
|
|
if (t.isSpreadElement(element)) continueWith(element.argument);
|
|
else if (t.isExpression(element)) continueWith(element);
|
|
} else if (!root.elements.some(t.isSpreadElement)) {
|
|
const index = Number(key);
|
|
if (key === null && possibleWrites) {
|
|
for (const element of root.elements)
|
|
if (t.isExpression(element))
|
|
continueWith(element, suffix.slice(1));
|
|
} else if (
|
|
key !== null &&
|
|
Number.isInteger(index) &&
|
|
index >= 0 &&
|
|
String(index) === key
|
|
) {
|
|
const element = root.elements[index];
|
|
if (t.isExpression(element)) continueWith(element, suffix.slice(1));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const paths = [...result.values()];
|
|
if (!possibleWrites) cached.set(node, paths);
|
|
return paths;
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Index source-local writes through ambient identities and stable aliases.
|
|
* The shared invocation owner resolves intrinsic aliases, bind/call/apply and
|
|
* literal argument spreads. Runtime reachability and arbitrary call effects
|
|
* remain unevaluated; a visible possible write is sufficient.
|
|
*/
|
|
export function* indexJavaScriptAmbientWritesSteps(
|
|
program: t.Program,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
observeOpaqueInvocation?: (node: t.Node) => void,
|
|
observeWriteSite?: (node: t.Node) => void,
|
|
): Generator<void> {
|
|
const ambientReads = createAmbientPathResolver(state, false);
|
|
const browserReceiverMethods = new Set<string>();
|
|
const storedSites = new WeakSet<t.Node>();
|
|
const storedReferences: SemanticIterationSource[] = [];
|
|
const writes: { node: t.Node; path: AmbientPath }[] = [];
|
|
const targets: { node: t.Node; capture: t.Node }[] = [];
|
|
const escapes: {
|
|
node: t.Node;
|
|
target: t.Node;
|
|
browserReceiver: boolean;
|
|
projection?: AmbientProjection;
|
|
}[] = [];
|
|
const stores = new WeakMap<
|
|
t.Node,
|
|
Map<SemanticIterationSource, AmbientProjection>
|
|
>();
|
|
const interop = createSemanticModuleInterop(state, {
|
|
intrinsicPath: (node) => {
|
|
const [path] = ambientReads(node);
|
|
return path?.[0] === "global" && path.every((key) => key !== null)
|
|
? ["intrinsic", ...(path.slice(1) as readonly string[])]
|
|
: undefined;
|
|
},
|
|
});
|
|
const ambientWrites = createAmbientPathResolver(state, true, {
|
|
stores,
|
|
freshObject: interop.isFreshObject,
|
|
prototypeTarget: (node) => {
|
|
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node))
|
|
return undefined;
|
|
const invocation = interop.intrinsicInvocation(node);
|
|
if (
|
|
invocation?.kind !== "known" ||
|
|
invocation.path.length !== 3 ||
|
|
!["Object", "Reflect"].includes(invocation.path[1] ?? "") ||
|
|
invocation.path[2] !== "getPrototypeOf"
|
|
)
|
|
return undefined;
|
|
const target = invocation.arguments?.[0];
|
|
return t.isNode(target) && !t.isSpreadElement(target)
|
|
? target
|
|
: undefined;
|
|
},
|
|
});
|
|
yield* collectJavaScriptIdentityWritesSteps(
|
|
program,
|
|
(root, kind, writeSite) => {
|
|
if (kind === "opaque-call") return [];
|
|
observeWriteSite?.(writeSite);
|
|
// Reassigning a local alias changes the binding, not the referenced object.
|
|
if (kind === "binding")
|
|
return t.isIdentifier(root) &&
|
|
isUnshadowedGlobal(root, state, root.name)
|
|
? [["global", root.name]]
|
|
: [];
|
|
if (!storedSites.has(writeSite)) {
|
|
storedSites.add(writeSite);
|
|
for (const source of semanticIterationSources(
|
|
writeSite,
|
|
state.parentsByNode.get(writeSite) ?? null,
|
|
)) {
|
|
if (source.kind === "reference-store") storedReferences.push(source);
|
|
}
|
|
}
|
|
// Resolve identities after the traversal has collected every local store.
|
|
const id = targets.push({ node: root, capture: writeSite }) - 1;
|
|
return [["pending-target", String(id)]];
|
|
},
|
|
interop.intrinsicInvocation,
|
|
interop.staticString,
|
|
interop.isFreshObject,
|
|
(node, path) => {
|
|
writes.push({ node, path });
|
|
},
|
|
(node, invocation) => {
|
|
observeOpaqueInvocation?.(node);
|
|
// Escaping objects can change contents without replacing their identity.
|
|
const retainEscape = (target: t.Node, browserReceiver = false) => {
|
|
escapes.push({ target, node, browserReceiver });
|
|
};
|
|
for (const argument of t.isTaggedTemplateExpression(node)
|
|
? node.quasi.expressions
|
|
: node.arguments)
|
|
if (t.isNode(argument))
|
|
retainEscape(
|
|
t.isSpreadElement(argument) ? argument.argument : argument,
|
|
);
|
|
const callee = unwrapJavaScriptExpression(
|
|
t.isTaggedTemplateExpression(node) ? node.tag : node.callee,
|
|
).node;
|
|
const browserMethod =
|
|
invocation?.kind === "known" &&
|
|
invocation.path.length === 2 &&
|
|
["open", "fetch"].includes(invocation.path[1] ?? "")
|
|
? invocation.path[1]
|
|
: undefined;
|
|
if (
|
|
!t.isNewExpression(node) &&
|
|
(t.isMemberExpression(callee) ||
|
|
t.isOptionalMemberExpression(callee)) &&
|
|
!(
|
|
invocation?.kind === "known" &&
|
|
["Object", "Reflect"].includes(invocation.path[1] ?? "")
|
|
)
|
|
) {
|
|
if (browserMethod !== undefined)
|
|
browserReceiverMethods.add(browserMethod);
|
|
retainEscape(callee.object, browserMethod !== undefined);
|
|
}
|
|
},
|
|
);
|
|
const pendingStores = new Set(storedReferences);
|
|
const dependents = new WeakMap<t.Node, Set<SemanticIterationSource>>();
|
|
while (pendingStores.size > 0) {
|
|
const source = pendingStores.values().next().value;
|
|
if (source === undefined) break;
|
|
pendingStores.delete(source);
|
|
if (source.target === undefined) continue;
|
|
const identities = new Map<t.Node, AmbientProjection>();
|
|
ambientWrites(source.target, source.mutation, false, [], (node, path) => {
|
|
const users = dependents.get(node) ?? new Set<SemanticIterationSource>();
|
|
users.add(source);
|
|
dependents.set(node, users);
|
|
identities.set(
|
|
node,
|
|
identities.has(node)
|
|
? joinAmbientProjection(identities.get(node), path)
|
|
: path,
|
|
);
|
|
});
|
|
// Publish only completed queries; each source/root projection can only widen.
|
|
for (const [node, path] of identities) {
|
|
const entries =
|
|
stores.get(node) ??
|
|
new Map<SemanticIterationSource, AmbientProjection>();
|
|
const previous = entries.get(source);
|
|
const joined = entries.has(source)
|
|
? joinAmbientProjection(previous, path)
|
|
: path;
|
|
if (
|
|
entries.has(source) &&
|
|
(previous === undefined ||
|
|
(joined !== undefined &&
|
|
previous.every((key, index) => key === joined[index])))
|
|
)
|
|
continue;
|
|
entries.set(source, joined);
|
|
stores.set(node, entries);
|
|
for (const dependent of dependents.get(node) ?? [])
|
|
pendingStores.add(dependent);
|
|
}
|
|
yield;
|
|
}
|
|
for (const source of storedReferences) {
|
|
if (source.target === undefined) continue;
|
|
let local = false;
|
|
const ambient = ambientWrites(
|
|
source.target.object,
|
|
source.mutation,
|
|
false,
|
|
[],
|
|
() => {
|
|
local = true;
|
|
},
|
|
);
|
|
if (!local || ambient.length > 0)
|
|
escapes.push({
|
|
node: source.mutation,
|
|
target: source.node,
|
|
browserReceiver: false,
|
|
projection: source.projection.map((key) =>
|
|
key === null || typeof key === "object" ? null : String(key),
|
|
),
|
|
});
|
|
}
|
|
for (const { node, path } of writes) {
|
|
const target =
|
|
path[0] === "pending-target" ? targets[Number(path[1])] : undefined;
|
|
const paths =
|
|
target === undefined
|
|
? [path]
|
|
: ambientWrites(target.node, target.capture).map((identity) => [
|
|
...identity,
|
|
...path.slice(2),
|
|
]);
|
|
for (const resolved of paths) {
|
|
state.ambientWrites.retain(resolved);
|
|
state.ambientWriteEffects.push({ path: resolved, node });
|
|
}
|
|
}
|
|
for (const { node, target, browserReceiver, projection } of escapes)
|
|
for (const path of ambientWrites(target, node, true, projection))
|
|
// Only proven browser APIs keep their global receiver from user code.
|
|
if (!(browserReceiver && path.length === 1 && path[0] === "global")) {
|
|
const escaped = [...path, null];
|
|
state.ambientEscapes.retain(escaped);
|
|
state.ambientEscapeEffects.push({ path: escaped, node });
|
|
}
|
|
// A function may call a browser method replaced later in source order.
|
|
if (
|
|
[...browserReceiverMethods].some((method) =>
|
|
state.ambientWrites.covers(["global", method]),
|
|
)
|
|
)
|
|
state.ambientEscapes.retain(["global", null]);
|
|
}
|
|
|
|
/**
|
|
* Recognize an ambient property path, optionally through a global object, without a visible
|
|
* source-local overwrite. This is a syntax-local identity fact, not proof that
|
|
* the runtime's globals have never been patched by other code.
|
|
*/
|
|
export const isUnmodifiedGlobalReference = (
|
|
callee: t.Node,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
name: string | readonly [string, ...string[]],
|
|
): boolean => {
|
|
const path: readonly [string, ...string[]] =
|
|
typeof name === "string" ? [name] : name;
|
|
if (state.ambientWrites.covers(["global", ...path])) return false;
|
|
const { root, members } = electronMemberPath(callee);
|
|
const matches = (expected: readonly string[]) =>
|
|
members.length === expected.length &&
|
|
members.every((key, index) => key === expected[index]);
|
|
if (isUnshadowedGlobal(root, state, path[0]) && matches(path.slice(1)))
|
|
return true;
|
|
const namespace = globalObjectName(root, state);
|
|
return (
|
|
namespace !== undefined &&
|
|
matches(path) &&
|
|
!state.ambientWrites.covers(["global", namespace])
|
|
);
|
|
};
|
|
|
|
/** A module URL base is known only while its property has no visible write. */
|
|
export const isUnmodifiedImportMetaUrl = (
|
|
node: t.Node | undefined,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): boolean =>
|
|
isImportMetaUrl(node) && !state.ambientWrites.covers(["import-meta", "url"]);
|