Skip to content

Commit 3caeeb1

Browse files
authored
Memoize class binding when compiling private methods and static elements (#15701)
* add test cases * fix: memoise class binding when lowering static elements and private methods * update test fixtures * update test fixtures
1 parent 2290647 commit 3caeeb1

333 files changed

Lines changed: 1755 additions & 863 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/babel-helper-create-class-features-plugin/src/fields.ts

Lines changed: 94 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,8 @@ type ReplaceThisState = {
956956
innerBinding: t.Identifier | null;
957957
};
958958

959+
type ReplaceInnerBindingReferenceState = ReplaceThisState;
960+
959961
const thisContextVisitor = traverse.visitors.merge<ReplaceThisState>([
960962
{
961963
UnaryExpression(path) {
@@ -984,7 +986,7 @@ const thisContextVisitor = traverse.visitors.merge<ReplaceThisState>([
984986
environmentVisitor,
985987
]);
986988

987-
const innerReferencesVisitor: Visitor<ReplaceThisState> = {
989+
const innerReferencesVisitor: Visitor<ReplaceInnerBindingReferenceState> = {
988990
ReferencedIdentifier(path, state) {
989991
if (
990992
path.scope.bindingIdentifierEquals(path.node.name, state.innerBinding)
@@ -998,42 +1000,23 @@ const innerReferencesVisitor: Visitor<ReplaceThisState> = {
9981000
function replaceThisContext(
9991001
path: PropPath,
10001002
ref: t.Identifier,
1001-
getSuperRef: () => t.Identifier,
1002-
file: File,
1003-
isStaticBlock: boolean,
1004-
constantSuper: boolean,
10051003
innerBindingRef: t.Identifier | null,
10061004
) {
10071005
const state: ReplaceThisState = {
10081006
classRef: ref,
10091007
needsClassRef: false,
10101008
innerBinding: innerBindingRef,
10111009
};
1012-
1013-
const replacer = new ReplaceSupers({
1014-
methodPath: path,
1015-
constantSuper,
1016-
file,
1017-
refToPreserve: ref,
1018-
getSuperRef,
1019-
getObjectRef() {
1020-
state.needsClassRef = true;
1021-
// @ts-expect-error: TS doesn't infer that path.node is not a StaticBlock
1022-
return t.isStaticBlock?.(path.node) || path.node.static
1023-
? ref
1024-
: t.memberExpression(ref, t.identifier("prototype"));
1025-
},
1026-
});
1027-
replacer.replace();
1028-
if (isStaticBlock || path.isProperty()) {
1010+
if (!path.isMethod()) {
1011+
// replace `this` in property initializers and static blocks
10291012
path.traverse(thisContextVisitor, state);
10301013
}
10311014

10321015
// todo: use innerBinding.referencePaths to avoid full traversal
10331016
if (
10341017
innerBindingRef != null &&
10351018
state.classRef?.name &&
1036-
state.classRef.name !== innerBindingRef?.name
1019+
state.classRef.name !== innerBindingRef.name
10371020
) {
10381021
path.traverse(innerReferencesVisitor, state);
10391022
}
@@ -1075,23 +1058,47 @@ function inheritPropComments<T extends t.Node>(node: T, prop: PropPath) {
10751058
return node;
10761059
}
10771060

1061+
/**
1062+
* ClassRefFlag records the requirement of the class binding reference.
1063+
*
1064+
* @enum {number}
1065+
*/
1066+
const enum ClassRefFlag {
1067+
None,
1068+
/**
1069+
* When this flag is enabled, the binding reference can be the class id,
1070+
* if exists, or the uid identifier generated for class expression. The
1071+
* reference is safe to be consumed by [[Define]].
1072+
*/
1073+
ForDefine = 1 << 0,
1074+
/**
1075+
* When this flag is enabled, the reference must be a uid, because the outer
1076+
* class binding can be mutated by user codes.
1077+
* E.g.
1078+
* class C { static p = C }; const oldC = C; C = null; oldC.p;
1079+
* we must memoize class `C` before defining the property `p`.
1080+
*/
1081+
ForInnerBinding = 1 << 1,
1082+
}
1083+
10781084
export function buildFieldsInitNodes(
1079-
ref: t.Identifier,
1085+
ref: t.Identifier | null,
10801086
superRef: t.Expression | undefined,
10811087
props: PropPath[],
10821088
privateNamesMap: PrivateNamesMap,
1083-
state: File,
1089+
file: File,
10841090
setPublicClassFields: boolean,
10851091
privateFieldsAsProperties: boolean,
10861092
constantSuper: boolean,
1087-
innerBindingRef: t.Identifier,
1093+
innerBindingRef: t.Identifier | null,
10881094
) {
1089-
let needsClassRef = false;
1095+
let classRefFlags = ClassRefFlag.None;
10901096
let injectSuperRef: t.Identifier;
10911097
const staticNodes: t.Statement[] = [];
10921098
const instanceNodes: t.Statement[] = [];
10931099
// These nodes are pure and can be moved to the closest statement position
10941100
const pureStaticNodes: t.FunctionDeclaration[] = [];
1101+
let classBindingNode: t.ExpressionStatement | null = null;
10951102

10961103
const getSuperRef = t.isIdentifier(superRef)
10971104
? () => superRef
@@ -1101,6 +1108,10 @@ export function buildFieldsInitNodes(
11011108
return injectSuperRef;
11021109
};
11031110

1111+
const classRefForInnerBinding =
1112+
ref ?? props[0].scope.generateUidIdentifier("class");
1113+
ref ??= t.cloneNode(innerBindingRef);
1114+
11041115
for (const prop of props) {
11051116
prop.isClassProperty() && ts.assertFieldTransformed(prop);
11061117

@@ -1113,17 +1124,36 @@ export function buildFieldsInitNodes(
11131124
const isMethod = !isField;
11141125
const isStaticBlock = prop.isStaticBlock?.();
11151126

1127+
if (isStatic) classRefFlags |= ClassRefFlag.ForDefine;
1128+
11161129
if (isStatic || (isMethod && isPrivate) || isStaticBlock) {
1130+
new ReplaceSupers({
1131+
methodPath: prop,
1132+
constantSuper,
1133+
file: file,
1134+
refToPreserve: innerBindingRef,
1135+
getSuperRef,
1136+
getObjectRef() {
1137+
classRefFlags |= ClassRefFlag.ForInnerBinding;
1138+
if (isStatic || isStaticBlock) {
1139+
return classRefForInnerBinding;
1140+
} else {
1141+
return t.memberExpression(
1142+
classRefForInnerBinding,
1143+
t.identifier("prototype"),
1144+
);
1145+
}
1146+
},
1147+
}).replace();
1148+
11171149
const replaced = replaceThisContext(
11181150
prop,
1119-
ref,
1120-
getSuperRef,
1121-
state,
1122-
isStaticBlock,
1123-
constantSuper,
1151+
classRefForInnerBinding,
11241152
innerBindingRef,
11251153
);
1126-
needsClassRef = needsClassRef || replaced;
1154+
if (replaced) {
1155+
classRefFlags |= ClassRefFlag.ForInnerBinding;
1156+
}
11271157
}
11281158

11291159
// TODO(ts): there are so many `ts-expect-error` inside cases since
@@ -1149,14 +1179,12 @@ export function buildFieldsInitNodes(
11491179
break;
11501180
}
11511181
case isStatic && isPrivate && isField && privateFieldsAsProperties:
1152-
needsClassRef = true;
11531182
staticNodes.push(
11541183
// @ts-expect-error checked in switch
11551184
buildPrivateFieldInitLoose(t.cloneNode(ref), prop, privateNamesMap),
11561185
);
11571186
break;
11581187
case isStatic && isPrivate && isField && !privateFieldsAsProperties:
1159-
needsClassRef = true;
11601188
staticNodes.push(
11611189
// @ts-expect-error checked in switch
11621190
buildPrivateStaticFieldInitSpec(prop, privateNamesMap),
@@ -1170,17 +1198,15 @@ export function buildFieldsInitNodes(
11701198
// not going to happen.
11711199
// @ts-expect-error checked in switch
11721200
if (!isNameOrLength(prop.node)) {
1173-
needsClassRef = true;
11741201
// @ts-expect-error checked in switch
11751202
staticNodes.push(buildPublicFieldInitLoose(t.cloneNode(ref), prop));
11761203
break;
11771204
}
11781205
// falls through
11791206
case isStatic && isPublic && isField && !setPublicClassFields:
1180-
needsClassRef = true;
11811207
staticNodes.push(
11821208
// @ts-expect-error checked in switch
1183-
buildPublicFieldInitSpec(t.cloneNode(ref), prop, state),
1209+
buildPublicFieldInitSpec(t.cloneNode(ref), prop, file),
11841210
);
11851211
break;
11861212
case isInstance && isPrivate && isField && privateFieldsAsProperties:
@@ -1196,7 +1222,7 @@ export function buildFieldsInitNodes(
11961222
// @ts-expect-error checked in switch
11971223
prop,
11981224
privateNamesMap,
1199-
state,
1225+
file,
12001226
),
12011227
);
12021228
break;
@@ -1225,7 +1251,7 @@ export function buildFieldsInitNodes(
12251251
// @ts-expect-error checked in switch
12261252
prop,
12271253
privateNamesMap,
1228-
state,
1254+
file,
12291255
),
12301256
);
12311257
pureStaticNodes.push(
@@ -1238,7 +1264,6 @@ export function buildFieldsInitNodes(
12381264
);
12391265
break;
12401266
case isStatic && isPrivate && isMethod && !privateFieldsAsProperties:
1241-
needsClassRef = true;
12421267
staticNodes.unshift(
12431268
// @ts-expect-error checked in switch
12441269
buildPrivateStaticFieldInitSpec(prop, privateNamesMap),
@@ -1253,13 +1278,12 @@ export function buildFieldsInitNodes(
12531278
);
12541279
break;
12551280
case isStatic && isPrivate && isMethod && privateFieldsAsProperties:
1256-
needsClassRef = true;
12571281
staticNodes.unshift(
12581282
buildPrivateStaticMethodInitLoose(
12591283
t.cloneNode(ref),
12601284
// @ts-expect-error checked in switch
12611285
prop,
1262-
state,
1286+
file,
12631287
privateNamesMap,
12641288
),
12651289
);
@@ -1279,18 +1303,29 @@ export function buildFieldsInitNodes(
12791303
case isInstance && isPublic && isField && !setPublicClassFields:
12801304
instanceNodes.push(
12811305
// @ts-expect-error checked in switch
1282-
buildPublicFieldInitSpec(t.thisExpression(), prop, state),
1306+
buildPublicFieldInitSpec(t.thisExpression(), prop, file),
12831307
);
12841308
break;
12851309
default:
12861310
throw new Error("Unreachable.");
12871311
}
12881312
}
12891313

1314+
if (classRefFlags & ClassRefFlag.ForInnerBinding && innerBindingRef != null) {
1315+
classBindingNode = t.expressionStatement(
1316+
t.assignmentExpression(
1317+
"=",
1318+
t.cloneNode(classRefForInnerBinding),
1319+
t.cloneNode(innerBindingRef),
1320+
),
1321+
);
1322+
}
1323+
12901324
return {
12911325
staticNodes: staticNodes.filter(Boolean),
12921326
instanceNodes: instanceNodes.filter(Boolean),
12931327
pureStaticNodes: pureStaticNodes.filter(Boolean),
1328+
classBindingNode,
12941329
wrapClass(path: NodePath<t.Class>) {
12951330
for (const prop of props) {
12961331
// Delete leading comments so that they don't get attached as
@@ -1310,16 +1345,21 @@ export function buildFieldsInitNodes(
13101345
);
13111346
}
13121347

1313-
if (!needsClassRef) return path;
1314-
1315-
if (path.isClassExpression()) {
1316-
path.scope.push({ id: ref });
1317-
path.replaceWith(
1318-
t.assignmentExpression("=", t.cloneNode(ref), path.node),
1319-
);
1320-
} else if (!path.node.id) {
1321-
// Anonymous class declaration
1322-
path.node.id = ref;
1348+
if (classRefFlags !== ClassRefFlag.None) {
1349+
if (path.isClassExpression()) {
1350+
path.scope.push({ id: ref });
1351+
path.replaceWith(
1352+
t.assignmentExpression("=", t.cloneNode(ref), path.node),
1353+
);
1354+
} else {
1355+
if (innerBindingRef == null) {
1356+
// export anonymous class declaration
1357+
path.node.id = ref;
1358+
}
1359+
if (classBindingNode != null) {
1360+
path.scope.push({ id: classRefForInnerBinding });
1361+
}
1362+
}
13231363
}
13241364

13251365
return path;

0 commit comments

Comments
 (0)