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:
morluto
2026-10-16 06:06:00 +08:00
parent 461eb16128
commit 880b3a510b
17 changed files with 1308 additions and 524 deletions
@@ -251,6 +251,7 @@ export function* collectSemanticMemberMutationsSteps(
readonly effect: ValueEffect;
readonly mutation?: t.Node;
readonly originAt: t.Node | undefined;
readonly widenedGetterResult?: true;
}[] = [];
const iterableReferences: IterableReference[] = [];
const iterableReferenceKeys = new WeakMap<t.Node, Set<string>>();
@@ -266,6 +267,7 @@ export function* collectSemanticMemberMutationsSteps(
reference.originAt?.start,
reference.originAt?.end,
reference.iterationOnly,
reference.widenedGetterResult === true,
reference.requiredSources?.map((source) => [
source.node.start,
source.node.end,
@@ -291,6 +293,7 @@ export function* collectSemanticMemberMutationsSteps(
mutation?: t.Node,
originAt: t.Node | undefined = mutation,
ownerInitializer?: t.Node,
widenedGetterResult = false,
): void => {
const pending: (ReferencedValue & {
readonly bindings: ReadonlySet<string>;
@@ -392,6 +395,7 @@ export function* collectSemanticMemberMutationsSteps(
mutation,
current.originAt,
current.ownerInitializer,
widenedGetterResult,
);
continue;
}
@@ -477,7 +481,7 @@ export function* collectSemanticMemberMutationsSteps(
current.originAt?.start ?? -1
}\u0001${current.originAt?.end ?? -1}\u0001${
current.ownerInitializer?.start ?? -1
}\u0001${current.ownerInitializer?.end ?? -1}`;
}\u0001${current.ownerInitializer?.end ?? -1}\u0001${widenedGetterResult}`;
let covering = coveringPaths.get(pathState);
if (covering === undefined) {
covering = new SemanticPropertyPathCoverage();
@@ -531,6 +535,7 @@ export function* collectSemanticMemberMutationsSteps(
path: copiedPath,
bindings: nested,
effect,
...(widenedGetterResult ? { widenedGetterResult: true } : {}),
originAt: initializer.node,
...(mutation === undefined ? {} : { mutation }),
});
@@ -550,9 +555,17 @@ export function* collectSemanticMemberMutationsSteps(
});
} else {
for (const value of effect === "escape"
? escapedReferenceLeaves(expression, current.path)
? escapedReferenceLeaves(
expression,
current.path,
widenedGetterResult,
)
: expandedValues(expression, current.path, effect)) {
if (value.escapes && effect !== "escape")
if (
value.escapes &&
(effect !== "escape" ||
(value.widenedGetterResult && !widenedGetterResult))
)
markValue(
value.node,
value.path,
@@ -561,6 +574,7 @@ export function* collectSemanticMemberMutationsSteps(
mutation,
current.originAt,
value.ownerInitializer,
widenedGetterResult || value.widenedGetterResult === true,
);
else if (value.iterableFallbackPath !== undefined)
deferIterable({
@@ -571,6 +585,7 @@ export function* collectSemanticMemberMutationsSteps(
bindings: current.bindings,
effect,
originAt: current.originAt,
...(widenedGetterResult ? { widenedGetterResult: true } : {}),
...(mutation === undefined ? {} : { mutation }),
});
else
@@ -592,6 +607,7 @@ export function* collectSemanticMemberMutationsSteps(
expression: t.Node,
path: PropertyPath,
effect: ValueEffect,
widenedGetterResult = false,
): readonly ReferencedValue[] {
if (
t.isArrayExpression(expression) &&
@@ -599,7 +615,20 @@ export function* collectSemanticMemberMutationsSteps(
(path[0] === "constructor" && path[1] === "prototype"))
)
arrayIterationUnknown = true;
return referencedValues(expression, path, effect, state);
const references = referencedValues(
expression,
path,
effect,
state,
widenedGetterResult,
);
return widenedGetterResult
? references.map((reference) => ({
...reference,
escapes: true,
widenedGetterResult: true,
}))
: references;
}
// Expanding a value below its references depends only on syntax, not on the
// escape site. Escaping an object reaches every method's results, so
@@ -613,53 +642,79 @@ export function* collectSemanticMemberMutationsSteps(
function escapedReferenceLeaves(
root: t.Node,
rootPath: PropertyPath,
widenedGetterResult: boolean,
): readonly ReferencedValue[] {
// Parser recovery can leave an argument hole, which expands to nothing.
if (!t.isNode(root)) return [];
const rootKey = JSON.stringify(rootPath);
const rootKey = JSON.stringify([rootPath, widenedGetterResult]);
const cached = escapedLeaves.get(root)?.get(rootKey);
if (cached !== undefined) return cached;
const leaves = collectEscapedLeaves([
...expandedValues(root, rootPath, "escape", widenedGetterResult),
]);
const byPath = escapedLeaves.get(root) ?? new Map();
byPath.set(rootKey, leaves);
escapedLeaves.set(root, byPath);
return leaves;
}
function retainEscapedLeaf(
value: ReferencedValue,
node: t.Node,
leaves: ReferencedValue[],
identities: Set<string>,
): boolean {
if (
value.iterableFallbackPath !== undefined ||
t.isMemberExpression(node) ||
t.isOptionalMemberExpression(node)
) {
leaves.push(value);
return true;
}
if (!t.isIdentifier(node)) return false;
const binding = resolveBinding(node, node.name);
const identity = JSON.stringify([
binding === undefined ? null : binding.bindingId,
binding === undefined ? node.name : null,
value.path,
value.widenedGetterResult === true,
value.ownerInitializer?.start,
value.ownerInitializer?.end,
]);
if (!identities.has(identity)) {
identities.add(identity);
leaves.push(value);
}
return true;
}
function collectEscapedLeaves(
pending: ReferencedValue[],
): readonly ReferencedValue[] {
const leaves: ReferencedValue[] = [];
const leafIdentities = new Set<string>();
const identities = new Set<string>();
const expanded = new WeakMap<t.Node, Set<string>>();
const pending = [...expandedValues(root, rootPath, "escape")];
for (let cursor = 0; cursor < pending.length; cursor++) {
const value = pending[cursor];
if (value === undefined) break;
const node = unwrapJavaScriptExpression(value.node).node;
if (!t.isNode(node)) continue;
const pathKey = JSON.stringify(value.path);
if (t.isIdentifier(node) && value.iterableFallbackPath === undefined) {
const binding = resolveBinding(node, node.name);
const identity = JSON.stringify([
binding === undefined ? null : binding.bindingId,
binding === undefined ? node.name : null,
pathKey,
value.ownerInitializer?.start,
value.ownerInitializer?.end,
]);
if (leafIdentities.has(identity)) continue;
leafIdentities.add(identity);
leaves.push(value);
} else if (
value.iterableFallbackPath !== undefined ||
t.isIdentifier(node) ||
t.isMemberExpression(node) ||
t.isOptionalMemberExpression(node)
)
leaves.push(value);
else {
const paths = expanded.get(node) ?? new Set<string>();
if (paths.has(pathKey)) continue;
paths.add(pathKey);
expanded.set(node, paths);
for (const reference of expandedValues(node, value.path, "escape"))
pending.push(reference);
}
if (!t.isNode(node) || retainEscapedLeaf(value, node, leaves, identities))
continue;
const key = JSON.stringify([
value.path,
value.widenedGetterResult === true,
]);
const paths = expanded.get(node) ?? new Set<string>();
if (paths.has(key)) continue;
paths.add(key);
expanded.set(node, paths);
for (const reference of expandedValues(
node,
value.path,
"escape",
value.widenedGetterResult === true,
))
pending.push(reference);
}
const byPath = escapedLeaves.get(root) ?? new Map();
byPath.set(rootKey, leaves);
escapedLeaves.set(root, byPath);
return leaves;
}
const markReceiver = (callee: t.Node, mutation: t.Node): void => {
@@ -743,6 +798,9 @@ export function* collectSemanticMemberMutationsSteps(
fallbackPath: [],
bindings: reference.bindings,
effect: reference.effect,
...(reference.widenedGetterResult
? { widenedGetterResult: true }
: {}),
originAt: initializer.node,
...(reference.mutation === undefined
? {}
@@ -761,6 +819,9 @@ export function* collectSemanticMemberMutationsSteps(
fallbackPath: projection,
bindings: reference.bindings,
effect: reference.effect,
...(reference.widenedGetterResult
? { widenedGetterResult: true }
: {}),
originAt: reference.originAt,
...(reference.mutation === undefined
? {}
@@ -774,6 +835,8 @@ export function* collectSemanticMemberMutationsSteps(
reference.effect,
reference.mutation,
reference.originAt,
undefined,
reference.widenedGetterResult === true,
);
}
for (const [index, reference] of [...iterableReferences].entries()) {
@@ -803,6 +866,8 @@ export function* collectSemanticMemberMutationsSteps(
knownArray ? reference.effect : "escape",
reference.mutation,
reference.originAt,
undefined,
reference.widenedGetterResult === true,
);
}
if (
@@ -899,6 +964,7 @@ const referencedValues = (
path: PropertyPath,
effect: ValueEffect,
state: JavaScriptSemanticAnalysisState,
widenedGetterResult = false,
): readonly ReferencedValue[] => {
if (effect === "escape" && t.isFunction(node))
return semanticCallableResultExpressions(node).flatMap(
@@ -912,30 +978,47 @@ const referencedValues = (
? semanticObjectPropertyReferences(node, path, state)
: arrayReferencedValues(node, path);
}
if (t.isClass(node)) {
const [key, ...remaining] = path;
if (key === undefined) return [];
// Replacing or deleting the getter's property does not use the value it
// would return. Only a deeper path can mutate the returned object.
if (effect === "write" && remaining.length === 0) return [];
return semanticClassPropertyReferences(
{ owner: node, kind: "static" },
if (t.isClass(node) || t.isNewExpression(node))
return constructedReferenceValues(
node,
path,
effect,
state,
widenedGetterResult,
);
return branchReferenceValues(node, path);
};
const constructedReferenceValues = (
node: t.ClassDeclaration | t.ClassExpression | t.NewExpression,
path: PropertyPath,
effect: ValueEffect,
state: JavaScriptSemanticAnalysisState,
widenedGetterResult: boolean,
): readonly ReferencedValue[] => {
// Only retry results lost a suffix that could reach any constructed field.
// Preserve that reachability through aliases and nested allocations, without
// broadening ordinary whole-receiver escapes.
const [key = widenedGetterResult ? null : undefined, ...remaining] = path;
// Replacing a receiver property does not mutate the object it previously held.
if (key === undefined || (effect === "write" && remaining.length === 0))
return [];
const classes = t.isClass(node)
? [node]
: semanticClassCandidates(node.callee, state);
return classes.flatMap((owner) =>
semanticClassPropertyReferences(
{ owner, kind: t.isClass(node) ? "static" : "instance" },
[key, ...remaining],
state,
);
}
if (t.isNewExpression(node)) {
const [key, ...remaining] = path;
if (key === undefined) return [];
if (effect === "write" && remaining.length === 0) return [];
return semanticClassCandidates(node.callee, state).flatMap((klass) =>
semanticClassPropertyReferences(
{ owner: klass, kind: "instance" },
[key, ...remaining],
state,
),
);
}
),
);
};
const branchReferenceValues = (
node: t.Node,
path: PropertyPath,
): readonly ReferencedValue[] => {
if (t.isAssignmentExpression(node))
return node.operator === "||=" || node.operator === "??="
? [