mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(javascript): require complete receiver ownership proofs
This commit is contained in:
@@ -90,6 +90,18 @@ const joinAmbientProjection = (
|
||||
? undefined
|
||||
: left.map((key, index) => (key === right[index] ? key : null));
|
||||
|
||||
type AmbientIdentityUnknownReason =
|
||||
| "unresolved-value"
|
||||
| "widened-projection"
|
||||
| "unresolved-call"
|
||||
| "mixed-identities";
|
||||
|
||||
interface AmbientResolution {
|
||||
readonly paths: readonly AmbientPath[];
|
||||
readonly ownership: "local" | "ambient" | "unknown";
|
||||
readonly unknownReasons: readonly AmbientIdentityUnknownReason[];
|
||||
}
|
||||
|
||||
interface AmbientReferenceFacts {
|
||||
readonly stores: WeakMap<
|
||||
t.Node,
|
||||
@@ -109,7 +121,7 @@ export const createAmbientPathResolver = (
|
||||
possibleWrites: boolean,
|
||||
facts?: AmbientReferenceFacts,
|
||||
) => {
|
||||
const cached = new WeakMap<t.Node, readonly AmbientPath[]>();
|
||||
const cached = new WeakMap<t.Node, AmbientResolution>();
|
||||
const returnedReferences = createSemanticReturnedReferences(state);
|
||||
return (
|
||||
node: t.Node,
|
||||
@@ -117,10 +129,12 @@ export const createAmbientPathResolver = (
|
||||
escapedContents = false,
|
||||
projection: AmbientProjection = [],
|
||||
localIdentity?: (node: t.Node, path: AmbientProjection) => void,
|
||||
): readonly AmbientPath[] => {
|
||||
): AmbientResolution => {
|
||||
const prior = possibleWrites ? undefined : cached.get(node);
|
||||
if (prior !== undefined) return prior;
|
||||
const result = new Map<string, AmbientPath>();
|
||||
let hasLocalIdentity = false;
|
||||
const unknownReasons = new Set<AmbientIdentityUnknownReason>();
|
||||
// The caller's write site selects initializers for the written target.
|
||||
// Reads reached through an initializer occur at their own position.
|
||||
const pending: {
|
||||
@@ -159,81 +173,61 @@ export const createAmbientPathResolver = (
|
||||
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)
|
||||
)
|
||||
const queuedBefore = pending.length;
|
||||
const pathsBefore = result.size;
|
||||
let resolvedIdentity = false;
|
||||
try {
|
||||
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) {
|
||||
resolvedIdentity = true;
|
||||
continue;
|
||||
const remaining =
|
||||
suffix === undefined || path === undefined
|
||||
? undefined
|
||||
: suffix.slice(path.length);
|
||||
}
|
||||
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])) {
|
||||
resolvedIdentity = true;
|
||||
continue;
|
||||
}
|
||||
suffix = joined;
|
||||
} else suffix = undefined;
|
||||
if (!possibleWrites) {
|
||||
const unresolved: AmbientResolution = {
|
||||
paths: [],
|
||||
ownership: "unknown",
|
||||
unknownReasons: ["widened-projection"],
|
||||
};
|
||||
cached.set(node, unresolved);
|
||||
return unresolved;
|
||||
}
|
||||
}
|
||||
expanded.set(root, suffix);
|
||||
if (suffix === undefined || suffix.includes(null))
|
||||
unknownReasons.add("widened-projection");
|
||||
// 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: source.node,
|
||||
readsAtSelf: true,
|
||||
suffix:
|
||||
remaining === undefined
|
||||
node,
|
||||
suffix: path,
|
||||
...(current.readsAtSelf && { readsAtSelf: true }),
|
||||
});
|
||||
const continueReturned = (callee: t.Node, path: AmbientProjection) => {
|
||||
const returned = returnedReferences(callee);
|
||||
// Returned references retain possible source-local values, not a
|
||||
// completeness proof for every runtime callable alternative.
|
||||
unknownReasons.add("unresolved-call");
|
||||
for (const source of returned.references)
|
||||
continueWith(
|
||||
source.node,
|
||||
path === undefined
|
||||
? undefined
|
||||
: [
|
||||
...source.projection.map((key) =>
|
||||
@@ -241,235 +235,311 @@ export const createAmbientPathResolver = (
|
||||
? null
|
||||
: String(key),
|
||||
),
|
||||
...remaining,
|
||||
...path,
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
);
|
||||
};
|
||||
if (!t.isIdentifier(root) && facts?.freshObject(root)) {
|
||||
localIdentity?.(root, suffix);
|
||||
// The allocation owns itself; a projected child needs its own proof.
|
||||
if (suffix?.length === 0) {
|
||||
hasLocalIdentity = true;
|
||||
resolvedIdentity = true;
|
||||
}
|
||||
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,
|
||||
],
|
||||
});
|
||||
}
|
||||
} 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) {
|
||||
if (
|
||||
t.isMetaProperty(root) &&
|
||||
root.meta.name === "import" &&
|
||||
root.property.name === "meta"
|
||||
) {
|
||||
retain(["import-meta", ...(suffix ?? [])]);
|
||||
resolvedIdentity = true;
|
||||
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 ?? [])]);
|
||||
resolvedIdentity = true;
|
||||
continue;
|
||||
}
|
||||
if (globalObjectName(root, state) !== undefined) {
|
||||
retain(["global", ...(suffix ?? [])]);
|
||||
resolvedIdentity = true;
|
||||
continue;
|
||||
}
|
||||
if (isUnshadowedGlobal(root, state, root.name)) {
|
||||
retain(["global", root.name, ...(suffix ?? [])]);
|
||||
resolvedIdentity = true;
|
||||
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 atRead =
|
||||
possibleWrites && readAt !== undefined
|
||||
? semanticBindingAtRead(binding, readAt, state.parentsByNode)
|
||||
: undefined;
|
||||
const selected = atRead?.binding ?? binding;
|
||||
if (possibleWrites && atRead?.hasDefiniteInitializer === false)
|
||||
unknownReasons.add("unresolved-value");
|
||||
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));
|
||||
} 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (
|
||||
!resolvedIdentity &&
|
||||
pending.length === queuedBefore &&
|
||||
result.size === pathsBefore
|
||||
)
|
||||
unknownReasons.add("unresolved-value");
|
||||
}
|
||||
}
|
||||
const paths = [...result.values()];
|
||||
if (!possibleWrites) cached.set(node, paths);
|
||||
return paths;
|
||||
if (paths.length > 0 && hasLocalIdentity)
|
||||
unknownReasons.add("mixed-identities");
|
||||
const resolution: AmbientResolution = {
|
||||
paths,
|
||||
ownership:
|
||||
unknownReasons.size > 0
|
||||
? "unknown"
|
||||
: paths.length > 0
|
||||
? "ambient"
|
||||
: hasLocalIdentity
|
||||
? "local"
|
||||
: "unknown",
|
||||
unknownReasons: [...unknownReasons],
|
||||
};
|
||||
if (!possibleWrites) cached.set(node, resolution);
|
||||
return resolution;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -503,7 +573,7 @@ export function* indexJavaScriptAmbientWritesSteps(
|
||||
>();
|
||||
const interop = createSemanticModuleInterop(state, {
|
||||
intrinsicPath: (node) => {
|
||||
const [path] = ambientReads(node);
|
||||
const [path] = ambientReads(node).paths;
|
||||
return path?.[0] === "global" && path.every((key) => key !== null)
|
||||
? ["intrinsic", ...(path.slice(1) as readonly string[])]
|
||||
: undefined;
|
||||
@@ -640,17 +710,8 @@ export function* indexJavaScriptAmbientWritesSteps(
|
||||
}
|
||||
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) {
|
||||
const receiver = ambientWrites(source.target.object, source.mutation);
|
||||
if (receiver.ownership !== "local") {
|
||||
state.escapingReferenceStores.add(source.target);
|
||||
escapes.push({
|
||||
node: source.mutation,
|
||||
@@ -668,7 +729,7 @@ export function* indexJavaScriptAmbientWritesSteps(
|
||||
const paths =
|
||||
target === undefined
|
||||
? [path]
|
||||
: ambientWrites(target.node, target.capture).map((identity) => [
|
||||
: ambientWrites(target.node, target.capture).paths.map((identity) => [
|
||||
...identity,
|
||||
...path.slice(2),
|
||||
]);
|
||||
@@ -677,7 +738,7 @@ export function* indexJavaScriptAmbientWritesSteps(
|
||||
}
|
||||
}
|
||||
for (const { node, target, browserReceiver, projection } of escapes)
|
||||
for (const path of ambientWrites(target, node, true, projection))
|
||||
for (const path of ambientWrites(target, node, true, projection).paths)
|
||||
// Only proven browser APIs keep their global receiver from user code.
|
||||
if (!(browserReceiver && path.length === 1 && path[0] === "global")) {
|
||||
retainSemanticAmbientEffect(state, "escape", path, node);
|
||||
|
||||
@@ -283,7 +283,7 @@ const visitSemanticReferenceEffectsAtSite = (
|
||||
} else if (lookup.kind === "ambient" || lookup.kind === "uncertain-ambient") {
|
||||
for (const node of state.ambientBindingWrites.get(root.name) ?? [])
|
||||
if (!retain("ambient-write", node)) return metadata;
|
||||
const ambientRoots = createAmbientPathResolver(state, false)(root);
|
||||
const ambientRoots = createAmbientPathResolver(state, false)(root).paths;
|
||||
for (const ambientRoot of ambientRoots) {
|
||||
const selectedPath = [...ambientRoot, ...path];
|
||||
for (const [kind, effects] of [
|
||||
|
||||
@@ -688,7 +688,7 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
current.path.map((key) =>
|
||||
key === null || typeof key === "object" ? null : String(key),
|
||||
),
|
||||
))
|
||||
).paths)
|
||||
if (retainSemanticAmbientEffect(state, effect, path, mutation)) {
|
||||
recordedEffectCount += 1;
|
||||
clearSemanticValueCaches(state);
|
||||
@@ -1810,7 +1810,7 @@ const observableMutationPaths = (
|
||||
["globalThis", "global", "window", "self"].includes(expression.name)
|
||||
)
|
||||
(observer.origins ??= new Set()).add("ambient");
|
||||
else if (ambientPaths(expression).length > 0)
|
||||
else if (ambientPaths(expression).paths.length > 0)
|
||||
(observer.origins ??= new Set()).add("opaque");
|
||||
} else if (
|
||||
t.isMemberExpression(expression) ||
|
||||
|
||||
@@ -63,7 +63,7 @@ export const createSemanticOpaqueGlobalObserver = (
|
||||
let invocationSite: t.Node | undefined;
|
||||
const interop = createSemanticModuleInterop(state, {
|
||||
intrinsicPath: (node) => {
|
||||
const paths = ambientPaths(node, invocationSite);
|
||||
const paths = ambientPaths(node, invocationSite).paths;
|
||||
const path = paths.find((path) => path[1] === "eval") ?? paths[0];
|
||||
if (path?.[0] !== "global" || path.some((key) => key === null))
|
||||
return undefined;
|
||||
|
||||
@@ -37,6 +37,8 @@ it.each([
|
||||
'const box = {}; box.value = { name: "Alice" }; inspect(box); new Map().get("/accounts")',
|
||||
'patch(() => ({ name: "Alice" })); new Map().get("/accounts")',
|
||||
'const box = {}; box.prototype = Map.prototype; new Map().get("/accounts")',
|
||||
'function expose() { const holder = { child: {} }; holder.child.prototype = Map.prototype; } new Map().get("/accounts")',
|
||||
'const box = flag ? {} : {}; box.prototype = Map.prototype; new Map().get("/accounts")',
|
||||
'const box = {}; const alias = box; alias.prototype = Map.prototype; inspect({}); new Map().get("/accounts")',
|
||||
'const box = {}; box.make = () => Map.prototype; new Map().get("/accounts")',
|
||||
'let box = {}; const first = box; box = {}; box.p = Map.prototype; patch(first); new Map().get("/accounts")',
|
||||
@@ -84,6 +86,15 @@ it.each([
|
||||
'const box = { set prototype(value) { patch(value); } }; box.prototype = Map.prototype; new Map().get("/accounts")',
|
||||
'const box = new ExternalBox(); box.prototype = Map.prototype; new Map().get("/accounts")',
|
||||
'function expose(box) { box.prototype = Map.prototype; } new Map().get("/accounts")',
|
||||
'function expose(box = {}) { box.prototype = Map.prototype; } new Map().get("/accounts")',
|
||||
'function expose(external, flag) { const box = flag ? {} : external; box.prototype = Map.prototype; } new Map().get("/accounts")',
|
||||
'function expose(external) { const holder = { child: external }; holder.child.prototype = Map.prototype; } new Map().get("/accounts")',
|
||||
'function expose(key) { const holder = { child: {} }; holder[key].prototype = Map.prototype; } new Map().get("/accounts")',
|
||||
'function expose(key) { const holder = [{}]; holder[key].prototype = Map.prototype; } new Map().get("/accounts")',
|
||||
'const box = flag ? {} : external(); box.prototype = Map.prototype; new Map().get("/accounts")',
|
||||
'function expose(external, flag) { const make = flag ? () => ({}) : external; const box = make(); box.prototype = Map.prototype; } new Map().get("/accounts")',
|
||||
'import external from "./external.mjs"; const box = flag ? {} : external; box.prototype = Map.prototype; new Map().get("/accounts")',
|
||||
'let box; if (flag) box = {}; box.prototype = Map.prototype; new Map().get("/accounts")',
|
||||
'const factory = { make() { return Map.prototype; } }; factory.make().get = fetch; new Map().get("/accounts")',
|
||||
'new Map().__proto__.get = fetch; new Map().get("/accounts")',
|
||||
'new Map().constructor.prototype.get = fetch; new Map().get("/accounts")',
|
||||
|
||||
Reference in New Issue
Block a user