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
This commit is contained in:
ShuaixinHuang authored and GitHub committed 2026-10-20 17:25:19 +08:00
1 parent d532b18123
commit e9fc8fb30c
149 files changed
+16881 -988

No files matched your search

@@ -5,14 +5,43 @@ import {
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import { semanticCallableResultExpressions } from "./javascriptSemanticCallableResults.js";
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
import {
semanticArrayIndex,
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. */
@@ -24,34 +53,23 @@ export interface SemanticReturnedReference {
interface Candidate {
readonly node: t.Node;
/** `undefined` means any projection depth after different projection depths merge. */
readonly path: readonly (string | number | null)[] | undefined;
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";
}
const joinProjection = (
left: Candidate["path"],
right: Candidate["path"],
): Candidate["path"] => {
if (left === undefined || right === undefined || left.length !== right.length)
return undefined;
return left.map((key, index) => (key === right[index] ? key : null));
};
const sameProjection = (
left: Candidate["path"],
right: Candidate["path"],
): boolean =>
left === undefined
? right === undefined
: right !== undefined &&
left.length === right.length &&
left.every((key, index) => key === right[index]);
interface ObjectTargetIndex {
readonly propertyByName: ReadonlyMap<
string,
@@ -71,49 +89,301 @@ interface ObjectTargetUnknown {
/** Resolve local result references conservatively, without claiming call execution. */
export const createSemanticReturnedReferences = (
state: JavaScriptSemanticAnalysisState,
): ((callee: t.Node) => SemanticReturnedReferenceResult) => {
const results = new WeakMap<t.Node, SemanticReturnedReferenceResult>();
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) => {
return (callee, invocation) => {
const existing = results.get(callee);
if (existing !== undefined) return existing;
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: t.Node[] = [];
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 projections = new WeakMap<t.Node, Candidate["path"]>();
const queued = new WeakSet<t.Node>();
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" };
const enqueue = (node: t.Node, path: Candidate["path"]): void => {
const source = unwrapJavaScriptExpression(node).node;
if (!projections.has(source)) {
projections.set(source, path);
} else {
const previous = projections.get(source);
const joined = joinProjection(previous, path);
if (sameProjection(previous, joined)) return;
projections.set(source, joined);
result.precision = "widened";
}
if (!queued.has(source)) {
work.push(source);
queued.add(source);
}
// 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",
);
};
enqueue(callee, []);
// 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 node = work.pop();
if (node === undefined) break;
queued.delete(node);
const current: Candidate = { node, path: projections.get(node) };
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) continue;
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,
@@ -123,14 +393,31 @@ export const createSemanticReturnedReferences = (
...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)
@@ -149,6 +436,15 @@ export const createSemanticReturnedReferences = (
pending.push({
...current,
node: receiver,
readAt: current.readAt ?? node,
...(current.capturedSelections === undefined
? {}
: {
capturedSelections: semanticShiftCapturedSelections(
current.capturedSelections,
path.length,
),
}),
path:
current.path === undefined
? undefined
@@ -171,19 +467,94 @@ export const createSemanticReturnedReferences = (
} else if (t.isSequenceExpression(node)) {
const last = node.expressions.at(-1);
if (last !== undefined) pending.push({ ...current, node: last });
} else if (
t.isFunction(node) &&
(current.path === undefined || current.path.length === 0)
)
functions.add(node);
else if (t.isObjectExpression(node))
} 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 ?? [];
const index =
current.path === undefined || key === undefined || key === null
? null
: semanticArrayIndex(key);
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;
@@ -193,12 +564,28 @@ export const createSemanticReturnedReferences = (
...current,
node: element.argument,
path: current.path === undefined ? undefined : [null, ...path],
capturedSelections: [
...(current.capturedSelections ?? []),
{ at: element.argument, depth: 1 },
],
});
} else if (index === null || uncertainOffset || index === offset)
} 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))
@@ -212,14 +599,25 @@ export const createSemanticReturnedReferences = (
member.kind === "constructor") ||
current.path === undefined ||
(key !== undefined &&
(key === null ||
semanticStaticPropertyKey(member.key, member.computed) ===
String(key)))
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) &&
@@ -227,14 +625,26 @@ export const createSemanticReturnedReferences = (
member.value !== undefined &&
(key !== undefined || current.path === undefined) &&
(current.path === undefined ||
key === null ||
semanticStaticPropertyKey(member.key, member.computed) ===
String(key))
(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)
@@ -242,17 +652,45 @@ export const createSemanticReturnedReferences = (
}
for (let index = pending.length - 1; index >= 0; index--) {
const child = pending[index];
if (child !== undefined) enqueue(child.node, child.path);
if (child !== undefined)
enqueue(
child.node,
child.path,
child.readAt,
child.capturedSelections,
child.invocation ?? current.invocation,
);
}
}
const output = [...functions].flatMap((callable) =>
semanticCallableResultExpressions(callable).flatMap(
({ node, delegated }) =>
node === null ? [] : [{ node, projection: delegated ? [null] : [] }],
),
);
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);
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;
@@ -267,7 +705,17 @@ const selectObjectTargets = (
): void => {
const [key, ...path] = item.path ?? [];
const childPath = item.path === undefined ? undefined : path;
if (key !== null && key !== undefined) {
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) {
@@ -287,11 +735,20 @@ const selectObjectTargets = (
? 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;
}
@@ -300,7 +757,14 @@ const selectObjectTargets = (
for (const property of [...node.properties].reverse()) {
if (replaced) break;
if (t.isSpreadElement(property)) {
pending.push({ ...item, node: property.argument });
pending.push({
...item,
node: property.argument,
capturedSelections: [
...(item.capturedSelections ?? []),
{ at: property.argument, depth: 1, ownEnumerable: true },
],
});
continue;
}
const name = semanticStaticPropertyKey(property.key, property.computed);
@@ -308,13 +772,31 @@ const selectObjectTargets = (
key !== null &&
key !== undefined &&
name !== null &&
name !== String(key)
!semanticPropertyPathKeyMatches(key, name)
)
continue;
if (t.isObjectProperty(property))
pending.push({ ...item, node: property.value, path: childPath });
else pending.push({ ...item, node: property, path: childPath });
if (key !== null && key !== undefined && name === String(key))
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;
}
};