Skip to content

Commit b0e6902

Browse files
committed
Handle private access chained on an optional chain
See the resolution from tc39/proposal-class-fields#301.
1 parent d030793 commit b0e6902

File tree

33 files changed

+2831
-29
lines changed

33 files changed

+2831
-29
lines changed

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

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,12 @@ const privateNameVisitor = {
7575
const { privateNamesMap } = this;
7676
const { node, parentPath } = path;
7777

78-
if (!parentPath.isMemberExpression({ property: node })) return;
78+
if (
79+
!parentPath.isMemberExpression({ property: node }) &&
80+
!parentPath.isOptionalMemberExpression({ property: node })
81+
) {
82+
return;
83+
}
7984
if (!privateNamesMap.has(node.id.name)) return;
8085

8186
this.handle(parentPath);
@@ -241,18 +246,28 @@ const privateNameHandlerSpec = {
241246
};
242247

243248
const privateNameHandlerLoose = {
244-
handle(member) {
249+
get(member) {
245250
const { privateNamesMap, file } = this;
246251
const { object } = member.node;
247252
const { name } = member.node.property.id;
248253

249-
member.replaceWith(
250-
template.expression`BASE(REF, PROP)[PROP]`({
251-
BASE: file.addHelper("classPrivateFieldLooseBase"),
252-
REF: object,
253-
PROP: privateNamesMap.get(name).id,
254-
}),
255-
);
254+
return template.expression`BASE(REF, PROP)[PROP]`({
255+
BASE: file.addHelper("classPrivateFieldLooseBase"),
256+
REF: object,
257+
PROP: privateNamesMap.get(name).id,
258+
});
259+
},
260+
261+
simpleSet(member) {
262+
return this.get(member);
263+
},
264+
265+
destructureSet(member) {
266+
return this.get(member);
267+
},
268+
269+
call(member, args) {
270+
return t.callExpression(this.get(member), args);
256271
},
257272
};
258273

@@ -264,21 +279,14 @@ export function transformPrivateNamesUsage(
264279
state,
265280
) {
266281
const body = path.get("body");
282+
const handler = loose ? privateNameHandlerLoose : privateNameHandlerSpec;
267283

268-
if (loose) {
269-
body.traverse(privateNameVisitor, {
270-
privateNamesMap,
271-
file: state,
272-
...privateNameHandlerLoose,
273-
});
274-
} else {
275-
memberExpressionToFunctions(body, privateNameVisitor, {
276-
privateNamesMap,
277-
classRef: ref,
278-
file: state,
279-
...privateNameHandlerSpec,
280-
});
281-
}
284+
memberExpressionToFunctions(body, privateNameVisitor, {
285+
privateNamesMap,
286+
classRef: ref,
287+
file: state,
288+
...handler,
289+
});
282290
}
283291

284292
function buildPrivateFieldInitLoose(ref, prop, privateNamesMap) {

packages/babel-helper-member-expression-to-functions/src/index.js

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,113 @@ const handle = {
3737
handle(member) {
3838
const { node, parent, parentPath } = member;
3939

40+
if (member.isOptionalMemberExpression()) {
41+
const root = member.find(({ node, parent, parentPath }) => {
42+
if (parentPath.isOptionalMemberExpression()) {
43+
return parent.optional || parent.object !== node;
44+
}
45+
if (parentPath.isOptionalCallExpression()) {
46+
return parent.optional || parent.callee !== node;
47+
}
48+
return true;
49+
});
50+
51+
const rootParentPath = root.parentPath;
52+
if (
53+
rootParentPath.isUpdateExpression({ argument: node }) ||
54+
rootParentPath.isAssignmentExpression({ left: node })
55+
) {
56+
throw member.buildCodeFrameError(`can't handle assignment`);
57+
}
58+
if (rootParentPath.isUnaryExpression({ operator: "delete" })) {
59+
throw member.buildCodeFrameError(`can't handle delete`);
60+
}
61+
62+
if (node.optional) {
63+
throw member.buildCodeFrameError(
64+
`can't handle '?.' directly before ${node.property.type}`,
65+
);
66+
}
67+
68+
let nearestOptional = member;
69+
for (;;) {
70+
if (nearestOptional.isOptionalMemberExpression()) {
71+
if (nearestOptional.node.optional) break;
72+
nearestOptional = nearestOptional.get("object");
73+
continue;
74+
} else if (nearestOptional.isOptionalCallExpression()) {
75+
if (nearestOptional.node.optional) break;
76+
nearestOptional = nearestOptional.get("object");
77+
continue;
78+
}
79+
80+
throw nearestOptional.buildCodeFrameError(
81+
"failed to find nearest optional",
82+
);
83+
}
84+
85+
const { scope } = member;
86+
const { object } = node;
87+
const baseRef = scope.generateUidIdentifierBasedOnNode(
88+
nearestOptional.node.object,
89+
);
90+
const valueRef = scope.generateUidIdentifierBasedOnNode(object);
91+
scope.push({ id: baseRef });
92+
scope.push({ id: valueRef });
93+
94+
nearestOptional
95+
.get("object")
96+
.replaceWith(
97+
t.assignmentExpression("=", baseRef, nearestOptional.node.object),
98+
);
99+
member.replaceWith(t.memberExpression(valueRef, node.property));
100+
101+
if (parentPath.isOptionalCallExpression({ callee: node })) {
102+
parentPath.replaceWith(this.call(member, parent.arguments));
103+
} else {
104+
member.replaceWith(this.get(member));
105+
}
106+
107+
let regular = member.node;
108+
for (let current = member; current !== root; ) {
109+
const { parentPath, parent } = current;
110+
if (parentPath.isOptionalMemberExpression()) {
111+
regular = t.memberExpression(
112+
regular,
113+
parent.property,
114+
parent.computed,
115+
);
116+
} else {
117+
regular = t.callExpression(regular, parent.arguments);
118+
}
119+
current = parentPath;
120+
}
121+
122+
root.replaceWith(
123+
t.conditionalExpression(
124+
t.sequenceExpression([
125+
t.assignmentExpression("=", valueRef, object),
126+
t.logicalExpression(
127+
"||",
128+
t.binaryExpression("===", baseRef, scope.buildUndefinedNode()),
129+
t.binaryExpression("===", baseRef, t.nullLiteral()),
130+
),
131+
]),
132+
scope.buildUndefinedNode(),
133+
regular,
134+
),
135+
);
136+
return;
137+
}
138+
40139
// MEMBER++ -> _set(MEMBER, (_ref = (+_get(MEMBER))) + 1), _ref
41140
// ++MEMBER -> _set(MEMBER, (+_get(MEMBER)) + 1)
42141
if (parentPath.isUpdateExpression({ argument: node })) {
142+
if (this.simpleSet) {
143+
member.replaceWith(this.simpleSet(member));
144+
return;
145+
}
146+
43147
const { operator, prefix } = parent;
44148

45149
// Give the state handler a chance to memoise the member, since we'll
@@ -72,6 +176,11 @@ const handle = {
72176
// MEMBER = VALUE -> _set(MEMBER, VALUE)
73177
// MEMBER += VALUE -> _set(MEMBER, _get(MEMBER) + VALUE)
74178
if (parentPath.isAssignmentExpression({ left: node })) {
179+
if (this.simpleSet) {
180+
member.replaceWith(this.simpleSet(member));
181+
return;
182+
}
183+
75184
const { operator, right } = parent;
76185
let value = right;
77186

@@ -92,11 +201,9 @@ const handle = {
92201
return;
93202
}
94203

95-
// MEMBER(ARGS) -> _call(MEMBER, ARGS)
204+
// MEMBER(ARGS) -> _call(MEMBER, ARGS)
96205
if (parentPath.isCallExpression({ callee: node })) {
97-
const { arguments: args } = parent;
98-
99-
parentPath.replaceWith(this.call(member, args));
206+
parentPath.replaceWith(this.call(member, parent.arguments));
100207
return;
101208
}
102209

packages/babel-parser/src/parser/expression.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -610,15 +610,16 @@ export default class ExpressionParser extends LValParser {
610610
node.object = base;
611611
node.property = computed
612612
? this.parseExpression()
613-
: optional
614-
? this.parseIdentifier(true)
615613
: this.parseMaybePrivateName(true);
616614
node.computed = computed;
617615

618616
if (node.property.type === "PrivateName") {
619617
if (node.object.type === "Super") {
620618
this.raise(startPos, Errors.SuperPrivateField);
621619
}
620+
if (optional) {
621+
this.raise(node.property.start, Errors.OptionalChainingNoPrivate);
622+
}
622623
this.classScope.usePrivateName(
623624
node.property.id.name,
624625
node.property.start,

packages/babel-parser/src/parser/location.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ export const Errors = Object.freeze({
109109
"await* has been removed from the async functions proposal. Use Promise.all() instead.",
110110
OptionalChainingNoNew:
111111
"constructors in/after an Optional Chain are not allowed",
112+
OptionalChainingNoPrivate:
113+
"Private property access cannot immediately follow an optional chain's `?.`",
112114
OptionalChainingNoTemplate:
113115
"Tagged Template Literals are not allowed in optionalChain",
114116
ParamDupe: "Argument name clash",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class Foo {
2+
static #x = 1;
3+
static #m = function() {};
4+
5+
static test() {
6+
const o = { Foo: Foo };
7+
return [
8+
o?.Foo.#x,
9+
o?.Foo.#x.toFixed,
10+
o?.Foo.#x.toFixed(2),
11+
o?.Foo.#m(),
12+
];
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"plugins": ["classPrivateProperties"]
3+
}

0 commit comments

Comments
 (0)