mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 09:55:46 +00:00
fix(javascript): bound static getter reference expansion
Replace recursive getter histories with a query-owned worklist and memo. Bound precise expansion at 512 states and widen growing receiver cycles; retain escaped roots and constructed children rather than silently dropping suffixes. Carry the resource limit through semantic coverage, worker transport and graph Evidence while preserving direct writes, imports and unrelated literal facts. The small source probe reduced 65/326 paths to one widened reference for five and six getters. The original large Claude artifact was not rerun. Validate receiver/alias/mutation controls and compiled CLI/MCP consumers. Refs #3839, #3848 Co-authored-by: Yi Han <yhan606@aucklanduni.ac.nz>
This commit is contained in:
1 parent
461eb16128
commit
880b3a510b
17 files changed
+1308
-524
No files matched your search
@@ -3,9 +3,6 @@ import {
|
||||
resolveSemanticBindingState,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { semanticCallableResultExpressions } from "./javascriptSemanticCallableResults.js";
|
||||
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
|
||||
import { semanticReferenceSourcesInactive } from "./javascriptSemanticReferenceSources.js";
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
unwrapJavaScriptExpression,
|
||||
@@ -15,11 +12,18 @@ import {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
@@ -229,68 +233,106 @@ export const semanticClassPropertyReferences = (
|
||||
lookup: ClassPropertyLookup,
|
||||
selection: PropertyPath,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
activeGetters: ReadonlyMap<t.Node, number> = new Map(),
|
||||
): readonly JavaScriptSemanticObjectReference[] => {
|
||||
const root = lookup.owner;
|
||||
const receiver = lookup.receiver ?? root;
|
||||
const [key, ...path] = selection;
|
||||
const propertyName =
|
||||
key == null || typeof key === "object" ? null : String(key);
|
||||
const pending: (t.ClassDeclaration | t.ClassExpression)[] = [root];
|
||||
const seen = new Set<t.Node>();
|
||||
const possibleResults: JavaScriptSemanticObjectReference[] = [];
|
||||
// Instance fields are own properties, including fields initialized by base
|
||||
// constructors. They take precedence over every prototype descriptor.
|
||||
let instanceFields: ReadonlySet<string> = new Set();
|
||||
if (lookup.kind === "instance") {
|
||||
const fields = instanceFieldReferences(
|
||||
root,
|
||||
propertyName,
|
||||
path,
|
||||
state,
|
||||
lookup.initializedBefore,
|
||||
);
|
||||
possibleResults.push(...fields.references);
|
||||
instanceFields = fields.definiteNames;
|
||||
if (propertyName !== null && instanceFields.has(propertyName))
|
||||
return possibleResults;
|
||||
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];
|
||||
}
|
||||
for (let cursor = 0; cursor < pending.length; cursor++) {
|
||||
const klass = pending[cursor];
|
||||
if (klass === undefined || seen.has(klass)) continue;
|
||||
seen.add(klass);
|
||||
|
||||
#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,
|
||||
propertyName,
|
||||
lookup.kind,
|
||||
lookup.initializedBefore,
|
||||
this.#name,
|
||||
this.lookup.kind,
|
||||
this.lookup.initializedBefore,
|
||||
);
|
||||
for (const field of descriptors.fields)
|
||||
if (field.value != null)
|
||||
possibleResults.push({ node: field.value, path });
|
||||
for (const getter of lookup.fieldsOnly ? [] : descriptors.getters) {
|
||||
const key = semanticStaticPropertyKey(getter.key, getter.computed);
|
||||
if (key !== null && instanceFields.has(key)) continue;
|
||||
possibleResults.push(
|
||||
...getterResultReferences(
|
||||
{ getter, owner: klass, receiver },
|
||||
path,
|
||||
state,
|
||||
activeGetters,
|
||||
),
|
||||
);
|
||||
}
|
||||
// An exact own descriptor suppresses inherited lookup. Dynamic computed
|
||||
// descriptors may or may not match, so retain possible superclass results.
|
||||
if (
|
||||
descriptors.inherits &&
|
||||
klass.superClass !== null &&
|
||||
klass.superClass !== undefined
|
||||
)
|
||||
pending.push(...semanticClassCandidates(klass.superClass, state));
|
||||
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);
|
||||
}
|
||||
return possibleResults;
|
||||
};
|
||||
|
||||
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,
|
||||
@@ -363,15 +405,6 @@ const objectReceiverReference = (
|
||||
// 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.
|
||||
interface GetterInvocation {
|
||||
readonly getter: t.ObjectMethod | t.ClassMethod;
|
||||
readonly owner: t.ObjectExpression | t.ClassDeclaration | t.ClassExpression;
|
||||
readonly receiver?:
|
||||
| t.ObjectExpression
|
||||
| t.ClassDeclaration
|
||||
| t.ClassExpression;
|
||||
}
|
||||
|
||||
/** Follow only data fields whose initializers retain a class receiver. */
|
||||
const privateReceiverProjection = (
|
||||
receiver: t.ClassDeclaration | t.ClassExpression,
|
||||
@@ -468,276 +501,20 @@ const privateReceiverProjection = (
|
||||
return false;
|
||||
};
|
||||
|
||||
const getterResultReferences = (
|
||||
invocation: GetterInvocation,
|
||||
path: PropertyPath,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
activeGetters: ReadonlyMap<t.Node, number> = new Map(),
|
||||
): readonly JavaScriptSemanticObjectReference[] => {
|
||||
const { getter, owner, receiver = owner } = invocation;
|
||||
const previousLength = activeGetters.get(getter);
|
||||
if (previousLength !== undefined && previousLength <= path.length) return [];
|
||||
// Repeated receiver reads may revisit a getter while consuming a finite
|
||||
// path; recursive getters that preserve or grow that path cannot progress.
|
||||
const active = new Map(activeGetters).set(getter, path.length);
|
||||
const isStatic = t.isClassMethod(getter) && getter.static;
|
||||
interface AliasVisit {
|
||||
readonly identity: string;
|
||||
readonly pathLength: number;
|
||||
readonly parent: AliasVisit | undefined;
|
||||
}
|
||||
const pending: {
|
||||
readonly node: t.Node;
|
||||
readonly path: PropertyPath;
|
||||
readonly aliases: AliasVisit | undefined;
|
||||
readonly escapes?: true;
|
||||
readonly privateRead?: {
|
||||
readonly node: t.MemberExpression | t.OptionalMemberExpression;
|
||||
readonly name: string;
|
||||
readonly path: PropertyPath;
|
||||
};
|
||||
}[] = semanticCallableResultExpressions(getter).flatMap(({ node }) =>
|
||||
node === null ? [] : [{ node, path, aliases: undefined }],
|
||||
);
|
||||
const references: JavaScriptSemanticObjectReference[] = [];
|
||||
const expandedAliases = new Map<string, Set<string>>();
|
||||
const getterScope = state.scopeByNode.get(getter)?.scopeId;
|
||||
for (let cursor = 0; cursor < pending.length; cursor++) {
|
||||
const current = pending[cursor];
|
||||
if (current === undefined) break;
|
||||
const { path: remaining } = current;
|
||||
const expression = unwrapJavaScriptExpression(current.node).node;
|
||||
const retain = (reference: JavaScriptSemanticObjectReference): void => {
|
||||
references.push(
|
||||
current.escapes ? { ...reference, escapes: true } : reference,
|
||||
);
|
||||
};
|
||||
const retainPrivateRead = (): void => {
|
||||
if (current.privateRead !== undefined)
|
||||
retain({
|
||||
node: current.privateRead.node,
|
||||
path: current.escapes ? [] : current.privateRead.path,
|
||||
});
|
||||
};
|
||||
if (t.isIdentifier(expression)) {
|
||||
const binding = resolveSemanticBindingState(
|
||||
state,
|
||||
expression,
|
||||
expression.name,
|
||||
);
|
||||
let scope =
|
||||
binding === undefined
|
||||
? undefined
|
||||
: state.scopesById.get(binding.scopeId);
|
||||
while (scope !== undefined && scope.scopeId !== getterScope) {
|
||||
if (scope.kind === "function" || scope.parentScopeId === null) {
|
||||
scope = undefined;
|
||||
break;
|
||||
}
|
||||
scope = state.scopesById.get(scope.parentScopeId);
|
||||
}
|
||||
if (
|
||||
binding !== undefined &&
|
||||
scope !== undefined &&
|
||||
binding.referenceInitializers.every(
|
||||
(initializer) => initializer.copyKind === undefined,
|
||||
)
|
||||
) {
|
||||
const identity = JSON.stringify([
|
||||
binding.bindingId,
|
||||
expression.start,
|
||||
expression.end,
|
||||
]);
|
||||
const known = expandedAliases.get(identity);
|
||||
const key = JSON.stringify([
|
||||
remaining,
|
||||
current.privateRead?.name,
|
||||
current.privateRead?.path,
|
||||
current.escapes,
|
||||
]);
|
||||
let cyclic = false;
|
||||
let growing = false;
|
||||
if (known !== undefined)
|
||||
for (
|
||||
let visit = current.aliases;
|
||||
visit !== undefined;
|
||||
visit = visit.parent
|
||||
)
|
||||
if (visit.identity === identity) {
|
||||
cyclic = true;
|
||||
growing = remaining.length > visit.pathLength;
|
||||
break;
|
||||
}
|
||||
if (cyclic) {
|
||||
// A repeated alias can select arbitrarily deep children. Revisit its
|
||||
// roots as escapes rather than enumerating iterations or dropping them.
|
||||
if (growing && !current.escapes)
|
||||
pending.push({
|
||||
...current,
|
||||
path: [],
|
||||
aliases: undefined,
|
||||
escapes: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (known?.has(key)) continue;
|
||||
const selections = known ?? new Set<string>();
|
||||
selections.add(key);
|
||||
expandedAliases.set(identity, selections);
|
||||
const aliases = {
|
||||
identity,
|
||||
pathLength: remaining.length,
|
||||
parent: current.aliases,
|
||||
};
|
||||
// Unknown read ordering retains possible aliases for effect reachability.
|
||||
const { binding: origins } = semanticBindingAtRead(
|
||||
binding,
|
||||
expression,
|
||||
state.parentsByNode,
|
||||
);
|
||||
for (const initializer of [
|
||||
...origins.initializers,
|
||||
...origins.referenceInitializers.filter(
|
||||
(initializer) =>
|
||||
!semanticReferenceSourcesInactive(initializer, state),
|
||||
),
|
||||
])
|
||||
pending.push({
|
||||
...current,
|
||||
node: initializer.node,
|
||||
path: [...initializer.projection, ...remaining],
|
||||
aliases,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (t.isThisExpression(expression)) {
|
||||
// An outer binding or a nested ordinary function has a different receiver.
|
||||
let ownerNode = state.parentsByNode.get(expression);
|
||||
while (
|
||||
ownerNode !== undefined &&
|
||||
!t.isClass(ownerNode) &&
|
||||
!(t.isFunction(ownerNode) && !t.isArrowFunctionExpression(ownerNode))
|
||||
)
|
||||
ownerNode = state.parentsByNode.get(ownerNode);
|
||||
if (ownerNode !== getter) continue;
|
||||
if (current.privateRead !== undefined && t.isClass(owner)) {
|
||||
if (
|
||||
!t.isClass(receiver) ||
|
||||
!privateReceiverProjection(
|
||||
receiver,
|
||||
owner,
|
||||
remaining,
|
||||
isStatic,
|
||||
state,
|
||||
)
|
||||
) {
|
||||
retainPrivateRead();
|
||||
continue;
|
||||
}
|
||||
for (const member of owner.body.body)
|
||||
if (
|
||||
t.isClassPrivateProperty(member) &&
|
||||
member.static === isStatic &&
|
||||
member.key.id.name === current.privateRead.name &&
|
||||
member.value != null
|
||||
)
|
||||
retain({
|
||||
node: member.value,
|
||||
path: current.escapes ? [] : current.privateRead.path,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
for (const reference of t.isClass(receiver)
|
||||
? semanticClassPropertyReferences(
|
||||
{ owner: receiver, kind: isStatic ? "static" : "instance" },
|
||||
remaining,
|
||||
state,
|
||||
active,
|
||||
)
|
||||
: objectReceiverReference(receiver, remaining, state))
|
||||
retain(reference);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
t.isMemberExpression(expression) ||
|
||||
t.isOptionalMemberExpression(expression)
|
||||
) {
|
||||
if (
|
||||
current.privateRead !== undefined &&
|
||||
t.isPrivateName(expression.property)
|
||||
) {
|
||||
retainPrivateRead();
|
||||
continue;
|
||||
}
|
||||
if (t.isPrivateName(expression.property) && t.isClass(owner)) {
|
||||
pending.push({
|
||||
...current,
|
||||
node: expression.object,
|
||||
path: [],
|
||||
privateRead: {
|
||||
node: expression,
|
||||
name: expression.property.id.name,
|
||||
path: remaining,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const key = semanticStaticPropertyKey(
|
||||
expression.property,
|
||||
expression.computed,
|
||||
);
|
||||
if (
|
||||
t.isSuper(expression.object) &&
|
||||
t.isClass(owner) &&
|
||||
owner.superClass != null
|
||||
) {
|
||||
for (const base of semanticClassCandidates(owner.superClass, state))
|
||||
for (const reference of semanticClassPropertyReferences(
|
||||
{
|
||||
owner: base,
|
||||
receiver: t.isClass(receiver) ? receiver : owner,
|
||||
kind: isStatic ? "static" : "prototype",
|
||||
},
|
||||
[key, ...remaining],
|
||||
state,
|
||||
active,
|
||||
))
|
||||
retain(reference);
|
||||
continue;
|
||||
}
|
||||
pending.push({
|
||||
...current,
|
||||
node: expression.object,
|
||||
path: [key, ...remaining],
|
||||
});
|
||||
} else if (t.isConditionalExpression(expression))
|
||||
pending.push(
|
||||
{ ...current, node: expression.consequent },
|
||||
{ ...current, node: expression.alternate },
|
||||
);
|
||||
else if (t.isLogicalExpression(expression))
|
||||
pending.push(
|
||||
{ ...current, node: expression.left },
|
||||
{ ...current, node: expression.right },
|
||||
);
|
||||
else if (t.isSequenceExpression(expression)) {
|
||||
const last = expression.expressions.at(-1);
|
||||
if (last !== undefined) pending.push({ ...current, node: last });
|
||||
} else if (t.isAssignmentExpression(expression, { operator: "=" }))
|
||||
pending.push({ ...current, node: expression.right });
|
||||
else if (
|
||||
t.isAssignmentExpression(expression) &&
|
||||
["&&=", "||=", "??="].includes(expression.operator)
|
||||
)
|
||||
pending.push(
|
||||
{ ...current, node: expression.left },
|
||||
{ ...current, node: expression.right },
|
||||
);
|
||||
else if (current.privateRead !== undefined) retainPrivateRead();
|
||||
else retain({ node: expression, path: remaining });
|
||||
}
|
||||
return references;
|
||||
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];
|
||||
@@ -814,127 +591,160 @@ export const semanticObjectPropertyReferences = (
|
||||
path: PropertyPath,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): readonly JavaScriptSemanticObjectReference[] => {
|
||||
const [key, ...remaining] = path;
|
||||
const references: JavaScriptSemanticObjectReference[] = [];
|
||||
const overwritten = new Set<string>();
|
||||
const dataOverwritten = new Set<string>();
|
||||
const accessorNames = new Set<string>();
|
||||
const getterNames = new Set<string>();
|
||||
const setterNames = new Set<string>();
|
||||
for (const property of relevantPropertiesInReverse(node, key)) {
|
||||
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,
|
||||
],
|
||||
});
|
||||
if (
|
||||
selected !== null &&
|
||||
typeof selected !== "object" &&
|
||||
t.isObjectExpression(property.argument) &&
|
||||
objectExpressionDefinesKey(property.argument, String(selected))
|
||||
) {
|
||||
overwritten.add(String(selected));
|
||||
dataOverwritten.add(String(selected));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const name = semanticStaticPropertyKey(property.key, property.computed);
|
||||
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],
|
||||
});
|
||||
if (
|
||||
t.isObjectProperty(property) &&
|
||||
!property.computed &&
|
||||
!property.shorthand &&
|
||||
name === "__proto__"
|
||||
selected !== null &&
|
||||
typeof selected !== "object" &&
|
||||
t.isObjectExpression(property.argument) &&
|
||||
objectExpressionDefinesKey(property.argument, String(selected))
|
||||
) {
|
||||
const selected = key ?? null;
|
||||
if (
|
||||
(selected === null || typeof selected === "object") &&
|
||||
t.isExpression(property.value)
|
||||
) {
|
||||
references.push({
|
||||
node: property.value,
|
||||
path: [selected, ...remaining],
|
||||
});
|
||||
} else if (
|
||||
typeof selected === "string" &&
|
||||
!objectExpressionDefinesKey(node, selected) &&
|
||||
t.isExpression(property.value)
|
||||
) {
|
||||
references.push({
|
||||
node: property.value,
|
||||
path: [selected, ...remaining],
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const isGetter = t.isObjectMethod(property) && property.kind === "get";
|
||||
const isSetter = t.isObjectMethod(property) && property.kind === "set";
|
||||
if (
|
||||
name !== null &&
|
||||
(isGetter || isSetter
|
||||
? dataOverwritten.has(name) ||
|
||||
(isGetter ? getterNames.has(name) : setterNames.has(name))
|
||||
: dataOverwritten.has(name) || accessorNames.has(name))
|
||||
)
|
||||
continue;
|
||||
if (name === null || semanticPropertyPathKeyMatches(key ?? null, name)) {
|
||||
if (t.isObjectMethod(property) && property.kind === "get") {
|
||||
references.push(
|
||||
...getterResultReferences(
|
||||
{ getter: property, owner: node },
|
||||
remaining,
|
||||
state,
|
||||
),
|
||||
);
|
||||
} else if (t.isObjectMethod(property) && property.kind === "set") {
|
||||
// A setter contributes no readable reference. A paired getter is
|
||||
// handled independently when it appears earlier in this object.
|
||||
} else {
|
||||
references.push({
|
||||
node: t.isObjectProperty(property) ? property.value : property,
|
||||
path: remaining,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (name !== null) {
|
||||
if (isGetter) {
|
||||
getterNames.add(name);
|
||||
accessorNames.add(name);
|
||||
overwritten.add(name);
|
||||
} else if (isSetter) {
|
||||
setterNames.add(name);
|
||||
accessorNames.add(name);
|
||||
overwritten.add(name);
|
||||
} else if (
|
||||
!(
|
||||
t.isObjectProperty(property) &&
|
||||
!property.computed &&
|
||||
!property.shorthand &&
|
||||
name === "__proto__"
|
||||
)
|
||||
) {
|
||||
overwritten.add(name);
|
||||
dataOverwritten.add(name);
|
||||
}
|
||||
this.#overwritten.add(String(selected));
|
||||
this.#data.add(String(selected));
|
||||
}
|
||||
}
|
||||
return references;
|
||||
};
|
||||
|
||||
#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],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
#read(
|
||||
property: t.ObjectMethod | t.ObjectProperty,
|
||||
name: string | null,
|
||||
): void {
|
||||
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,
|
||||
|
||||
Reference in new issue
Block a user