Skip to content

Commit aa18570

Browse files
jridgewellJLHwung
authored andcommitted
Handle private access chained on an optional chain
See the resolution from tc39/proposal-class-fields#301.
1 parent 4379f5c commit aa18570

File tree

33 files changed

+2827
-32
lines changed

33 files changed

+2827
-32
lines changed

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

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,12 @@ const privateNameVisitor = privateNameVisitorFactory({
128128
const { privateNamesMap, redeclared } = this;
129129
const { node, parentPath } = path;
130130

131-
if (!parentPath.isMemberExpression({ property: node })) return;
132-
131+
if (
132+
!parentPath.isMemberExpression({ property: node }) &&
133+
!parentPath.isOptionalMemberExpression({ property: node })
134+
) {
135+
return;
136+
}
133137
const { name } = node.id;
134138
if (!privateNamesMap.has(name)) return;
135139
if (redeclared && redeclared.includes(name)) return;
@@ -301,18 +305,28 @@ const privateNameHandlerSpec = {
301305
};
302306

303307
const privateNameHandlerLoose = {
304-
handle(member) {
308+
get(member) {
305309
const { privateNamesMap, file } = this;
306310
const { object } = member.node;
307311
const { name } = member.node.property.id;
308312

309-
member.replaceWith(
310-
template.expression`BASE(REF, PROP)[PROP]`({
311-
BASE: file.addHelper("classPrivateFieldLooseBase"),
312-
REF: object,
313-
PROP: privateNamesMap.get(name).id,
314-
}),
315-
);
313+
return template.expression`BASE(REF, PROP)[PROP]`({
314+
BASE: file.addHelper("classPrivateFieldLooseBase"),
315+
REF: object,
316+
PROP: privateNamesMap.get(name).id,
317+
});
318+
},
319+
320+
simpleSet(member) {
321+
return this.get(member);
322+
},
323+
324+
destructureSet(member) {
325+
return this.get(member);
326+
},
327+
328+
call(member, args) {
329+
return t.callExpression(this.get(member), args);
316330
},
317331
};
318332

@@ -326,26 +340,13 @@ export function transformPrivateNamesUsage(
326340
if (!privateNamesMap.size) return;
327341

328342
const body = path.get("body");
343+
const handler = loose ? privateNameHandlerLoose : privateNameHandlerSpec;
329344

330-
if (loose) {
331-
body.traverse(privateNameVisitor, {
332-
privateNamesMap,
333-
file: state,
334-
...privateNameHandlerLoose,
335-
});
336-
} else {
337-
memberExpressionToFunctions(body, privateNameVisitor, {
338-
privateNamesMap,
339-
classRef: ref,
340-
file: state,
341-
...privateNameHandlerSpec,
342-
});
343-
}
344-
body.traverse(privateInVisitor, {
345+
memberExpressionToFunctions(body, privateNameVisitor, {
345346
privateNamesMap,
346347
classRef: ref,
347348
file: state,
348-
loose,
349+
...handler,
349350
});
350351
}
351352

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
@@ -616,15 +616,16 @@ export default class ExpressionParser extends LValParser {
616616
node.object = base;
617617
node.property = computed
618618
? this.parseExpression()
619-
: optional
620-
? this.parseIdentifier(true)
621619
: this.parseMaybePrivateName(true);
622620
node.computed = computed;
623621

624622
if (node.property.type === "PrivateName") {
625623
if (node.object.type === "Super") {
626624
this.raise(startPos, Errors.SuperPrivateField);
627625
}
626+
if (optional) {
627+
this.raise(node.property.start, Errors.OptionalChainingNoPrivate);
628+
}
628629
this.classScope.usePrivateName(
629630
node.property.id.name,
630631
node.property.start,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ export const Errors = Object.freeze({
108108
"await* has been removed from the async functions proposal. Use Promise.all() instead.",
109109
OptionalChainingNoNew:
110110
"constructors in/after an Optional Chain are not allowed",
111+
OptionalChainingNoPrivate:
112+
"Private property access cannot immediately follow an optional chain's `?.`",
111113
OptionalChainingNoTemplate:
112114
"Tagged Template Literals are not allowed in optionalChain",
113115
ParamDupe: "Argument name clash",
Lines changed: 14 additions & 0 deletions
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+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"plugins": ["classPrivateProperties"]
3+
}

0 commit comments

Comments
 (0)