Files
rea/src/domain/javascript/javascriptSemanticObjectReferences.ts
T

863 lines
29 KiB
TypeScript

import * as t from "@babel/types";
import {
resolveSemanticBindingState,
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import {
isRuntimeJavaScriptClassProperty,
semanticStaticPropertyKey,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import {
semanticArrayIndex,
semanticPropertyPathKeyMatches,
type JavaScriptSemanticPropertyPath as PropertyPath,
} from "./javascriptSemanticPropertyPaths.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import {
SemanticGetterQuery,
type GetterQueryJob,
} from "./javascriptSemanticGetterQueries.js";
import { expandSemanticGetterResult } from "./javascriptSemanticGetterResults.js";
export interface JavaScriptSemanticObjectReference {
readonly node: t.Node;
readonly path: PropertyPath;
/** This selection crossed the object's prototype, rather than an own slot. */
readonly inherited?: true;
/** A shallow copy selects this prefix now while retaining its child identity. */
readonly capturedSelectionAt?: t.Node;
readonly ownEnumerable?: true;
readonly iterableFallbackPath?: PropertyPath;
readonly escapes?: true;
/** A retry omitted suffixes, so escaping constructed children must cover them. */
readonly widenedGetterResult?: true;
/** The allocation owned by this binding, rather than a read of its prior value. */
readonly ownerInitializer?: t.Node;
}
export const semanticClassCandidates = (
root: t.Node,
state: JavaScriptSemanticAnalysisState,
): readonly (t.ClassDeclaration | t.ClassExpression)[] => {
const pending: t.Node[] = [root];
const seenNodes = new Set<t.Node>();
const seenBindings = new Set<string>();
const classes: (t.ClassDeclaration | t.ClassExpression)[] = [];
for (let cursor = 0; cursor < pending.length; cursor++) {
const node = pending[cursor];
if (node === undefined) continue;
const expression = unwrapJavaScriptExpression(node).node;
if (seenNodes.has(expression)) continue;
seenNodes.add(expression);
if (t.isClass(expression)) {
classes.push(expression);
} else if (t.isIdentifier(expression)) {
const binding = resolveSemanticBindingState(
state,
expression,
expression.name,
);
if (binding === undefined || seenBindings.has(binding.bindingId))
continue;
seenBindings.add(binding.bindingId);
for (const initializer of binding.initializers)
pending.push(initializer.node);
} else if (
t.isConditionalExpression(expression) ||
t.isLogicalExpression(expression)
) {
pending.push(
t.isConditionalExpression(expression)
? expression.consequent
: expression.left,
t.isConditionalExpression(expression)
? expression.alternate
: expression.right,
);
} else if (t.isSequenceExpression(expression)) {
const last = expression.expressions.at(-1);
if (last !== undefined) pending.push(last);
}
}
return classes;
};
const instanceFieldReferences = (
root: t.ClassDeclaration | t.ClassExpression,
propertyName: string | null,
path: PropertyPath,
state: JavaScriptSemanticAnalysisState,
initializedBefore?: t.ClassProperty,
): {
readonly references: readonly JavaScriptSemanticObjectReference[];
readonly definiteNames: ReadonlySet<string>;
} => {
const references: JavaScriptSemanticObjectReference[] = [];
const pending = [{ klass: root, excluded: new Set<string>() }];
const selections = new Map<t.Node, ReadonlySet<string>>();
let definiteNames: ReadonlySet<string> | undefined;
for (let cursor = 0; cursor < pending.length; cursor++) {
const current = pending[cursor];
if (current === undefined) continue;
const previous = selections.get(current.klass);
// A shared base can be reached through different derived classes. Only
// exclusions common to those paths can suppress a field in that base.
const excluded =
previous === undefined
? current.excluded
: new Set([...previous].filter((name) => current.excluded.has(name)));
if (previous !== undefined && previous.size === excluded.size) continue;
selections.set(current.klass, excluded);
const shadowed = new Set(excluded);
const before =
initializedBefore === undefined
? -1
: current.klass.body.body.indexOf(initializedBefore);
const members =
before < 0
? current.klass.body.body
: current.klass.body.body.slice(0, before);
for (const member of [...members].reverse()) {
if (!isRuntimeJavaScriptClassProperty(member) || member.static) continue;
const key = semanticStaticPropertyKey(member.key, member.computed);
if (key !== null && shadowed.has(key)) continue;
if (key !== null) shadowed.add(key);
if (
(propertyName === null || key === null || key === propertyName) &&
member.value != null
)
references.push({ node: member.value, path });
}
const bases =
current.klass.superClass == null
? []
: semanticClassCandidates(current.klass.superClass, state);
if (bases.length === 0)
definiteNames =
definiteNames === undefined
? shadowed
: new Set([...definiteNames].filter((name) => shadowed.has(name)));
for (const klass of bases) pending.push({ klass, excluded: shadowed });
}
return { references, definiteNames: definiteNames ?? new Set() };
};
interface ClassPropertyLookup {
readonly owner: t.ClassDeclaration | t.ClassExpression;
readonly receiver?: t.ClassDeclaration | t.ClassExpression;
readonly kind: "instance" | "static" | "prototype";
readonly fieldsOnly?: true;
readonly initializedBefore?: t.ClassProperty;
}
const classPropertyDescriptors = (
klass: t.ClassDeclaration | t.ClassExpression,
propertyName: string | null,
kind: ClassPropertyLookup["kind"],
initializedBefore?: t.ClassProperty,
) => {
const isStatic = kind === "static";
const selectedGetters: t.ClassMethod[] = [];
const fields: t.ClassProperty[] = [];
const indexedMembers: {
readonly member: t.ClassMethod | t.ClassProperty;
readonly index: number;
readonly key: string | null;
}[] = [];
const before =
initializedBefore === undefined
? -1
: klass.body.body.indexOf(initializedBefore);
klass.body.body.forEach((member, index) => {
if (
before >= 0 &&
index >= before &&
isRuntimeJavaScriptClassProperty(member)
)
return;
if (
(t.isClassMethod(member) ||
(kind === "static" && isRuntimeJavaScriptClassProperty(member))) &&
member.static === isStatic
)
indexedMembers.push({
member,
index,
key: semanticStaticPropertyKey(member.key, member.computed),
});
});
const matchingMembers =
propertyName === null
? []
: indexedMembers.filter(({ key }) => key === propertyName);
const uncertainGetters = indexedMembers.filter(
(entry): entry is typeof entry & { member: t.ClassMethod } => {
const { member, key } = entry;
return (
t.isClassMethod(member) &&
member.kind === "get" &&
(propertyName === null || (member.computed && key === null))
);
},
);
const getters = matchingMembers.filter(
(entry): entry is typeof entry & { member: t.ClassMethod } =>
t.isClassMethod(entry.member) && entry.member.kind === "get",
);
const getter = getters.at(-1);
const getterIndex = getter?.index ?? -1;
const laterDataMember = matchingMembers.some(
({ member, index }) =>
t.isClassProperty(member) ||
(t.isClassMethod(member) &&
member.kind === "method" &&
index > getterIndex),
);
const seenFields = new Set<string>();
for (const { member, key } of [...indexedMembers].reverse()) {
if (!t.isClassProperty(member)) continue;
if (key !== null && seenFields.has(key)) continue;
if (key !== null) seenFields.add(key);
if (propertyName === null || key === null || key === propertyName)
fields.push(member);
}
if (getters.length > 0 && !laterDataMember && getter !== undefined)
selectedGetters.push(getter.member);
for (const { member, index } of uncertainGetters)
if (
!matchingMembers.some(
({ member: matching, index: matchingIndex }) =>
(t.isClassProperty(matching) ||
(t.isClassMethod(matching) &&
(matching.kind === "method" || matching.kind === "get"))) &&
matchingIndex > index,
)
)
selectedGetters.push(member);
return {
getters: selectedGetters,
fields,
inherits: matchingMembers.length === 0,
};
};
export const semanticClassPropertyReferences = (
lookup: ClassPropertyLookup,
selection: PropertyPath,
state: JavaScriptSemanticAnalysisState,
): readonly JavaScriptSemanticObjectReference[] => {
const query = new SemanticGetterQuery(state);
return query.run(
() => classPropertyReferences(lookup, selection, state, { query }),
(job) => expandGetterResult(job, query),
);
};
class ClassReferenceWalk {
readonly #references: JavaScriptSemanticObjectReference[] = [];
readonly #pending: (t.ClassDeclaration | t.ClassExpression)[];
readonly #seen = new Set<t.Node>();
readonly #name: string | null;
readonly #path: PropertyPath;
#fields: ReadonlySet<string> = new Set();
constructor(
readonly lookup: ClassPropertyLookup,
selection: PropertyPath,
readonly state: JavaScriptSemanticAnalysisState,
readonly context: {
readonly query: SemanticGetterQuery;
readonly escapes?: boolean;
},
) {
const [key, ...path] = selection;
this.#name = key == null || typeof key === "object" ? null : String(key);
this.#path = path;
this.#pending = [lookup.owner];
}
#instanceFields(): boolean {
if (this.lookup.kind !== "instance") return false;
// Own fields, including inherited constructor fields, suppress prototype descriptors.
const fields = instanceFieldReferences(
this.lookup.owner,
this.#name,
this.#path,
this.state,
this.lookup.initializedBefore,
);
for (const reference of fields.references) this.#references.push(reference);
this.#fields = fields.definiteNames;
return this.#name !== null && this.#fields.has(this.#name);
}
#getter(
klass: t.ClassDeclaration | t.ClassExpression,
getter: t.ClassMethod,
): void {
const key = semanticStaticPropertyKey(getter.key, getter.computed);
if (key !== null && this.#fields.has(key)) return;
this.context.query.schedule(
{
getter,
owner: klass,
receiver: this.lookup.receiver ?? this.lookup.owner,
},
this.#path,
this.context.escapes,
);
}
#visit(klass: t.ClassDeclaration | t.ClassExpression): void {
const descriptors = classPropertyDescriptors(
klass,
this.#name,
this.lookup.kind,
this.lookup.initializedBefore,
);
for (const field of descriptors.fields)
if (field.value != null)
this.#references.push({ node: field.value, path: this.#path });
for (const getter of this.lookup.fieldsOnly ? [] : descriptors.getters)
this.#getter(klass, getter);
// Only a definite own descriptor can suppress possible superclass results.
if (descriptors.inherits && klass.superClass != null)
for (const base of semanticClassCandidates(klass.superClass, this.state))
this.#pending.push(base);
}
run(): readonly JavaScriptSemanticObjectReference[] {
if (this.#instanceFields()) return this.#references;
for (let cursor = 0; cursor < this.#pending.length; cursor++) {
const klass = this.#pending[cursor];
if (klass === undefined || this.#seen.has(klass)) continue;
this.#seen.add(klass);
this.#visit(klass);
}
return this.#references;
}
}
const classPropertyReferences = (
lookup: ClassPropertyLookup,
selection: PropertyPath,
state: JavaScriptSemanticAnalysisState,
context: { readonly query: SemanticGetterQuery; readonly escapes?: boolean },
): readonly JavaScriptSemanticObjectReference[] =>
new ClassReferenceWalk(lookup, selection, state, context).run();
const objectReceiverReference = (
object: t.ObjectExpression,
path: PropertyPath,
state: JavaScriptSemanticAnalysisState,
): readonly JavaScriptSemanticObjectReference[] => {
// A literal's enclosing binding owns its slots. Returning the literal
// itself would bypass invalidation of that binding's observed properties.
let current: t.Node = object;
const projection: PropertyPath[number][] = [];
while (true) {
const parent = state.parentsByNode.get(current);
if (
t.isVariableDeclarator(parent) &&
parent.init === current &&
t.isIdentifier(parent.id)
)
return [
{
node: parent.id,
path: [...projection, ...path],
ownerInitializer: current,
},
];
if (
t.isAssignmentExpression(parent, { operator: "=" }) &&
parent.right === current &&
t.isIdentifier(parent.left)
)
return [
{
node: parent.left,
path: [...projection, ...path],
ownerInitializer: current,
},
];
if (t.isArrayExpression(parent)) {
let minimumIndex = 0;
let uncertainIndex = false;
for (const element of parent.elements) {
if (element === current) break;
if (t.isSpreadElement(element)) uncertainIndex = true;
else minimumIndex++;
}
// A spread contributes an unknown number of slots. Retain the known
// prefix without treating the element's syntax position as its index.
projection.unshift(
uncertainIndex
? { excludedKeys: [], startIndex: minimumIndex }
: minimumIndex,
);
current = parent;
continue;
}
if (t.isObjectProperty(parent) && parent.value === current) {
projection.unshift(
semanticStaticPropertyKey(parent.key, parent.computed),
);
const container = state.parentsByNode.get(parent);
if (t.isObjectExpression(container)) {
current = container;
continue;
}
}
break;
}
return [{ node: object, path }];
};
// Getter return expressions use the receiver of the property read. Keep that
// identity when translating `this`, private fields, and superclass reads into
// the same reference paths used by ordinary member mutations.
/** Follow only data fields whose initializers retain a class receiver. */
const privateReceiverProjection = (
receiver: t.ClassDeclaration | t.ClassExpression,
owner: t.ClassDeclaration | t.ClassExpression,
path: PropertyPath,
isStatic: boolean,
state: JavaScriptSemanticAnalysisState,
): boolean => {
if (path.some((key) => key === null || typeof key === "object")) return false;
type Selection = { readonly key: string; readonly before?: t.ClassProperty };
const pending: {
receiver: typeof receiver;
path: readonly Selection[];
visits: ReadonlyMap<string, number>;
}[] = [
{
receiver,
path: path.map((key) => ({ key: String(key) })),
visits: new Map(),
},
];
const seen = new Set<string>();
const declaringClass = (field: t.ClassProperty) => {
const body = state.parentsByNode.get(field);
return body === undefined ? undefined : state.parentsByNode.get(body);
};
for (let cursor = 0; cursor < pending.length; cursor++) {
const current = pending[cursor];
if (current === undefined) break;
const [selection, ...remaining] = current.path;
if (selection === undefined) {
if (!isStatic || current.receiver === owner) return true;
continue;
}
const origin = JSON.stringify([
current.receiver.start,
selection.key,
selection.before?.start,
]);
const previous = current.visits.get(origin);
// Repeated fields may consume a finite getter path. An initializer cycle
// that preserves or grows it cannot establish another receiver identity.
if (previous !== undefined && previous <= current.path.length) continue;
const visits = new Map(current.visits).set(origin, current.path.length);
const identity = JSON.stringify([
current.receiver.start,
current.path.map(({ key, before }) => [key, before?.start]),
]);
if (seen.has(identity)) continue;
seen.add(identity);
const lookupOwner =
selection.before === undefined
? current.receiver
: declaringClass(selection.before);
if (!t.isClass(lookupOwner)) continue;
for (const reference of semanticClassPropertyReferences(
{
owner: lookupOwner,
receiver: current.receiver,
kind: isStatic ? "static" : "instance",
fieldsOnly: true,
...(selection.before === undefined
? {}
: { initializedBefore: selection.before }),
},
[selection.key],
state,
)) {
const field = state.parentsByNode.get(reference.node);
if (!t.isClassProperty(field)) continue;
const fieldOwner = declaringClass(field);
if (!t.isClass(fieldOwner)) continue;
const projection: Selection[] = [];
let value = unwrapJavaScriptExpression(reference.node).node;
while (
t.isMemberExpression(value) ||
t.isOptionalMemberExpression(value)
) {
const key = semanticStaticPropertyKey(value.property, value.computed);
if (key === null || t.isPrivateName(value.property)) break;
projection.unshift({ key, before: field });
value = unwrapJavaScriptExpression(value.object).node;
}
if (!t.isThisExpression(value)) continue;
// Instance fields retain the actual instance. Static field `this` belongs
// to its declaring class, even when a subclass inherits the field.
pending.push({
receiver: isStatic ? fieldOwner : current.receiver,
path: [...projection, ...remaining],
visits,
});
}
}
return false;
};
const expandGetterResult = (
job: GetterQueryJob,
query: SemanticGetterQuery,
): void => {
const state = query.state;
expandSemanticGetterResult(job, query, {
classReferences: (lookup, path, escapes) =>
classPropertyReferences(lookup, path, state, { query, escapes }),
objectReferences: (receiver, path) =>
objectReceiverReference(receiver, path, state),
privateReceiver: (receiver, owner, path, isStatic) =>
privateReceiverProjection(receiver, owner, path, isStatic, state),
bases: (node) => semanticClassCandidates(node, state),
});
};
type ObjectLiteralMember = t.ObjectExpression["properties"][number];
/** Property positions grouped by whether an exact key can reach them. */
interface ObjectPropertyIndex {
/** Spreads, computed keys and `__proto__` data entries reach every key. */
readonly keyIndependent: readonly number[];
readonly byName: ReadonlyMap<string, readonly number[]>;
}
// Mutation tracking resolves one key of the same literal per write, so the
// index removes the per-write scan of every property.
const propertyIndexes = new WeakMap<t.ObjectExpression, ObjectPropertyIndex>();
const objectPropertyIndex = (node: t.ObjectExpression): ObjectPropertyIndex => {
const cached = propertyIndexes.get(node);
if (cached !== undefined) return cached;
const keyIndependent: number[] = [];
const byName = new Map<string, number[]>();
node.properties.forEach((property, index) => {
const name = t.isSpreadElement(property)
? null
: semanticStaticPropertyKey(property.key, property.computed);
if (
name === null ||
(t.isObjectProperty(property) &&
!property.computed &&
!property.shorthand &&
name === "__proto__")
) {
keyIndependent.push(index);
return;
}
const positions = byName.get(name);
if (positions === undefined) byName.set(name, [index]);
else positions.push(index);
});
const index = { keyIndependent, byName };
propertyIndexes.set(node, index);
return index;
};
/**
* Properties in reverse source order that can affect `key`. For an exact key,
* another static name only updates bookkeeping for that other name, which
* this key never reads, so only same-name and key-independent entries remain.
*/
const relevantPropertiesInReverse = (
node: t.ObjectExpression,
key: PropertyPath[number] | undefined,
): readonly ObjectLiteralMember[] => {
if (typeof key !== "string" && typeof key !== "number")
return [...node.properties].reverse();
const { keyIndependent, byName } = objectPropertyIndex(node);
const named = byName.get(String(key)) ?? [];
const relevant: ObjectLiteralMember[] = [];
let left = keyIndependent.length - 1;
let right = named.length - 1;
while (left >= 0 || right >= 0) {
const fromLeft =
right < 0 ||
(left >= 0 && (keyIndependent[left] ?? -1) > (named[right] ?? -1));
const position = fromLeft ? keyIndependent[left--] : named[right--];
const property =
position === undefined ? undefined : node.properties[position];
if (property !== undefined) relevant.push(property);
}
return relevant;
};
export const semanticObjectPropertyReferences = (
node: t.ObjectExpression,
path: PropertyPath,
state: JavaScriptSemanticAnalysisState,
): readonly JavaScriptSemanticObjectReference[] => {
const query = new SemanticGetterQuery(state);
return query.run(
() => objectPropertyReferences(node, path, query),
(job) => expandGetterResult(job, query),
);
};
class ObjectReferenceWalk {
readonly #references: JavaScriptSemanticObjectReference[] = [];
readonly #overwritten = new Set<string>();
readonly #data = new Set<string>();
readonly #accessors = new Set<string>();
readonly #getters = new Set<string>();
readonly #setters = new Set<string>();
readonly #key: PropertyPath[number] | undefined;
readonly #remaining: PropertyPath;
constructor(
readonly node: t.ObjectExpression,
path: PropertyPath,
readonly query: SemanticGetterQuery,
) {
[this.#key, ...this.#remaining] = path;
}
#spreadKey(): PropertyPath[number] {
const selected = this.#key ?? null;
return typeof selected !== "object"
? selected
: {
...selected,
excludedKeys: [
...new Set([
...(selected?.excludedKeys ?? []),
...this.#overwritten,
]),
].sort(compareUnicodeCodePoints),
};
}
#spread(property: t.SpreadElement): void {
const selected = this.#key ?? null;
if (typeof selected !== "object" && this.#overwritten.has(String(selected)))
return;
this.#references.push({
node: property.argument,
path: [this.#spreadKey(), ...this.#remaining],
capturedSelectionAt: property.argument,
ownEnumerable: true,
});
if (
selected !== null &&
typeof selected !== "object" &&
t.isObjectExpression(property.argument) &&
objectExpressionDefinesKey(property.argument, String(selected))
) {
this.#overwritten.add(String(selected));
this.#data.add(String(selected));
}
}
#prototype(
property: t.ObjectMethod | t.ObjectProperty,
name: string | null,
): boolean {
if (
!t.isObjectProperty(property) ||
property.computed ||
property.shorthand ||
name !== "__proto__"
)
return false;
const selected = this.#key ?? null;
if (
t.isExpression(property.value) &&
(selected === null ||
typeof selected === "object" ||
(typeof selected === "string" &&
!objectExpressionDefinesKey(this.node, selected)))
)
this.#references.push({
node: property.value,
path: [selected, ...this.#remaining],
inherited: true,
});
return true;
}
#read(
property: t.ObjectMethod | t.ObjectProperty,
name: string | null,
): void {
// An earlier unknown computed key cannot replace a later definite slot.
// Spreads obey the same overwrite proof; paired named accessors below
// retain their separate getter/setter semantics.
if (
name === null &&
this.#key != null &&
typeof this.#key !== "object" &&
(t.isObjectMethod(property, { kind: "get" })
? this.#data.has(String(this.#key)) ||
this.#getters.has(String(this.#key))
: this.#overwritten.has(String(this.#key)))
)
return;
if (
name !== null &&
!semanticPropertyPathKeyMatches(this.#key ?? null, name)
)
return;
if (t.isObjectMethod(property)) {
if (property.kind === "get")
this.query.schedule(
{ getter: property, owner: this.node },
this.#remaining,
);
// A paired getter remains readable even when its setter appears later.
if (property.kind === "get" || property.kind === "set") return;
}
this.#references.push({
node: t.isObjectProperty(property) ? property.value : property,
path: this.#remaining,
});
}
#record(name: string, kind: "get" | "set" | "data"): void {
this.#overwritten.add(name);
if (kind === "data") this.#data.add(name);
else {
this.#accessors.add(name);
(kind === "get" ? this.#getters : this.#setters).add(name);
}
}
#visit(property: ObjectLiteralMember): void {
if (t.isSpreadElement(property)) {
this.#spread(property);
return;
}
const name = semanticStaticPropertyKey(property.key, property.computed);
if (this.#prototype(property, name)) return;
const kind =
t.isObjectMethod(property) && property.kind !== "method"
? property.kind
: "data";
if (
name !== null &&
(kind === "data"
? this.#data.has(name) || this.#accessors.has(name)
: this.#data.has(name) ||
(kind === "get" ? this.#getters : this.#setters).has(name))
)
return;
this.#read(property, name);
if (name !== null) this.#record(name, kind);
}
run(): readonly JavaScriptSemanticObjectReference[] {
for (const property of relevantPropertiesInReverse(this.node, this.#key))
this.#visit(property);
return this.#references;
}
}
const objectPropertyReferences = (
node: t.ObjectExpression,
path: PropertyPath,
query: SemanticGetterQuery,
): readonly JavaScriptSemanticObjectReference[] =>
new ObjectReferenceWalk(node, path, query).run();
const objectExpressionDefinesKey = (
root: t.ObjectExpression,
key: string,
): boolean => {
const pending: t.ObjectExpression[] = [root];
const seen = new Set<t.ObjectExpression>();
for (let cursor = 0; cursor < pending.length; cursor++) {
const object = pending[cursor];
if (object === undefined || seen.has(object)) continue;
seen.add(object);
for (const property of object.properties) {
if (t.isSpreadElement(property)) {
if (t.isObjectExpression(property.argument))
pending.push(property.argument);
continue;
}
const name = semanticStaticPropertyKey(property.key, property.computed);
if (
name === key &&
!(
t.isObjectProperty(property) &&
!property.computed &&
!property.shorthand &&
name === "__proto__"
)
)
return true;
}
}
return false;
};
export const semanticArrayPropertyReferences = (
node: t.ArrayExpression,
path: PropertyPath,
): readonly JavaScriptSemanticObjectReference[] => {
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: JavaScriptSemanticObjectReference[] = [];
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],
capturedSelectionAt: element.argument,
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;
};