Skip to content

Commit bacb1df

Browse files
fix: Cannot polyfilled on optional chains (#221)
Co-authored-by: Nicolò Ribaudo <[email protected]>
1 parent 2fa4e97 commit bacb1df

File tree

10 files changed

+243
-22
lines changed

10 files changed

+243
-22
lines changed

packages/babel-helper-define-polyfill-provider/src/visitors/usage.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ export default (callProvider: CallProvider) => {
3030
callProvider({ kind: "global", name }, path);
3131
}
3232

33-
function analyzeMemberExpression(path: NodePath<t.MemberExpression>) {
33+
function analyzeMemberExpression(
34+
path: NodePath<t.MemberExpression | t.OptionalMemberExpression>,
35+
) {
3436
const key = resolveKey(path.get("property"), path.node.computed);
3537
return { key, handleAsMemberExpression: !!key && key !== "prototype" };
3638
}
@@ -48,7 +50,9 @@ export default (callProvider: CallProvider) => {
4850
handleReferencedIdentifier(path);
4951
},
5052

51-
MemberExpression(path: NodePath<t.MemberExpression>) {
53+
"MemberExpression|OptionalMemberExpression"(
54+
path: NodePath<t.MemberExpression | t.OptionalMemberExpression>,
55+
) {
5256
const { key, handleAsMemberExpression } = analyzeMemberExpression(path);
5357
if (!handleAsMemberExpression) return;
5458

packages/babel-helper-define-polyfill-provider/test/descriptors.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,17 @@ describe("descriptors", () => {
152152
expect(path.type).toBe("BinaryExpression");
153153
expect(path.toString()).toBe("'values' in Object");
154154
});
155+
156+
it("optional chains", () => {
157+
const [desc, path] = getDescriptor("a?.includes();", "property");
158+
159+
expect(desc).toEqual({
160+
kind: "property",
161+
object: "a",
162+
key: "includes",
163+
placement: "static",
164+
});
165+
expect(path.type).toBe("OptionalMemberExpression");
166+
expect(path.toString()).toBe("a?.includes");
167+
});
155168
});

packages/babel-plugin-polyfill-corejs3/src/index.ts

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ import * as BabelRuntimePaths from "./babel-runtime-corejs3-paths";
1515
import canSkipPolyfill from "./usage-filters";
1616

1717
import type { NodePath } from "@babel/traverse";
18-
import { types as t } from "@babel/core";
18+
import { types as t, template } from "@babel/core";
1919
import {
2020
callMethod,
2121
coreJSModule,
2222
isCoreJSSource,
2323
coreJSPureHelper,
2424
BABEL_RUNTIME,
25+
extractOptionalCheck,
26+
maybeMemoizeContext,
2527
} from "./utils";
2628

2729
import defineProvider from "@babel/helper-define-polyfill-provider";
@@ -111,9 +113,9 @@ export default defineProvider<Options>(function (
111113

112114
function maybeInjectPure(
113115
desc: CoreJSPolyfillDescriptor,
114-
hint,
115-
utils,
116-
object?,
116+
hint: string,
117+
utils: ReturnType<typeof getUtils>,
118+
object?: string,
117119
) {
118120
if (
119121
desc.pure &&
@@ -247,7 +249,9 @@ export default defineProvider<Options>(function (
247249

248250
if (meta.kind === "property") {
249251
// We can't compile destructuring and updateExpression.
250-
if (!path.isMemberExpression()) return;
252+
if (!path.isMemberExpression() && !path.isOptionalMemberExpression()) {
253+
return;
254+
}
251255
if (!path.isReferenced()) return;
252256
if (path.parentPath.isUpdateExpression()) return;
253257
if (t.isSuper(path.node.object)) {
@@ -326,7 +330,31 @@ export default defineProvider<Options>(function (
326330
// @ts-expect-error
327331
meta.object,
328332
);
329-
if (id) path.replaceWith(id);
333+
if (id) {
334+
path.replaceWith(id);
335+
let { parentPath } = path;
336+
if (
337+
parentPath.isOptionalMemberExpression() ||
338+
parentPath.isOptionalCallExpression()
339+
) {
340+
do {
341+
const parentAsNotOptional = parentPath as NodePath as NodePath<
342+
t.MemberExpression | t.CallExpression
343+
>;
344+
parentAsNotOptional.type = parentAsNotOptional.node.type =
345+
parentPath.type === "OptionalMemberExpression"
346+
? "MemberExpression"
347+
: "CallExpression";
348+
delete parentAsNotOptional.node.optional;
349+
350+
({ parentPath } = parentPath);
351+
} while (
352+
(parentPath.isOptionalMemberExpression() ||
353+
parentPath.isOptionalCallExpression()) &&
354+
!parentPath.node.optional
355+
);
356+
}
357+
}
330358
} else if (resolved.kind === "instance") {
331359
const id = maybeInjectPure(
332360
resolved.desc,
@@ -337,9 +365,40 @@ export default defineProvider<Options>(function (
337365
);
338366
if (!id) return;
339367

340-
const { node } = path as NodePath<t.MemberExpression>;
341-
if (t.isCallExpression(path.parent, { callee: node })) {
342-
callMethod(path, id);
368+
const { node, parent } = path as NodePath<
369+
t.MemberExpression | t.OptionalMemberExpression
370+
>;
371+
372+
if (t.isOptionalCallExpression(parent) && parent.callee === node) {
373+
const wasOptional = parent.optional;
374+
parent.optional = !wasOptional;
375+
376+
if (!wasOptional) {
377+
const check = extractOptionalCheck(
378+
path.scope,
379+
node as t.OptionalMemberExpression,
380+
);
381+
const [thisArg, thisArg2] = maybeMemoizeContext(node, path.scope);
382+
383+
path.replaceWith(
384+
check(
385+
template.expression.ast`
386+
Function.call.bind(${id}(${thisArg}), ${thisArg2})
387+
`,
388+
),
389+
);
390+
} else if (t.isOptionalMemberExpression(node)) {
391+
const check = extractOptionalCheck(path.scope, node);
392+
callMethod(path, id, true, check);
393+
} else {
394+
callMethod(path, id, true);
395+
}
396+
} else if (t.isCallExpression(parent) && parent.callee === node) {
397+
callMethod(path, id, false);
398+
} else if (t.isOptionalMemberExpression(node)) {
399+
const check = extractOptionalCheck(path.scope, node);
400+
path.replaceWith(check(t.callExpression(id, [node.object])));
401+
if (t.isOptionalMemberExpression(parent)) parent.optional = true;
343402
} else {
344403
path.replaceWith(t.callExpression(id, [node.object]));
345404
}

packages/babel-plugin-polyfill-corejs3/src/utils.ts

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,71 @@
1-
import { types as t } from "@babel/core";
1+
import { types as t, type NodePath } from "@babel/core";
22
import corejsEntries from "../core-js-compat/entries.js";
33

44
export const BABEL_RUNTIME = "@babel/runtime-corejs3";
55

6-
export function callMethod(path: any, id: t.Identifier) {
7-
const { object } = path.node;
6+
export function callMethod(
7+
path: any,
8+
id: t.Identifier,
9+
optionalCall?: boolean,
10+
wrapCallee?: (callee: t.Expression) => t.Expression,
11+
) {
12+
const [context1, context2] = maybeMemoizeContext(path.node, path.scope);
13+
14+
let callee: t.Expression = t.callExpression(id, [context1]);
15+
if (wrapCallee) callee = wrapCallee(callee);
16+
17+
const call = t.identifier("call");
18+
19+
path.replaceWith(
20+
optionalCall
21+
? t.optionalMemberExpression(callee, call, false, true)
22+
: t.memberExpression(callee, call),
23+
);
24+
25+
path.parentPath.unshiftContainer("arguments", context2);
26+
}
27+
28+
export function maybeMemoizeContext(
29+
node: t.MemberExpression | t.OptionalMemberExpression,
30+
scope: NodePath["scope"],
31+
) {
32+
const { object } = node;
833

934
let context1, context2;
1035
if (t.isIdentifier(object)) {
11-
context1 = object;
12-
context2 = t.cloneNode(object);
36+
context2 = object;
37+
context1 = t.cloneNode(object);
1338
} else {
14-
context1 = path.scope.generateDeclaredUidIdentifier("context");
15-
context2 = t.assignmentExpression("=", t.cloneNode(context1), object);
39+
context2 = scope.generateDeclaredUidIdentifier("context");
40+
context1 = t.assignmentExpression("=", t.cloneNode(context2), object);
1641
}
1742

18-
path.replaceWith(
19-
t.memberExpression(t.callExpression(id, [context2]), t.identifier("call")),
20-
);
43+
return [context1, context2];
44+
}
45+
46+
export function extractOptionalCheck(
47+
scope: NodePath["scope"],
48+
node: t.OptionalMemberExpression,
49+
) {
50+
let optionalNode = node;
51+
while (
52+
!optionalNode.optional &&
53+
t.isOptionalMemberExpression(optionalNode.object)
54+
) {
55+
optionalNode = optionalNode.object;
56+
}
57+
optionalNode.optional = false;
58+
59+
const ctx = scope.generateDeclaredUidIdentifier("context");
60+
const assign = t.assignmentExpression("=", ctx, optionalNode.object);
61+
optionalNode.object = t.cloneNode(ctx);
2162

22-
path.parentPath.unshiftContainer("arguments", context1);
63+
return ifNotNullish =>
64+
t.conditionalExpression(
65+
t.binaryExpression("==", assign, t.nullLiteral()),
66+
t.unaryExpression("void", t.numericLiteral(0)),
67+
ifNotNullish,
68+
);
2369
}
2470

2571
export function isCoreJSSource(source: string) {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Array?.from();
2+
c?.toSorted();
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"plugins": [
3+
[
4+
"@@/polyfill-corejs3",
5+
{
6+
"method": "usage-global"
7+
}
8+
]
9+
]
10+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import "core-js/modules/es.array.from.js";
2+
import "core-js/modules/es.array.sort.js";
3+
import "core-js/modules/es.string.iterator.js";
4+
Array?.from();
5+
c?.toSorted();
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
a.toSorted(x);
2+
a.toSorted?.(x);
3+
a.b.toSorted?.(x);
4+
a.b.toSorted?.(x).c;
5+
a.b.toSorted?.(x)?.c;
6+
7+
a?.toSorted(x);
8+
a?.b.toSorted(x).c;
9+
a.b?.toSorted(x).c;
10+
a?.b.toSorted(x)?.c;
11+
a.b?.toSorted(x)?.c;
12+
a?.b.toSorted?.(x).c;
13+
14+
a.b.c.toSorted?.d.e;
15+
a.b?.c.toSorted.d.e;
16+
a.b?.c.toSorted?.d.e;
17+
18+
Array.from(x);
19+
Array.from?.(x);
20+
Array.from?.(x).c;
21+
Array.from?.(x)?.c;
22+
23+
Array?.from(x);
24+
Array?.from(x).c;
25+
Array?.from(x)?.c;
26+
Array?.from?.(x);
27+
Array?.from?.(x).c;
28+
Array?.from?.(x)?.c;
29+
30+
Array?.from;
31+
Array?.from.x.y;
32+
Array.from?.x.y;
33+
Array?.from?.x.y;
34+
Array.from.x?.y;
35+
Array?.from.x().y?.();
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"targets": {
3+
"chrome": "30"
4+
},
5+
"plugins": [
6+
[
7+
"@@/polyfill-corejs3",
8+
{
9+
"method": "usage-pure",
10+
"version": "3.38"
11+
}
12+
]
13+
]
14+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
var _context, _context2, _context3, _context4, _context5, _context6, _context7, _context8, _context9, _context10, _context11, _context12, _context13, _context14;
2+
import _toSortedInstanceProperty from "core-js-pure/stable/instance/to-sorted.js";
3+
import _Array$from from "core-js-pure/stable/array/from.js";
4+
_toSortedInstanceProperty(a).call(a, x);
5+
_toSortedInstanceProperty(a)?.call(a, x);
6+
_toSortedInstanceProperty(_context = a.b)?.call(_context, x);
7+
_toSortedInstanceProperty(_context2 = a.b)?.call(_context2, x).c;
8+
_toSortedInstanceProperty(_context3 = a.b)?.call(_context3, x)?.c;
9+
((_context4 = a) == null ? void 0 : Function.call.bind(_toSortedInstanceProperty(_context4), _context4))?.(x);
10+
((_context5 = a) == null ? void 0 : Function.call.bind(_toSortedInstanceProperty(_context6 = _context5.b), _context6))?.(x).c;
11+
((_context7 = a.b) == null ? void 0 : Function.call.bind(_toSortedInstanceProperty(_context7), _context7))?.(x).c;
12+
((_context8 = a) == null ? void 0 : Function.call.bind(_toSortedInstanceProperty(_context9 = _context8.b), _context9))?.(x)?.c;
13+
((_context10 = a.b) == null ? void 0 : Function.call.bind(_toSortedInstanceProperty(_context10), _context10))?.(x)?.c;
14+
((_context11 = a) == null ? void 0 : _toSortedInstanceProperty(_context12 = _context11.b))?.call(_context12, x).c;
15+
_toSortedInstanceProperty(a.b.c)?.d.e;
16+
((_context13 = a.b) == null ? void 0 : _toSortedInstanceProperty(_context13.c))?.d.e;
17+
((_context14 = a.b) == null ? void 0 : _toSortedInstanceProperty(_context14.c))?.d.e;
18+
_Array$from(x);
19+
_Array$from(x);
20+
_Array$from(x).c;
21+
_Array$from(x)?.c;
22+
_Array$from(x);
23+
_Array$from(x).c;
24+
_Array$from(x)?.c;
25+
_Array$from(x);
26+
_Array$from(x).c;
27+
_Array$from(x)?.c;
28+
_Array$from;
29+
_Array$from.x.y;
30+
_Array$from.x.y;
31+
_Array$from.x.y;
32+
_Array$from.x?.y;
33+
_Array$from.x().y?.();

0 commit comments

Comments
 (0)