Files
rea/src/domain/javascript/javascriptSemanticReturnedReferences.ts
T
ShuaixinHuang e9fc8fb30c fix(javascript): preserve runtime path bytes and Electron ownership (#4941)
* 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
2026-10-20 17:25:19 +08:00

835 lines
30 KiB
TypeScript

import * as t from "@babel/types";
import {
semanticStaticPropertyKey,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import { semanticCallableResultExpressions } from "./javascriptSemanticCallableResults.js";
import {
javaScriptSourceInvocationContext,
javaScriptGenericInvocationMethod,
resolveJavaScriptGenericInvocation,
type JavaScriptSourceInvocationContext,
} from "./javascriptIntrinsicInvocation.js";
import {
semanticBindingAtRead,
semanticEffectTiming,
} from "./javascriptSemanticMutationInitializers.js";
import {
semanticReferenceSourcesInactive,
semanticKnownArrayIteration,
semanticKnownArrayIterationAt,
semanticSelectedReferenceSlot,
} from "./javascriptSemanticReferenceSources.js";
import { semanticArrayPropertyReferences } from "./javascriptSemanticObjectReferences.js";
import {
semanticNormalizeCapturedSelections,
semanticProjectedSourcesAtRead,
semanticShiftCapturedSelections,
type SemanticCapturedSelection,
} from "./javascriptSemanticProjectedSources.js";
import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js";
import {
semanticPropertyPathKeyMatches,
type JavaScriptSemanticPropertyPath,
} from "./javascriptSemanticPropertyPaths.js";
import {
SemanticAliasSelections,
semanticAliasSelectionChoices,
type SemanticAliasSelectionPath,
} from "./javascriptSemanticAliasSelections.js";
import {
resolveSemanticBindingState,
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticBindingState,
} from "./javascriptSemanticState.js";
/** A reference potentially handed to a caller or generator consumer. */
export interface SemanticReturnedReference {
readonly node: t.Node;
readonly projection: JavaScriptSemanticPropertyPath;
}
interface Candidate {
readonly node: t.Node;
/** `undefined` means any projection depth after different projection depths merge. */
readonly path: SemanticAliasSelectionPath | undefined;
/** Member selection happens at its use, while a captured function keeps its read. */
readonly readAt?: t.Node;
readonly capturedSelections?:
| readonly SemanticCapturedSelection[]
| undefined;
readonly invocation?: JavaScriptSourceInvocationContext;
}
export interface SemanticReturnedReferenceResult {
readonly references: readonly SemanticReturnedReference[];
/** A visible invocation-method mutation prevents a source-local result proof. */
readonly unresolvedInvocation?: boolean;
/** Both states close the supported source-local may-target graph. Widened joins projections without dropping represented targets; it does not claim runtime completeness. */
readonly precision: "exact" | "widened";
}
interface ObjectTargetIndex {
readonly propertyByName: ReadonlyMap<
string,
t.ObjectMethod | t.ObjectProperty
>;
readonly trailingUnknownsByProperty: ReadonlyMap<
t.ObjectMethod | t.ObjectProperty,
ObjectTargetUnknown
>;
}
interface ObjectTargetUnknown {
readonly node: t.ObjectMethod | t.ObjectProperty | t.SpreadElement;
readonly previous: ObjectTargetUnknown | undefined;
}
/** Resolve local result references conservatively, without claiming call execution. */
export const createSemanticReturnedReferences = (
state: JavaScriptSemanticAnalysisState,
consumption:
| "potential-reference"
| "direct-call"
| "awaited-call" = "potential-reference",
): ((
callee: t.Node,
invocation?: t.CallExpression | t.OptionalCallExpression,
) => SemanticReturnedReferenceResult) => {
const results = new WeakMap<
t.Node,
{
readonly result: SemanticReturnedReferenceResult;
readonly invocation: t.Node | undefined;
readonly iterationUnknown: boolean;
readonly ambientWrites: number;
readonly dependencies: readonly {
readonly binding: JavaScriptSemanticBindingState;
readonly writes: number;
readonly escapes: number;
}[];
}
>();
const objectTargets = new WeakMap<t.ObjectExpression, ObjectTargetIndex>();
return (callee, invocation) => {
const existing = results.get(callee);
if (
existing !== undefined &&
existing.invocation === invocation &&
existing.iterationUnknown === state.arrayIterationUnknown &&
existing.ambientWrites === state.ambientWriteEffects.length &&
existing.dependencies.every(
({ binding, writes, escapes }) =>
binding.memberWrites.length === writes &&
binding.escapedPaths.length === escapes,
)
)
return existing.result;
const dependencies = new Map<
JavaScriptSemanticBindingState,
{ readonly writes: number; readonly escapes: number }
>();
const functions = new Set<t.Function>();
const work: Candidate[] = [];
// A transition depends on the source read and current projection, not the
// initializer history used to reach it. Keeping history out of the key lets
// shared diamonds and reassignment cycles converge to the graph fixed point.
const projectionDepths = new WeakMap<
t.Node,
Map<string, number | undefined>
>();
const expanded = new WeakMap<t.Node, Set<string>>();
let additionalContexts = 0;
let contextsWidened = false;
const result: {
references: readonly SemanticReturnedReference[];
precision: SemanticReturnedReferenceResult["precision"];
unresolvedInvocation?: boolean;
} = { references: [], precision: "exact" };
// The invocation owner retains the actual callable receiver. A bare alias
// of fn.call has lost that receiver and cannot execute fn by itself.
const nativeArrayIteration = (array: t.ArrayExpression) =>
semanticKnownArrayIterationAt(array, state);
const sourceInvocation =
invocation === undefined
? undefined
: javaScriptSourceInvocationContext(invocation, nativeArrayIteration);
const sourceGenericInvocation =
invocation !== undefined &&
javaScriptGenericInvocationMethod(invocation.callee) !== undefined;
const inheritedInvocationChanged = (method: "call" | "apply"): boolean => {
if (invocation === undefined) return false;
const inheritedPath = ["global", "Function", "prototype", method];
return state.ambientWriteEffects.some(
(effect) =>
effect.path.length <= inheritedPath.length &&
effect.path.every(
(key, index) =>
key === null ||
semanticPropertyPathKeyMatches(key, inheritedPath[index] ?? ""),
) &&
semanticEffectTiming(invocation, effect.node, state) !== "after",
);
};
// Exact finite alternatives retain unrelated keys and cross-path
// correlations. Growing/shrinking cycles still converge at any depth;
// selector-budget widening retains the complete may-target closure.
const selections = new SemanticAliasSelections(() => {
state.valueResourceLimits.add("alias-selections");
result.precision = "widened";
});
const enqueue = (
node: t.Node,
path: Candidate["path"],
readAt?: t.Node,
capturedSelections?: readonly SemanticCapturedSelection[],
invocationContext?: JavaScriptSourceInvocationContext,
): void => {
const source = unwrapJavaScriptExpression(node).node;
if (contextsWidened) {
path = undefined;
capturedSelections = undefined;
invocationContext = undefined;
}
const invocationKey = () =>
invocationContext === undefined
? null
: [
invocationContext.receiver?.start,
invocationContext.receiver?.end,
invocationContext.receiverUnknown,
invocationContext.arguments?.map((argument) => [
argument.start,
argument.end,
]),
];
let readKey = JSON.stringify([
readAt?.start ?? -1,
readAt?.end ?? -1,
invocationKey(),
]);
const depths = projectionDepths.get(source) ?? new Map();
projectionDepths.set(source, depths);
if (!depths.has(readKey)) {
depths.set(readKey, path?.length);
} else {
const previous = depths.get(readKey);
if (previous === undefined || previous !== path?.length) {
if (previous !== undefined) result.precision = "widened";
depths.set(readKey, undefined);
path = undefined;
}
}
if (path !== undefined) path = selections.normalize(source, path);
capturedSelections =
path === undefined
? undefined
: semanticNormalizeCapturedSelections(
capturedSelections,
path.length,
);
const expansionKey = () =>
JSON.stringify([
readKey,
path ?? null,
capturedSelections?.map(({ at, depth, ownEnumerable }) => [
at.start,
at.end,
depth,
ownEnumerable,
]),
]);
let key = expansionKey();
const paths = expanded.get(source) ?? new Set<string>();
if (paths.has(key)) return;
// Copy/invocation contexts add selector dimensions. Bound them by the
// same query budget, then visit the complete unknown-projection closure.
if (
!contextsWidened &&
(capturedSelections !== undefined || invocationContext !== undefined) &&
paths.size > 0 &&
++additionalContexts > SEMANTIC_ALIAS_SELECTION_LIMIT
) {
contextsWidened = true;
state.valueResourceLimits.add("alias-selections");
result.precision = "widened";
// Generic transitions select only the source receiver and authored
// argument values. Keep that complete finite source closure when their
// precise positional contexts merge, including later callable receivers.
if (sourceInvocation !== undefined) {
if (sourceInvocation.receiver !== undefined)
enqueue(sourceInvocation.receiver, undefined);
for (const argument of sourceInvocation.arguments ?? [])
if (t.isExpression(argument)) enqueue(argument, undefined);
else if (t.isSpreadElement(argument))
enqueue(argument.argument, undefined);
}
}
if (contextsWidened) {
path = undefined;
capturedSelections = undefined;
invocationContext = undefined;
readKey = JSON.stringify([
readAt?.start ?? -1,
readAt?.end ?? -1,
null,
]);
if (sourceGenericInvocation) result.unresolvedInvocation = true;
depths.set(readKey, undefined);
key = expansionKey();
if (paths.has(key)) return;
}
paths.add(key);
expanded.set(source, paths);
work.push({
node: source,
path,
...(readAt === undefined ? {} : { readAt }),
...(capturedSelections === undefined ? {} : { capturedSelections }),
...(invocationContext === undefined
? {}
: { invocation: invocationContext }),
});
};
enqueue(callee, [], undefined, undefined, sourceInvocation);
while (work.length > 0) {
const current = work.pop();
if (current === undefined) break;
const { node } = current;
// An exact empty selection with an invocation is the value being called,
// not a container whose callable children are potential escape targets.
// Ordinary objects and arrays cannot supply that callable receiver. An
// unknown projection still visits the full closure after budget widening.
if (
current.invocation !== undefined &&
current.path?.length === 0 &&
(t.isObjectExpression(node) || t.isArrayExpression(node))
)
continue;
const pending: Candidate[] = [];
if (t.isIdentifier(node)) {
const binding = resolveSemanticBindingState(state, node, node.name);
if (binding === undefined) {
if (sourceGenericInvocation && current.path?.length === 0)
result.unresolvedInvocation = true;
continue;
}
dependencies.set(binding, {
writes: binding.memberWrites.length,
escapes: binding.escapedPaths.length,
});
if (current.path !== undefined) {
if (
sourceGenericInvocation &&
current.path.length > 0 &&
semanticProjectedSourcesAtRead(
binding,
node,
current.path,
state,
true,
// The current invocation receives its callee before its own
// opaque effects; earlier captured member reads keep their site.
invocation !== undefined &&
current.readAt ===
unwrapJavaScriptExpression(invocation.callee).node
? invocation
: (current.readAt ?? node),
current.capturedSelections,
) === undefined
)
result.unresolvedInvocation = true;
const selectedSources = semanticProjectedSourcesAtRead(
binding,
node,
current.path,
state,
false,
current.readAt ?? node,
current.capturedSelections,
);
if (selectedSources !== undefined) {
for (const source of selectedSources) {
const capturedSelections = [
...(semanticShiftCapturedSelections(
current.capturedSelections,
source.path.length - current.path.length,
) ?? []),
...(source.capturedSelections ?? []),
];
pending.push({
...source,
...(current.path.length === 0
? {}
: { readAt: current.readAt ?? node }),
...(capturedSelections.length === 0
? {}
: { capturedSelections }),
});
}
for (let index = pending.length - 1; index >= 0; index--) {
const child = pending[index];
if (child !== undefined)
enqueue(
child.node,
child.path,
child.readAt,
child.capturedSelections,
child.invocation ?? current.invocation,
);
}
continue;
}
result.precision = "widened";
if (sourceGenericInvocation) result.unresolvedInvocation = true;
}
const selected = semanticBindingAtRead(
binding,
node,
state.parentsByNode,
).binding;
for (const initializer of [
...selected.initializers,
...selected.referenceInitializers,
]) {
if (semanticReferenceSourcesInactive(initializer, state)) continue;
pending.push({
node: initializer.node,
path:
current.path === undefined
? undefined
: [...initializer.projection, ...current.path],
...(current.path?.length === 0
? {}
: { readAt: current.readAt ?? node }),
...(current.capturedSelections === undefined
? {}
: {
capturedSelections: semanticShiftCapturedSelections(
current.capturedSelections,
initializer.projection.length,
),
}),
});
}
if (current.path === undefined)
for (const effect of binding.memberWrites)
for (const source of effect.assignedSources ?? [])
if (!semanticReferenceSourcesInactive(source, state))
pending.push({ node: source.node, path: undefined });
} else if (
t.isMemberExpression(node) ||
t.isOptionalMemberExpression(node)
) {
const path: (string | number | null)[] = [];
let receiver: t.Node = node;
while (
t.isMemberExpression(receiver) ||
t.isOptionalMemberExpression(receiver)
) {
path.push(
semanticStaticPropertyKey(receiver.property, receiver.computed),
);
receiver = unwrapJavaScriptExpression(receiver.object).node;
}
pending.push({
...current,
node: receiver,
readAt: current.readAt ?? node,
...(current.capturedSelections === undefined
? {}
: {
capturedSelections: semanticShiftCapturedSelections(
current.capturedSelections,
path.length,
),
}),
path:
current.path === undefined
? undefined
: [...path.reverse(), ...current.path],
});
} else if (
t.isConditionalExpression(node) ||
t.isLogicalExpression(node)
) {
pending.push(
{
...current,
node: t.isConditionalExpression(node) ? node.consequent : node.left,
},
{
...current,
node: t.isConditionalExpression(node) ? node.alternate : node.right,
},
);
} else if (t.isSequenceExpression(node)) {
const last = node.expressions.at(-1);
if (last !== undefined) pending.push({ ...current, node: last });
} else if (t.isFunction(node)) {
if (current.path === undefined || current.path.length === 0)
functions.add(node);
else if (
current.invocation !== undefined &&
current.path.every((key) =>
semanticAliasSelectionChoices(key).every(
(choice) => choice === "call" || choice === "apply",
),
)
) {
// Own member writes have already selected their actual RHS above.
// Only a remaining inherited invocation member reaches this branch.
const methods = semanticAliasSelectionChoices(
current.path.at(-1)!,
).filter(
(method): method is "call" | "apply" =>
method === "call" || method === "apply",
);
for (const method of methods) {
const selected = resolveJavaScriptGenericInvocation(
method,
current.invocation,
nativeArrayIteration,
);
if (selected !== undefined)
pending.push({
node: selected.callable,
path: [],
invocation: selected.context,
});
else if (current.invocation.receiverUnknown)
result.unresolvedInvocation = true;
if (inheritedInvocationChanged(method))
result.unresolvedInvocation = true;
}
}
} else if (t.isObjectExpression(node))
selectObjectTargets(node, current, pending, objectTargets);
else if (t.isArrayExpression(node)) {
const [key, ...path] = current.path ?? [];
if (current.path !== undefined && key !== undefined) {
for (const choice of semanticAliasSelectionChoices(key))
for (const source of semanticArrayPropertyReferences(node, [
choice,
])) {
let projection = source.path;
if (
source.capturedSelectionAt !== undefined &&
!semanticKnownArrayIteration(
semanticSelectedReferenceSlot(
{ node: source.node, projection: [] },
state,
source.capturedSelectionAt,
)?.value,
state,
)
)
projection = [null, ...source.path.slice(1)];
const selectedPath = [...projection, ...path];
pending.push({
...current,
node: source.node,
path: selectedPath,
capturedSelections: [
...(semanticShiftCapturedSelections(
current.capturedSelections,
selectedPath.length - current.path.length,
) ?? []),
...(source.capturedSelectionAt === undefined
? []
: [{ at: source.capturedSelectionAt, depth: 1 }]),
],
});
}
for (let index = pending.length - 1; index >= 0; index--) {
const child = pending[index];
if (child !== undefined)
enqueue(
child.node,
child.path,
child.readAt,
child.capturedSelections,
child.invocation ?? current.invocation,
);
}
continue;
}
let uncertainOffset = false;
node.elements.forEach((element, offset) => {
if (element === null) return;
if (t.isSpreadElement(element)) {
uncertainOffset = true;
pending.push({
...current,
node: element.argument,
path: current.path === undefined ? undefined : [null, ...path],
capturedSelections: [
...(current.capturedSelections ?? []),
{ at: element.argument, depth: 1 },
],
});
} else if (
current.path === undefined ||
key === undefined ||
uncertainOffset ||
semanticAliasSelectionChoices(key).some((choice) =>
semanticPropertyPathKeyMatches(choice, String(offset)),
)
)
pending.push({
...current,
node: element,
path: current.path === undefined ? undefined : path,
capturedSelections:
semanticShiftCapturedSelections(
current.capturedSelections,
-1,
) ?? [],
});
});
} else if (t.isNewExpression(node))
pending.push({ ...current, node: node.callee });
else if (t.isClass(node)) {
const [key, ...path] = current.path ?? [];
for (const member of node.body.body) {
if (t.isClassMethod(member)) {
if (
((current.path === undefined || current.path.length === 0) &&
member.kind === "constructor") ||
current.path === undefined ||
(key !== undefined &&
semanticAliasSelectionChoices(key).some((choice) => {
const name = semanticStaticPropertyKey(
member.key,
member.computed,
);
return (
name === null ||
semanticPropertyPathKeyMatches(choice, name)
);
}))
)
pending.push({
...current,
node: member,
path: current.path === undefined ? undefined : path,
capturedSelections: semanticShiftCapturedSelections(
current.capturedSelections,
-1,
),
});
} else if (
t.isClassProperty(member) &&
member.value !== null &&
member.value !== undefined &&
(key !== undefined || current.path === undefined) &&
(current.path === undefined ||
(key !== undefined &&
semanticAliasSelectionChoices(key).some((choice) => {
const name = semanticStaticPropertyKey(
member.key,
member.computed,
);
return (
name === null ||
semanticPropertyPathKeyMatches(choice, name)
);
})))
)
pending.push({
...current,
node: member.value,
path: current.path === undefined ? undefined : path,
capturedSelections: semanticShiftCapturedSelections(
current.capturedSelections,
-1,
),
});
}
if (node.superClass !== null && node.superClass !== undefined)
pending.push({ ...current, node: node.superClass });
}
for (let index = pending.length - 1; index >= 0; index--) {
const child = pending[index];
if (child !== undefined)
enqueue(
child.node,
child.path,
child.readAt,
child.capturedSelections,
child.invocation ?? current.invocation,
);
}
}
const output = [...functions].flatMap((callable) => {
// A direct generator/async call produces a fresh iterator/Promise, not
// the yielded/resolved contents. Escape analysis still retains those
// potential references for their eventual consumers.
if (
consumption !== "potential-reference" &&
(callable.generator ||
(consumption === "direct-call" && callable.async))
)
return [];
return semanticCallableResultExpressions(callable).flatMap(
({ node, delegated, kind }) =>
node === null ||
(consumption !== "potential-reference" && kind !== "return")
? []
: [{ node, projection: delegated ? [null] : [] }],
);
});
result.references = output;
results.set(callee, {
result,
invocation,
iterationUnknown: state.arrayIterationUnknown,
ambientWrites: state.ambientWriteEffects.length,
dependencies: [...dependencies].map(([binding, counts]) => ({
binding,
...counts,
})),
});
if (result.precision === "widened")
state.returnedReferenceWidenings.add(callee);
return result;
};
};
const selectObjectTargets = (
node: t.ObjectExpression,
item: Candidate,
pending: Candidate[],
indexes: WeakMap<t.ObjectExpression, ObjectTargetIndex>,
): void => {
const [key, ...path] = item.path ?? [];
const childPath = item.path === undefined ? undefined : path;
if (key !== null && typeof key === "object" && "alternatives" in key) {
for (const choice of semanticAliasSelectionChoices(key))
selectObjectTargets(
node,
{ ...item, path: [choice, ...path] },
pending,
indexes,
);
return;
}
if (key !== null && key !== undefined && typeof key !== "object") {
const index = objectTargetIndex(node, indexes);
const property = index.propertyByName.get(String(key));
if (property !== undefined) {
const unknowns: ObjectTargetUnknown[] = [];
for (
let unknown = index.trailingUnknownsByProperty.get(property);
unknown !== undefined;
unknown = unknown.previous
)
unknowns.push(unknown);
for (const { node: unknown } of unknowns.reverse())
pending.push({
...item,
node: t.isSpreadElement(unknown)
? unknown.argument
: t.isObjectProperty(unknown)
? unknown.value
: unknown,
...(t.isSpreadElement(unknown) ? {} : { path: childPath }),
capturedSelections: t.isSpreadElement(unknown)
? [
...(item.capturedSelections ?? []),
{ at: unknown.argument, depth: 1, ownEnumerable: true },
]
: (semanticShiftCapturedSelections(item.capturedSelections, -1) ??
[]),
});
pending.push({
...item,
node: t.isObjectProperty(property) ? property.value : property,
path: childPath,
capturedSelections:
semanticShiftCapturedSelections(item.capturedSelections, -1) ?? [],
});
return;
}
}
let replaced = false;
for (const property of [...node.properties].reverse()) {
if (replaced) break;
if (t.isSpreadElement(property)) {
pending.push({
...item,
node: property.argument,
capturedSelections: [
...(item.capturedSelections ?? []),
{ at: property.argument, depth: 1, ownEnumerable: true },
],
});
continue;
}
const name = semanticStaticPropertyKey(property.key, property.computed);
if (
key !== null &&
key !== undefined &&
name !== null &&
!semanticPropertyPathKeyMatches(key, name)
)
continue;
if (t.isObjectProperty(property))
pending.push({
...item,
node: property.value,
path: childPath,
capturedSelections:
semanticShiftCapturedSelections(item.capturedSelections, -1) ?? [],
});
else
pending.push({
...item,
node: property,
path: childPath,
capturedSelections:
semanticShiftCapturedSelections(item.capturedSelections, -1) ?? [],
});
if (
key !== null &&
key !== undefined &&
typeof key !== "object" &&
name === String(key)
)
replaced = true;
}
};
const objectTargetIndex = (
node: t.ObjectExpression,
indexes: WeakMap<t.ObjectExpression, ObjectTargetIndex>,
): ObjectTargetIndex => {
const existing = indexes.get(node);
if (existing !== undefined) return existing;
const propertyByName = new Map<string, t.ObjectMethod | t.ObjectProperty>();
const trailingUnknownsByProperty = new Map<
t.ObjectMethod | t.ObjectProperty,
ObjectTargetUnknown
>();
let trailingUnknown: ObjectTargetUnknown | undefined;
for (const property of [...node.properties].reverse()) {
if (t.isSpreadElement(property)) {
trailingUnknown = { node: property, previous: trailingUnknown };
continue;
}
const name = semanticStaticPropertyKey(property.key, property.computed);
if (name === null) {
trailingUnknown = { node: property, previous: trailingUnknown };
continue;
}
if (propertyByName.has(name)) continue;
propertyByName.set(name, property);
if (trailingUnknown !== undefined)
trailingUnknownsByProperty.set(property, trailingUnknown);
}
const created = { propertyByName, trailingUnknownsByProperty };
indexes.set(node, created);
return created;
};