Skip to content

Commit bda759a

Browse files
jridgewellnicolo-ribaudoJLHwung
committed
Handle private access chained on an optional chain (babel#11248)
Co-authored-by: Nicolò Ribaudo <[email protected]> Co-authored-by: Huáng Jùnliàng <[email protected]>
1 parent 852520e commit bda759a

104 files changed

Lines changed: 6964 additions & 37 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.js

Lines changed: 42 additions & 25 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;
@@ -296,23 +300,43 @@ const privateNameHandlerSpec = {
296300
// The first access (the get) should do the memo assignment.
297301
this.memoise(member, 1);
298302

299-
return optimiseCall(this.get(member), this.receiver(member), args);
303+
return optimiseCall(this.get(member), this.receiver(member), args, false);
304+
},
305+
306+
optionalCall(member, args) {
307+
this.memoise(member, 1);
308+
309+
return optimiseCall(this.get(member), this.receiver(member), args, true);
300310
},
301311
};
302312

303313
const privateNameHandlerLoose = {
304-
handle(member) {
314+
get(member) {
305315
const { privateNamesMap, file } = this;
306316
const { object } = member.node;
307317
const { name } = member.node.property.id;
308318

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-
);
319+
return template.expression`BASE(REF, PROP)[PROP]`({
320+
BASE: file.addHelper("classPrivateFieldLooseBase"),
321+
REF: object,
322+
PROP: privateNamesMap.get(name).id,
323+
});
324+
},
325+
326+
simpleSet(member) {
327+
return this.get(member);
328+
},
329+
330+
destructureSet(member) {
331+
return this.get(member);
332+
},
333+
334+
call(member, args) {
335+
return t.callExpression(this.get(member), args);
336+
},
337+
338+
optionalCall(member, args) {
339+
return t.optionalCallExpression(this.get(member), args, true);
316340
},
317341
};
318342

@@ -326,21 +350,14 @@ export function transformPrivateNamesUsage(
326350
if (!privateNamesMap.size) return;
327351

328352
const body = path.get("body");
353+
const handler = loose ? privateNameHandlerLoose : privateNameHandlerSpec;
329354

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-
}
355+
memberExpressionToFunctions(body, privateNameVisitor, {
356+
privateNamesMap,
357+
classRef: ref,
358+
file: state,
359+
...handler,
360+
});
344361
body.traverse(privateInVisitor, {
345362
privateNamesMap,
346363
classRef: ref,

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

Lines changed: 227 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,55 @@ class AssignmentMemoiser {
2929
}
3030
}
3131

32+
function toNonOptional(path, base) {
33+
const { node } = path;
34+
if (path.isOptionalMemberExpression()) {
35+
return t.memberExpression(base, node.property, node.computed);
36+
}
37+
38+
if (path.isOptionalCallExpression()) {
39+
const callee = path.get("callee");
40+
if (path.node.optional && callee.isOptionalMemberExpression()) {
41+
const { object } = callee.node;
42+
const context = path.scope.maybeGenerateMemoised(object) || object;
43+
callee
44+
.get("object")
45+
.replaceWith(t.assignmentExpression("=", context, object));
46+
47+
return t.callExpression(t.memberExpression(base, t.identifier("call")), [
48+
context,
49+
...node.arguments,
50+
]);
51+
}
52+
53+
return t.callExpression(base, node.arguments);
54+
}
55+
56+
return path.node;
57+
}
58+
59+
// Determines if the current path is in a detached tree. This can happen when
60+
// we are iterating on a path, and replace an ancestor with a new node. Babel
61+
// doesn't always stop traversing the old node tree, and that can cause
62+
// inconsistencies.
63+
function isInDetachedTree(path) {
64+
while (path) {
65+
if (path.isProgram()) break;
66+
67+
const { parentPath, container, listKey } = path;
68+
const parentNode = parentPath.node;
69+
if (listKey) {
70+
if (container !== parentNode[listKey]) return true;
71+
} else {
72+
if (container !== parentNode) return true;
73+
}
74+
75+
path = parentPath;
76+
}
77+
78+
return false;
79+
}
80+
3281
const handle = {
3382
memoise() {
3483
// noop.
@@ -37,9 +86,175 @@ const handle = {
3786
handle(member) {
3887
const { node, parent, parentPath } = member;
3988

89+
if (member.isOptionalMemberExpression()) {
90+
// Transforming optional chaining requires we replace ancestors.
91+
if (isInDetachedTree(member)) return;
92+
93+
// We're looking for the end of _this_ optional chain, which is actually
94+
// the "rightmost" property access of the chain. This is because
95+
// everything up to that property access is "optional".
96+
//
97+
// Let's take the case of `FOO?.BAR.baz?.qux`, with `FOO?.BAR` being our
98+
// member. The "end" to most users would be `qux` property access.
99+
// Everything up to it could be skipped if it `FOO` were nullish. But
100+
// actually, we can consider the `baz` access to be the end. So we're
101+
// looking for the nearest optional chain that is `optional: true`.
102+
const endPath = member.find(({ node, parent, parentPath }) => {
103+
if (parentPath.isOptionalMemberExpression()) {
104+
// We need to check `parent.object` since we could be inside the
105+
// computed expression of a `bad?.[FOO?.BAR]`. In this case, the
106+
// endPath is the `FOO?.BAR` member itself.
107+
return parent.optional || parent.object !== node;
108+
}
109+
if (parentPath.isOptionalCallExpression()) {
110+
// Checking `parent.callee` since we could be in the arguments, eg
111+
// `bad?.(FOO?.BAR)`.
112+
// Also skip `FOO?.BAR` in `FOO?.BAR?.()` since we need to transform the optional call to ensure proper this
113+
return (
114+
// In FOO?.#BAR?.(), endPath points the optional call expression so we skip FOO?.#BAR
115+
(node !== member.node && parent.optional) || parent.callee !== node
116+
);
117+
}
118+
return true;
119+
});
120+
121+
const rootParentPath = endPath.parentPath;
122+
if (
123+
rootParentPath.isUpdateExpression({ argument: node }) ||
124+
rootParentPath.isAssignmentExpression({ left: node })
125+
) {
126+
throw member.buildCodeFrameError(`can't handle assignment`);
127+
}
128+
if (rootParentPath.isUnaryExpression({ operator: "delete" })) {
129+
throw member.buildCodeFrameError(`can't handle delete`);
130+
}
131+
132+
// Now, we're looking for the start of this optional chain, which is
133+
// optional to the left of this member.
134+
//
135+
// Let's take the case of `foo?.bar?.baz.QUX?.BAM`, with `QUX?.BAM` being
136+
// our member. The "start" to most users would be `foo` object access.
137+
// But actually, we can consider the `bar` access to be the start. So
138+
// we're looking for the nearest optional chain that is `optional: true`,
139+
// which is guaranteed to be somewhere in the object/callee tree.
140+
let startingOptional = member;
141+
for (;;) {
142+
if (startingOptional.isOptionalMemberExpression()) {
143+
if (startingOptional.node.optional) break;
144+
startingOptional = startingOptional.get("object");
145+
continue;
146+
} else if (startingOptional.isOptionalCallExpression()) {
147+
if (startingOptional.node.optional) break;
148+
startingOptional = startingOptional.get("callee");
149+
continue;
150+
}
151+
// prevent infinite loop: unreachable if the AST is well-formed
152+
throw new Error(
153+
`Internal error: unexpected ${startingOptional.node.type}`,
154+
);
155+
}
156+
157+
const { scope } = member;
158+
const startingProp = startingOptional.isOptionalMemberExpression()
159+
? "object"
160+
: "callee";
161+
const startingNode = startingOptional.node[startingProp];
162+
const baseNeedsMemoised = scope.maybeGenerateMemoised(startingNode);
163+
const baseRef = baseNeedsMemoised ?? startingNode;
164+
165+
// Compute parentIsOptionalCall before `startingOptional` is replaced
166+
// as `node` may refer to `startingOptional.node` before replaced.
167+
const parentIsOptionalCall = parentPath.isOptionalCallExpression({
168+
callee: node,
169+
});
170+
startingOptional.replaceWith(toNonOptional(startingOptional, baseRef));
171+
if (parentIsOptionalCall) {
172+
if (parent.optional) {
173+
parentPath.replaceWith(this.optionalCall(member, parent.arguments));
174+
} else {
175+
parentPath.replaceWith(this.call(member, parent.arguments));
176+
}
177+
} else {
178+
member.replaceWith(this.get(member));
179+
}
180+
181+
let regular = member.node;
182+
for (let current = member; current !== endPath; ) {
183+
const { parentPath } = current;
184+
// skip transforming `Foo.#BAR?.call(FOO)`
185+
if (parentPath === endPath && parentIsOptionalCall && parent.optional) {
186+
regular = parentPath.node;
187+
break;
188+
}
189+
regular = toNonOptional(parentPath, regular);
190+
current = parentPath;
191+
}
192+
193+
let context;
194+
const endParentPath = endPath.parentPath;
195+
if (
196+
t.isMemberExpression(regular) &&
197+
endParentPath.isOptionalCallExpression({
198+
callee: endPath.node,
199+
optional: true,
200+
})
201+
) {
202+
const { object } = regular;
203+
context = member.scope.maybeGenerateMemoised(object);
204+
if (context) {
205+
regular.object = t.assignmentExpression("=", context, object);
206+
}
207+
}
208+
209+
endPath.replaceWith(
210+
t.conditionalExpression(
211+
t.logicalExpression(
212+
"||",
213+
t.binaryExpression(
214+
"===",
215+
baseNeedsMemoised
216+
? t.assignmentExpression("=", baseRef, startingNode)
217+
: baseRef,
218+
t.nullLiteral(),
219+
),
220+
t.binaryExpression(
221+
"===",
222+
t.cloneNode(baseRef),
223+
scope.buildUndefinedNode(),
224+
),
225+
),
226+
scope.buildUndefinedNode(),
227+
regular,
228+
),
229+
);
230+
231+
if (context) {
232+
const endParent = endParentPath.node;
233+
endParentPath.replaceWith(
234+
t.optionalCallExpression(
235+
t.optionalMemberExpression(
236+
endParent.callee,
237+
t.identifier("call"),
238+
false,
239+
true,
240+
),
241+
[context, ...endParent.arguments],
242+
false,
243+
),
244+
);
245+
}
246+
247+
return;
248+
}
249+
40250
// MEMBER++ -> _set(MEMBER, (_ref = (+_get(MEMBER))) + 1), _ref
41251
// ++MEMBER -> _set(MEMBER, (+_get(MEMBER)) + 1)
42252
if (parentPath.isUpdateExpression({ argument: node })) {
253+
if (this.simpleSet) {
254+
member.replaceWith(this.simpleSet(member));
255+
return;
256+
}
257+
43258
const { operator, prefix } = parent;
44259

45260
// Give the state handler a chance to memoise the member, since we'll
@@ -72,6 +287,11 @@ const handle = {
72287
// MEMBER = VALUE -> _set(MEMBER, VALUE)
73288
// MEMBER += VALUE -> _set(MEMBER, _get(MEMBER) + VALUE)
74289
if (parentPath.isAssignmentExpression({ left: node })) {
290+
if (this.simpleSet) {
291+
member.replaceWith(this.simpleSet(member));
292+
return;
293+
}
294+
75295
const { operator, right } = parent;
76296
let value = right;
77297

@@ -92,11 +312,15 @@ const handle = {
92312
return;
93313
}
94314

95-
// MEMBER(ARGS) -> _call(MEMBER, ARGS)
315+
// MEMBER(ARGS) -> _call(MEMBER, ARGS)
96316
if (parentPath.isCallExpression({ callee: node })) {
97-
const { arguments: args } = parent;
317+
parentPath.replaceWith(this.call(member, parent.arguments));
318+
return;
319+
}
98320

99-
parentPath.replaceWith(this.call(member, args));
321+
// MEMBER?.(ARGS) -> _optionalCall(MEMBER, ARGS)
322+
if (parentPath.isOptionalCallExpression({ callee: node })) {
323+
parentPath.replaceWith(this.optionalCall(member, parent.arguments));
100324
return;
101325
}
102326

packages/babel-helper-optimise-call-expression/src/index.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as t from "@babel/types";
22

3-
export default function(callee, thisNode, args) {
3+
export default function(callee, thisNode, args, optional) {
44
if (
55
args.length === 1 &&
66
t.isSpreadElement(args[0]) &&
@@ -12,6 +12,13 @@ export default function(callee, thisNode, args) {
1212
args[0].argument,
1313
]);
1414
} else {
15+
if (optional) {
16+
return t.optionalCallExpression(
17+
t.optionalMemberExpression(callee, t.identifier("call"), false, true),
18+
[thisNode, ...args],
19+
false,
20+
);
21+
}
1522
return t.callExpression(t.memberExpression(callee, t.identifier("call")), [
1623
thisNode,
1724
...args,

packages/babel-helper-replace-supers/src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ const specHandlers = {
158158
this._get(superMember, thisRefs),
159159
t.cloneNode(thisRefs.this),
160160
args,
161+
false,
161162
);
162163
},
163164
};
@@ -215,7 +216,7 @@ const looseHandlers = {
215216
},
216217

217218
call(superMember, args) {
218-
return optimiseCall(this.get(superMember), t.thisExpression(), args);
219+
return optimiseCall(this.get(superMember), t.thisExpression(), args, false);
219220
},
220221
};
221222

0 commit comments

Comments
 (0)