fix(javascript): preserve container ownership for getter mutations (#2765)

This commit is contained in:
To3akaRin
2026-10-13 20:10:39 +08:00
committed by GitHub
parent f974353c32
commit eeeb4ca379
5 changed files with 233 additions and 28 deletions
@@ -14,9 +14,11 @@ import {
} from "./javascriptSemanticPropertyPaths.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
interface ReferencedValue {
export interface JavaScriptSemanticObjectReference {
readonly node: t.Node;
readonly path: PropertyPath;
/** The allocation owned by this binding, rather than a read of its prior value. */
readonly ownerInitializer?: t.Node;
}
export const semanticClassCandidates = (
@@ -72,10 +74,10 @@ const instanceFieldReferences = (
path: PropertyPath,
state: JavaScriptSemanticAnalysisState,
): {
readonly references: readonly ReferencedValue[];
readonly references: readonly JavaScriptSemanticObjectReference[];
readonly definiteNames: ReadonlySet<string>;
} => {
const references: ReferencedValue[] = [];
const references: JavaScriptSemanticObjectReference[] = [];
const pending = [{ klass: root, excluded: new Set<string>() }];
const selections = new Map<t.Node, ReadonlySet<string>>();
let definiteNames: ReadonlySet<string> | undefined;
@@ -208,7 +210,7 @@ export const semanticClassPropertyReferences = (
selection: PropertyPath,
state: JavaScriptSemanticAnalysisState,
activeGetters: ReadonlyMap<t.Node, number> = new Map(),
): readonly ReferencedValue[] => {
): readonly JavaScriptSemanticObjectReference[] => {
const root = lookup.owner;
const receiver = lookup.receiver ?? root;
const [key, ...path] = selection;
@@ -216,7 +218,7 @@ export const semanticClassPropertyReferences = (
key == null || typeof key === "object" ? null : String(key);
const pending: (t.ClassDeclaration | t.ClassExpression)[] = [root];
const seen = new Set<t.Node>();
const possibleResults: ReferencedValue[] = [];
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();
@@ -267,11 +269,11 @@ const objectReceiverReference = (
object: t.ObjectExpression,
path: PropertyPath,
state: JavaScriptSemanticAnalysisState,
): readonly ReferencedValue[] => {
): 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: (string | number | null)[] = [];
const projection: PropertyPath[number][] = [];
while (true) {
const parent = state.parentsByNode.get(current);
if (
@@ -279,7 +281,43 @@ const objectReceiverReference = (
parent.init === current &&
t.isIdentifier(parent.id)
)
return [{ node: parent.id, path: [...projection, ...path] }];
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),
@@ -312,7 +350,7 @@ const getterResultReferences = (
path: PropertyPath,
state: JavaScriptSemanticAnalysisState,
activeGetters: ReadonlyMap<t.Node, number> = new Map(),
): readonly ReferencedValue[] => {
): readonly JavaScriptSemanticObjectReference[] => {
const { getter, owner, receiver = owner } = invocation;
const previousLength = activeGetters.get(getter);
if (previousLength !== undefined && previousLength <= path.length) return [];
@@ -323,7 +361,7 @@ const getterResultReferences = (
const resolve = (
node: t.Node,
remaining: PropertyPath,
): readonly ReferencedValue[] => {
): readonly JavaScriptSemanticObjectReference[] => {
const expression = unwrapJavaScriptExpression(node).node;
if (t.isThisExpression(expression)) {
if (t.isClass(receiver)) {
@@ -405,9 +443,9 @@ export const semanticObjectPropertyReferences = (
node: t.ObjectExpression,
path: PropertyPath,
state: JavaScriptSemanticAnalysisState,
): readonly ReferencedValue[] => {
): readonly JavaScriptSemanticObjectReference[] => {
const [key, ...remaining] = path;
const references: ReferencedValue[] = [];
const references: JavaScriptSemanticObjectReference[] = [];
const overwritten = new Set<string>();
const dataOverwritten = new Set<string>();
const accessorNames = new Set<string>();