fix(javascript): preserve uncertainty after references escape to calls (#1292)

* fix(javascript): preserve uncertainty after references escape to calls

* fix(javascript): track iterator root escapes and preserve static captures

* fix(javascript): scope comparison unknowns to their evidence

* fix(javascript): track parameter origins at function body entry

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
You Wish
2026-10-09 18:53:42 -04:00
committed by GitHub
parent d0e606930a
commit 355a292198
17 changed files with 2574 additions and 220 deletions
@@ -2,77 +2,268 @@ import * as t from "@babel/types";
import {
resolveSemanticBindingState,
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticBindingState,
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import { evaluateSemanticBinding } from "./javascriptSemanticValues.js";
import {
clearSemanticPrimitiveBindingValues,
evaluateSemanticBinding,
evaluateSemanticExpression,
} from "./javascriptSemanticValues.js";
import {
semanticContainer,
semanticSlotAtPath,
} from "./javascriptSemanticSlots.js";
import { semanticIterationSources } from "./javascriptSemanticIterationSources.js";
import { semanticMutationInitializers } from "./javascriptSemanticMutationInitializers.js";
import {
semanticArrayIndex,
semanticPropertyPathKeyMatches,
type JavaScriptSemanticPropertyPath,
} from "./javascriptSemanticPropertyPaths.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import {
semanticStaticPropertyKey,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
type PropertyPath = readonly (string | number | null)[];
type PropertyPath = JavaScriptSemanticPropertyPath;
type ValueEffect = "write" | "escape";
/** Keep explicit property mutations outside the initializer-only value lattice. */
/** Preserve uncertainty from property writes and references exposed to calls. */
export const collectSemanticMemberMutations = (
program: t.Program,
state: JavaScriptSemanticAnalysisState,
): void => {
const parents = new WeakMap<t.Node, t.Node>();
const parents = state.parentsByNode;
traverseJavaScriptAst(program, {
enter: (node, parent) => {
if (parent !== null) parents.set(node, parent);
},
});
const recordedEffects = new Set<string>();
const selections = new Map<string, readonly string[]>();
let arrayIterationUnknown = false;
const pendingReferences: {
readonly initializer: JavaScriptSemanticBindingState["referenceInitializers"][number];
readonly path: PropertyPath;
readonly bindings: ReadonlySet<string>;
readonly effect: ValueEffect;
readonly mutation?: t.Node;
readonly originAt: t.Node | undefined;
}[] = [];
const iterableReferences: IterableReference[] = [];
const iterableReferenceKeys = new WeakMap<t.Node, Set<string>>();
const deferIterable = (reference: IterableReference): void => {
const keys = iterableReferenceKeys.get(reference.node) ?? new Set<string>();
const key = JSON.stringify([
reference.projection,
reference.path,
reference.fallbackPath,
reference.effect,
reference.mutation?.start,
reference.mutation?.end,
reference.originAt?.start,
reference.originAt?.end,
reference.iterationOnly,
reference.requiredSources?.map((source) => [
source.node.start,
source.node.end,
source.projection,
]),
reference.fallbackSources?.map((source) => [
source.node.start,
source.node.end,
source.projection,
]),
[...reference.bindings].sort(compareUnicodeCodePoints),
]);
if (keys.has(key)) return;
keys.add(key);
iterableReferenceKeys.set(reference.node, keys);
iterableReferences.push(reference);
};
const markValue = (
node: t.Node,
path: PropertyPath,
bindings: ReadonlySet<string>,
mutation: t.Node,
effect: ValueEffect = "write",
mutation?: t.Node,
originAt: t.Node | undefined = mutation,
): void => {
const unwrapped = unwrapJavaScriptExpression(node).node;
if (unwrapped !== node) {
markValue(unwrapped, path, bindings, mutation);
return;
}
if (t.isIdentifier(node)) {
const binding = resolveSemanticBindingState(state, node, node.name);
if (binding === undefined || bindings.has(binding.bindingId)) return;
const value = evaluateSemanticBinding(binding, state);
if (value.status === "literal" || value.status === "union") return;
binding.mutatedPaths.push(path);
const nested = new Set([...bindings, binding.bindingId]);
for (const initializer of mutationInitializers(
binding,
mutation,
parents,
))
markValue(
initializer.node,
[...initializer.projection, ...path],
nested,
mutation,
const pending = [{ node, path, bindings, originAt }];
while (pending.length > 0) {
const current = pending.pop();
if (current === undefined) break;
const expression = unwrapJavaScriptExpression(current.node).node;
if (t.isIdentifier(expression)) {
const binding = resolveSemanticBindingState(
state,
expression,
expression.name,
);
return;
}
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
const members: (string | null)[] = [];
let current: t.Node = node;
while (
t.isMemberExpression(current) ||
t.isOptionalMemberExpression(current)
if (binding === undefined) {
if (
(expression.name === "Array" &&
(current.path[0] === "prototype" || current.path[0] === null)) ||
(["globalThis", "global", "window", "self"].includes(
expression.name,
) &&
(current.path[0] === "Array" || current.path[0] === null) &&
(current.path[1] === "prototype" || current.path[1] === null))
)
arrayIterationUnknown = true;
continue;
}
// An alias restored from a saved reference can revisit the same binding
// at an earlier capture. Only a repeated lifetime closes a cycle.
const lifetime = JSON.stringify([
binding.bindingId,
current.originAt?.start,
current.originAt?.end,
]);
if (current.bindings.has(lifetime)) continue;
// Union compatible copied-slot selections at each binding. Carrying every
// combination of exclusions through branch diamonds is exponential.
const selectorIndex = current.path.findIndex(
(key) => key !== null && typeof key === "object",
);
let path = current.path;
const selector = path[selectorIndex];
if (selector !== null && typeof selector === "object") {
const selectionIdentity = JSON.stringify([
binding.bindingId,
effect,
path.map((key, index) =>
index === selectorIndex
? { startIndex: selector.startIndex }
: key,
),
mutation?.start,
mutation?.end,
current.originAt?.start,
current.originAt?.end,
]);
const previous = selections.get(selectionIdentity);
const excludedKeys =
previous === undefined
? selector.excludedKeys
: previous.filter((key) => selector.excludedKeys.includes(key));
if (previous !== undefined && excludedKeys.length === previous.length)
continue;
selections.set(selectionIdentity, excludedKeys);
path = path.map((key, index) =>
index === selectorIndex ? { ...selector, excludedKeys } : key,
);
}
const identity = JSON.stringify([
binding.bindingId,
effect,
path,
mutation?.start,
mutation?.end,
current.originAt?.start,
current.originAt?.end,
]);
if (recordedEffects.has(identity)) continue;
const value =
effect === "write"
? evaluateSemanticBinding(binding, state)
: undefined;
const primitiveWrite =
value?.status === "literal" || value?.status === "union";
recordedEffects.add(identity);
const nested = new Set([...current.bindings, lifetime]);
const origins =
current.originAt === undefined
? binding
: semanticMutationInitializers(binding, current.originAt, parents);
if (!primitiveWrite) {
if (effect === "escape")
binding.escapedPaths.push({ path, node: mutation });
else binding.mutatedPaths.push(path);
clearSemanticPrimitiveBindingValues(state);
for (const initializer of origins.initializers)
pending.push({
node: initializer.node,
path: [...initializer.projection, ...path],
bindings: nested,
originAt: initializer.node,
});
}
for (const initializer of origins.referenceInitializers) {
const copiedPath = copiedReferencePath(initializer, path, effect);
if (copiedPath === null) continue;
pendingReferences.push({
initializer,
path: copiedPath,
bindings: nested,
effect,
originAt: initializer.node,
...(mutation === undefined ? {} : { mutation }),
});
}
} else if (
t.isMemberExpression(expression) ||
t.isOptionalMemberExpression(expression)
) {
members.push(
semanticStaticPropertyKey(current.property, current.computed),
);
current = current.object;
pending.push({
node: expression.object,
path: [
semanticStaticPropertyKey(expression.property, expression.computed),
...current.path,
],
bindings: current.bindings,
originAt: current.originAt,
});
} else {
if (
t.isArrayExpression(expression) &&
((current.path[0] === "__proto__" &&
(effect === "escape" || current.path.length > 1)) ||
(current.path[0] === "constructor" &&
current.path[1] === "prototype"))
)
arrayIterationUnknown = true;
for (const value of referencedValues(
expression,
current.path,
effect,
)) {
if (value.iterableFallbackPath !== undefined)
deferIterable({
node: value.node,
projection: [],
path: value.path,
fallbackPath: value.iterableFallbackPath,
bindings: current.bindings,
effect,
originAt: current.originAt,
...(mutation === undefined ? {} : { mutation }),
});
else
pending.push({
...value,
bindings: current.bindings,
originAt: current.originAt,
});
}
}
markValue(current, [...members.reverse(), ...path], bindings, mutation);
return;
}
for (const value of referencedValues(node, path))
markValue(value.node, value.path, bindings, mutation);
};
const markEscaped = (
node: t.Node,
mutation: t.Node,
path: PropertyPath = [],
): void => markValue(node, path, new Set(), "escape", mutation);
const markReceiver = (callee: t.Node, mutation: t.Node): void => {
const expression = unwrapJavaScriptExpression(callee).node;
if (
t.isMemberExpression(expression) ||
t.isOptionalMemberExpression(expression)
)
markEscaped(expression.object, mutation);
};
const markTarget = (node: t.Node, mutation: t.Node): void => {
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
@@ -80,6 +271,7 @@ export const collectSemanticMemberMutations = (
node.object,
[semanticStaticPropertyKey(node.property, node.computed)],
new Set(),
"write",
mutation,
);
else if (t.isRestElement(node)) markTarget(node.argument, mutation);
@@ -96,143 +288,245 @@ export const collectSemanticMemberMutations = (
}
};
traverseJavaScriptAst(program, {
enter: (node) => {
enter: (node, parent) => {
for (const source of semanticIterationSources(node, parent))
deferIterable({
...source,
path: source.projection,
fallbackPath: source.projection,
bindings: new Set(),
effect: "escape",
originAt: source.mutation,
iterationOnly: true,
});
if (t.isAssignmentExpression(node)) markTarget(node.left, node);
else if (t.isUpdateExpression(node)) markTarget(node.argument, node);
else if (t.isUnaryExpression(node, { operator: "delete" }))
markTarget(node.argument, node);
else if (t.isForOfStatement(node) || t.isForInStatement(node))
markTarget(node.left, node);
else if (
t.isCallExpression(node) ||
t.isOptionalCallExpression(node) ||
t.isNewExpression(node)
) {
for (const argument of node.arguments)
if (t.isSpreadElement(argument))
deferIterable({
node: argument.argument,
projection: [],
path: [null],
fallbackPath: [],
bindings: new Set(),
effect: "escape",
mutation: node,
originAt: node,
});
else markEscaped(argument, node);
// Constructing a member does not pass its container as `this`.
if (!t.isNewExpression(node)) markReceiver(node.callee, node);
} else if (t.isTaggedTemplateExpression(node)) {
markReceiver(node.tag, node);
for (const expression of node.quasi.expressions)
markEscaped(expression, node);
}
},
});
};
interface StatementPosition {
readonly body: readonly t.Statement[];
readonly index: number;
}
const statementPosition = (
statement: t.Statement,
parents: WeakMap<t.Node, t.Node>,
): StatementPosition | null => {
const parent = parents.get(statement);
if (!t.isProgram(parent) && !t.isBlockStatement(parent)) return null;
const index = parent.body.indexOf(statement);
return index < 0 ? null : { body: parent.body, index };
};
const directInitializerPosition = (
node: t.Node,
parents: WeakMap<t.Node, t.Node>,
): StatementPosition | null => {
const parent = parents.get(node);
if (t.isVariableDeclarator(parent) && parent.init === node) {
const declaration = parents.get(parent);
return t.isVariableDeclaration(declaration)
? statementPosition(declaration, parents)
: null;
}
if (
t.isAssignmentExpression(parent) &&
parent.operator === "=" &&
parent.right === node &&
t.isIdentifier(parent.left)
) {
const statement = parents.get(parent);
return t.isExpressionStatement(statement) && statement.expression === parent
? statementPosition(statement, parents)
: null;
}
return null;
};
const directMutationPosition = (
node: t.Node,
parents: WeakMap<t.Node, t.Node>,
): StatementPosition | null => {
if (
!t.isAssignmentExpression(node) &&
!t.isUpdateExpression(node) &&
!t.isUnaryExpression(node, { operator: "delete" })
)
return null;
const statement = parents.get(node);
return t.isExpressionStatement(statement) && statement.expression === node
? statementPosition(statement, parents)
: null;
};
const mutationInitializers = (
binding: JavaScriptSemanticBindingState,
mutation: t.Node,
parents: WeakMap<t.Node, t.Node>,
): JavaScriptSemanticBindingState["initializers"] => {
if (binding.initializers.length < 2) return binding.initializers;
const mutationPosition = directMutationPosition(mutation, parents);
if (mutationPosition === null) return binding.initializers;
const positions = binding.initializers.map(({ node, entryBody }) =>
entryBody === undefined
? directInitializerPosition(node, parents)
: { body: entryBody.body, index: -1 },
);
if (
positions.some(
(position) =>
position === null ||
position.body !== mutationPosition.body ||
position.index >= mutationPosition.index,
// A later effect can make a previously defined destructuring source uncertain.
// Reconsider defaults and iterable origins until their effects are stable.
while (pendingReferences.length > 0 || iterableReferences.length > 0) {
const effectsBefore = recordedEffects.size;
const iterablesBefore = iterableReferences.length;
const iterationBefore: boolean = arrayIterationUnknown;
for (const reference of pendingReferences.splice(0)) {
const { initializer } = reference;
if (referenceSourcesInactive(initializer, state))
pendingReferences.push(reference);
else if (
initializer.copyKind === "array-rest" &&
initializer.copyProjectionOffset !== undefined
) {
const offset = initializer.copyProjectionOffset;
const projection = initializer.projection.slice(0, offset);
deferIterable({
node: initializer.node,
projection,
path: reference.path,
fallbackPath: projection,
bindings: reference.bindings,
effect: reference.effect,
originAt: reference.originAt,
...(reference.mutation === undefined
? {}
: { mutation: reference.mutation }),
});
} else
markValue(
initializer.node,
reference.path,
reference.bindings,
reference.effect,
reference.mutation,
reference.originAt,
);
}
for (const reference of [...iterableReferences]) {
if (referenceSourcesInactive(reference, state)) continue;
if (reference.iterationOnly) {
const expanded = expandedIterationReferences(reference, state);
if (expanded !== null) {
for (const selected of expanded) deferIterable(selected);
continue;
}
}
const slot = selectedReferenceSlot(reference, state);
// Custom iteration can yield the iterable itself as well as any child.
// Treat its root as escaped, including writes through yielded references.
const knownArray =
!arrayIterationUnknown &&
slot?.value.status === "array" &&
slot.value.items.every(
(item) => semanticArrayIndex(item.name) !== null,
);
if (knownArray && reference.iterationOnly) continue;
markValue(
reference.node,
knownArray ? reference.path : reference.fallbackPath,
reference.bindings,
knownArray ? reference.effect : "escape",
reference.mutation,
reference.originAt,
);
}
if (
recordedEffects.size === effectsBefore &&
iterableReferences.length === iterablesBefore &&
arrayIterationUnknown === iterationBefore
)
)
return binding.initializers;
const latestIndex = positions.reduce(
(latest, position) => Math.max(latest, position?.index ?? -1),
-1,
);
return binding.initializers.filter(
(_, index) => positions[index]?.index === latestIndex,
);
break;
}
// Gathering effects can evaluate a primitive projection before its object
// escapes. Final values must use the completed mutation state.
clearSemanticPrimitiveBindingValues(state);
};
interface ReferencedValue {
readonly node: t.Node;
readonly path: PropertyPath;
readonly iterableFallbackPath?: PropertyPath;
}
interface IterableReference extends ReferencedValue {
readonly projection: PropertyPath;
readonly fallbackPath: PropertyPath;
readonly bindings: ReadonlySet<string>;
readonly effect: ValueEffect;
readonly mutation?: t.Node;
readonly originAt: t.Node | undefined;
readonly iterationOnly?: boolean;
readonly requiredSources?: readonly {
readonly node: t.Node;
readonly projection: PropertyPath;
}[];
readonly fallbackSources?: readonly {
readonly node: t.Node;
readonly projection: PropertyPath;
}[];
}
const referenceSourcesInactive = (
reference: Pick<IterableReference, "requiredSources" | "fallbackSources">,
state: JavaScriptSemanticAnalysisState,
): boolean =>
reference.requiredSources?.some(
(source) => selectedReferenceSlot(source, state)?.presence === "absent",
) === true ||
reference.fallbackSources?.some((source) => {
const slot = selectedReferenceSlot(source, state);
return (
slot?.presence === "present" &&
(slot.value.status === "literal" ||
slot.value.status === "union" ||
slot.value.status === "object" ||
slot.value.status === "array")
);
}) === true;
const expandedIterationReferences = (
reference: IterableReference,
state: JavaScriptSemanticAnalysisState,
): readonly IterableReference[] | null => {
if (!t.isForOfStatement(reference.mutation)) return null;
const iterable = reference.mutation.right;
// Expand only the loop's yielded-item selector. Enumerating dynamic property
// selections would multiply paths through shared object aliases.
const source = [
reference,
...(reference.requiredSources ?? []),
...(reference.fallbackSources ?? []),
].find((source) => source.node === iterable && source.projection[0] === null);
if (source === undefined) return null;
const slot = selectedReferenceSlot({ node: iterable, projection: [] }, state);
const container = slot === undefined ? null : semanticContainer(slot.value);
if (container?.coverage.status !== "complete") return null;
const select = (
candidate: { readonly node: t.Node; readonly projection: PropertyPath },
name: string,
) => ({
...candidate,
projection:
candidate.node === iterable && candidate.projection[0] === null
? [name, ...candidate.projection.slice(1)]
: candidate.projection,
});
return container.slots.map((slot) => {
const { projection } = select(reference, slot.name);
return {
...reference,
projection,
path: projection,
fallbackPath: projection,
...(reference.requiredSources === undefined
? {}
: {
requiredSources: reference.requiredSources.map((source) =>
select(source, slot.name),
),
}),
...(reference.fallbackSources === undefined
? {}
: {
fallbackSources: reference.fallbackSources.map((source) =>
select(source, slot.name),
),
}),
};
});
};
const referencedValues = (
node: t.Node,
path: PropertyPath,
effect: ValueEffect,
): readonly ReferencedValue[] => {
const [key, ...remaining] = path;
// An initializer owns its slots; only deeper writes can affect shared children.
if (t.isObjectExpression(node))
return path.length < 2
? []
: node.properties.flatMap<ReferencedValue>((property) => {
if (t.isSpreadElement(property))
return [{ node: property.argument, path }];
if (!t.isObjectProperty(property)) return [];
const name = semanticStaticPropertyKey(
property.key,
property.computed,
);
return key === null || name === null || String(key) === name
? [{ node: property.value, path: remaining }]
: [];
});
if (t.isArrayExpression(node)) {
if (path.length < 2) return [];
let uncertainIndex = false;
return node.elements.flatMap<ReferencedValue>((element, index) => {
if (element === null) return [];
if (t.isSpreadElement(element)) {
uncertainIndex = true;
return [{ node: element.argument, path: [null, ...remaining] }];
}
return uncertainIndex || key === null || String(key) === String(index)
? [{ node: element, path: remaining }]
: [];
});
if (t.isObjectExpression(node) || t.isArrayExpression(node)) {
if (effect === "write" && path.length < 2) return [];
return t.isObjectExpression(node)
? objectReferencedValues(node, path)
: arrayReferencedValues(node, path);
}
if (t.isAssignmentExpression(node))
return node.operator === "||=" || node.operator === "??="
? [
{ node: node.left, path },
{ node: node.right, path },
]
: [{ node: node.right, path }];
if (t.isAwaitExpression(node)) return [{ node: node.argument, path }];
if (t.isSequenceExpression(node)) {
const last = node.expressions.at(-1);
return last === undefined ? [] : [{ node: last, path }];
}
if (t.isConditionalExpression(node))
return [
@@ -244,7 +538,169 @@ const referencedValues = (
{ node: node.left, path },
{ node: node.right, path },
];
if (t.isAssignmentExpression(node, { operator: "&&=" }))
return [{ node: node.right, path }];
return [];
};
const objectReferencedValues = (
node: t.ObjectExpression,
path: PropertyPath,
): readonly ReferencedValue[] => {
const [key, ...remaining] = path;
const references: ReferencedValue[] = [];
const overwritten = new Set<string>();
for (const property of [...node.properties].reverse()) {
if (t.isSpreadElement(property)) {
const selected = key ?? null;
if (typeof selected !== "object" && overwritten.has(String(selected)))
continue;
references.push({
node: property.argument,
path: [
typeof selected === "object"
? {
...(selected ?? {}),
excludedKeys: [
...new Set([
...(selected?.excludedKeys ?? []),
...overwritten,
]),
].sort(compareUnicodeCodePoints),
}
: selected,
...remaining,
],
});
continue;
}
const name = semanticStaticPropertyKey(property.key, property.computed);
if (name !== null && overwritten.has(name)) continue;
if (
t.isObjectProperty(property) &&
(name === null || semanticPropertyPathKeyMatches(key ?? null, name))
)
references.push({ node: property.value, path: remaining });
// A prototype setter does not replace an own property from a spread.
if (
name !== null &&
!(t.isObjectMethod(property) && property.kind !== "method") &&
!(
t.isObjectProperty(property) &&
!property.computed &&
!property.shorthand &&
name === "__proto__"
)
)
overwritten.add(name);
}
return references;
};
const arrayReferencedValues = (
node: t.ArrayExpression,
path: PropertyPath,
): readonly ReferencedValue[] => {
const [key, ...remaining] = path;
const selected = key ?? null;
const selectedIndex =
typeof selected === "object" ? null : semanticArrayIndex(selected);
if (typeof selected !== "object" && selectedIndex === null) return [];
const references: ReferencedValue[] = [];
let minimumIndex = 0;
let uncertainIndex = false;
for (const element of node.elements) {
if (t.isSpreadElement(element)) {
if (selectedIndex === null || selectedIndex >= minimumIndex) {
let sourceKey: PropertyPath[number] = null;
if (!uncertainIndex) {
if (selectedIndex !== null) sourceKey = selectedIndex - minimumIndex;
else if (selected !== null && typeof selected === "object")
sourceKey = {
excludedKeys: selected.excludedKeys.flatMap((name) => {
const index = semanticArrayIndex(name);
return index === null || index < minimumIndex
? []
: [String(index - minimumIndex)];
}),
startIndex: Math.max(
0,
(selected.startIndex ?? 0) - minimumIndex,
),
};
}
references.push({
node: element.argument,
path: [sourceKey, ...remaining],
iterableFallbackPath: [],
});
}
uncertainIndex = true;
continue;
}
if (
element !== null &&
(uncertainIndex
? selectedIndex === null || selectedIndex >= minimumIndex
: semanticPropertyPathKeyMatches(selected, String(minimumIndex)))
)
references.push({ node: element, path: remaining });
minimumIndex++;
}
return references;
};
const copiedReferencePath = (
initializer: JavaScriptSemanticBindingState["referenceInitializers"][number],
path: PropertyPath,
effect: ValueEffect,
): PropertyPath | null => {
const offset = initializer.copyProjectionOffset;
if (offset === undefined) return [...initializer.projection, ...path];
const copiedPath = [...initializer.projection.slice(offset + 1), ...path];
if (effect === "write" && copiedPath.length < 2) return null;
let [key, ...remaining] = copiedPath;
if (initializer.copyKind === "array-rest") {
const startIndex = initializer.copyStartIndex ?? 0;
if (key === undefined || key === null)
key = { excludedKeys: [], startIndex };
else if (typeof key === "object")
key = {
excludedKeys: key.excludedKeys.flatMap((name) => {
const index = semanticArrayIndex(name);
return index === null ? [] : [String(index + startIndex)];
}),
startIndex: (key.startIndex ?? 0) + startIndex,
};
else {
const index = semanticArrayIndex(key);
if (index === null) return null;
key = index + startIndex;
}
} else {
const excludedKeys = initializer.copyExcludedKeys ?? [];
if (key === undefined || key === null) key = { excludedKeys };
else if (typeof key === "object")
key = {
...key,
excludedKeys: [...new Set([...key.excludedKeys, ...excludedKeys])].sort(
compareUnicodeCodePoints,
),
};
else if (excludedKeys.includes(String(key))) return null;
}
return [...initializer.projection.slice(0, offset), key, ...remaining];
};
const selectedReferenceSlot = (
source: { readonly node: t.Node; readonly projection: PropertyPath },
state: JavaScriptSemanticAnalysisState,
) => {
const path: string[] = [];
for (const key of source.projection) {
if (key === null || typeof key === "object") return undefined;
path.push(String(key));
}
return semanticSlotAtPath(
evaluateSemanticExpression(source.node, state),
path,
);
};