diff --git a/packages/babel-plugin-transform-optional-chaining/src/index.js b/packages/babel-plugin-transform-optional-chaining/src/index.js index 49ff4c81e7..9c4b7805a5 100644 --- a/packages/babel-plugin-transform-optional-chaining/src/index.js +++ b/packages/babel-plugin-transform-optional-chaining/src/index.js @@ -1,6 +1,78 @@ import syntaxOptionalChaining from "babel-plugin-syntax-optional-chaining"; export default function ({ types: t }) { + function optional(path, key, replacementPath, needsContext = false, loose = false) { + const { scope } = path; + const optionals = [path.node]; + + let objectPath = path.get(key); + while (objectPath.isMemberExpression()) { + const { node } = objectPath; + if (node.optional) { + optionals.push(node); + } + + objectPath = objectPath.get("object"); + } + + for (let i = optionals.length - 1; i >= 0; i--) { + const node = optionals[i]; + node.optional = false; + + const replaceKey = i == 0 ? key : "object"; + const atCall = needsContext && i == 0; + + const chain = node[replaceKey]; + + let ref; + let check; + if (loose && atCall) { + // If we are using a loose transform (avoiding a Function#call) and we are at the call, + // we can avoid a needless memoize. + ref = chain; + check = ref; + } else { + ref = scope.maybeGenerateMemoised(chain); + if (ref) { + check = t.assignmentExpression("=", ref, chain); + node[replaceKey] = ref; + } else { + ref = chain; + check = chain; + } + } + + // Ensure call expressions have the proper `this` + // `foo.bar()` has context `foo`. + if (atCall && t.isMemberExpression(chain)) { + if (loose) { + // To avoid a Function#call, we can instead re-grab the property from the context object. + // `a.?b.?()` translates roughly to `_a.b != null && _a.b()` + node.callee = chain + } else { + // Otherwise, we need to memoize the context object, and change the call into a Function#call. + // `a.?b.?()` translates roughly to `(_b = _a.b) != null && _b.call(_a)` + const { object } = chain; + const context = scope.generateUidIdentifierBasedOnNode(object); + + scope.push({ id: context }); + chain.object = t.assignmentExpression("=", context, object); + + node.arguments.unshift(context); + node.callee = t.memberExpression(node.callee, t.identifier("call")); + } + } + + replacementPath.replaceWith(t.conditionalExpression( + t.binaryExpression("==", check, t.nullLiteral()), + scope.buildUndefinedNode(), + replacementPath.node + )); + + replacementPath = replacementPath.get("alternate"); + } + } + return { inherits: syntaxOptionalChaining, @@ -10,28 +82,25 @@ export default function ({ types: t }) { return; } - const { scope, node } = path; - const { object } = node; + const replace = path.find(path => { + const { parentPath } = path; + if (parentPath.isStatement()) { + return true; + } - node.optional = false; + if (path.key == "left" && parentPath.isAssignmentExpression()) { + return false; + } + if (path.key == "object" && parentPath.isMemberExpression()) { + return false; + } + if (path.key == "callee" && (parentPath.isCallExpression() || parentPath.isNewExpression())) { + return false; + } + return true; + }); - const ref = scope.generateUidIdentifierBasedOnNode(object); - scope.push({ id: ref }); - node.object = ref; - - let parent = path; - let expression = path; - while (parent.listKey === undefined) { - expression = parent; - parent = parent.parentPath; - } - - const replace = parent.isExpression() ? parent : expression; - replace.replaceWith(t.conditionalExpression( - t.binaryExpression("==", t.assignmentExpression("=", ref, object), t.nullLiteral()), - scope.buildUndefinedNode(), - replace.node - )); + optional(path, "object", replace); }, "NewExpression|CallExpression"(path) { @@ -39,29 +108,7 @@ export default function ({ types: t }) { return; } - const { scope, node } = path; - const { callee } = node; - - node.optional = false; - - const ref = scope.generateUidIdentifierBasedOnNode(callee); - scope.push({ id: ref }); - node.callee = ref; - - if (t.isMemberExpression(callee) && !t.isNewExpression(node)) { - const context = scope.generateUidIdentifierBasedOnNode(callee.object); - scope.push({ id: context }); - callee.object = t.assignmentExpression("=", context, callee.object); - - node.arguments.unshift(context); - node.callee = t.memberExpression(node.callee, t.identifier("call")); - } - - path.replaceWith(t.conditionalExpression( - t.binaryExpression("==", t.assignmentExpression("=", ref, callee), t.nullLiteral()), - scope.buildUndefinedNode(), - node - )); + optional(path, "callee", path, path.isCallExpression(), this.opts.loose); }, }, }; diff --git a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/assignement/actual.js b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/assignement/actual.js index b9405035c9..b7d9cee953 100644 --- a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/assignement/actual.js +++ b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/assignement/actual.js @@ -1,3 +1,7 @@ a?.b = 42 a?.b?.c?.d = 42 + +a?.b?.c?.d++ + +a?.b?.c?.d += 1 diff --git a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/assignement/expected.js b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/assignement/expected.js index 86ba0f57ef..5670c0f0e1 100644 --- a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/assignement/expected.js +++ b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/assignement/expected.js @@ -1,5 +1,9 @@ -var _a, _a$b$c, _a$b, _a2; +var _a, _a2, _a2$b, _a2$b$c, _a3, _a3$b, _a3$b$c, _a4, _a4$b, _a4$b$c; (_a = a) == null ? void 0 : _a.b = 42; -(_a2 = a) == null ? void 0 : (_a$b = _a2.b) == null ? void 0 : (_a$b$c = _a$b.c) == null ? void 0 : _a$b$c.d = 42; \ No newline at end of file +(_a2 = a) == null ? void 0 : (_a2$b = _a2.b) == null ? void 0 : (_a2$b$c = _a2$b.c) == null ? void 0 : _a2$b$c.d = 42; + +(_a3 = a) == null ? void 0 : (_a3$b = _a3.b) == null ? void 0 : (_a3$b$c = _a3$b.c) == null ? void 0 : _a3$b$c.d++; + +(_a4 = a) == null ? void 0 : (_a4$b = _a4.b) == null ? void 0 : (_a4$b$c = _a4$b.c) == null ? void 0 : _a4$b$c.d += 1; \ No newline at end of file diff --git a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/containers/expected.js b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/containers/expected.js index 811e1603a0..b0dc489b21 100644 --- a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/containers/expected.js +++ b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/containers/expected.js @@ -1,8 +1,8 @@ var _user$address, _user$address2, _a, _a2; var street = (_user$address = user.address) == null ? void 0 : _user$address.street; -(_user$address2 = user.address) == null ? void 0 : street = _user$address2.street; +street = (_user$address2 = user.address) == null ? void 0 : _user$address2.street; test((_a = a) == null ? void 0 : _a.b, 1); -1, (_a2 = a) == null ? void 0 : _a2.b, 2; +1, (_a2 = a) == null ? void 0 : _a2.b, 2; \ No newline at end of file diff --git a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/delete/expected.js b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/delete/expected.js index 49afe9cd6e..d1c9cb8617 100644 --- a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/delete/expected.js +++ b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/delete/expected.js @@ -1,5 +1,5 @@ -var _a, _a$b$c, _a$b, _a2; +var _a, _a2, _a2$b, _a2$b$c; -(_a = a) == null ? void 0 : delete _a.b; +delete ((_a = a) == null ? void 0 : _a.b); -(_a2 = a) == null ? void 0 : (_a$b = _a2.b) == null ? void 0 : (_a$b$c = _a$b.c) == null ? void 0 : delete _a$b$c.d; \ No newline at end of file +delete ((_a2 = a) == null ? void 0 : (_a2$b = _a2.b) == null ? void 0 : (_a2$b$c = _a2$b.c) == null ? void 0 : _a2$b$c.d); \ No newline at end of file diff --git a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/function-call-loose/actual.js b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/function-call-loose/actual.js new file mode 100644 index 0000000000..9499c5f20e --- /dev/null +++ b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/function-call-loose/actual.js @@ -0,0 +1,7 @@ +foo?.(foo); + +foo?.bar() + +foo.bar?.(foo.bar, false) + +foo?.bar?.(foo.bar, true) diff --git a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/function-call-loose/expected.js b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/function-call-loose/expected.js new file mode 100644 index 0000000000..7c40785d92 --- /dev/null +++ b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/function-call-loose/expected.js @@ -0,0 +1,9 @@ +var _foo, _foo2; + +foo == null ? void 0 : foo(foo); + +(_foo = foo) == null ? void 0 : _foo.bar(); + +foo.bar == null ? void 0 : foo.bar(foo.bar, false); + +(_foo2 = foo) == null ? void 0 : _foo2.bar == null ? void 0 : _foo2.bar(foo.bar, true); diff --git a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/function-call-loose/options.json b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/function-call-loose/options.json new file mode 100644 index 0000000000..acc83616f9 --- /dev/null +++ b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/function-call-loose/options.json @@ -0,0 +1,3 @@ +{ + "plugins": [["transform-optional-chaining", {"loose": true}]] +} diff --git a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/function-call/expected.js b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/function-call/expected.js index 0b6f985356..1df03edc8c 100644 --- a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/function-call/expected.js +++ b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/function-call/expected.js @@ -1,4 +1,4 @@ -var _foo, _foo2, _foo$bar, _foo3, _foo$bar2, _foo4, _foo5; +var _foo, _foo2, _foo$bar, _foo3, _foo4, _foo4$bar, _foo5; (_foo = foo) == null ? void 0 : _foo(foo); @@ -6,4 +6,4 @@ var _foo, _foo2, _foo$bar, _foo3, _foo$bar2, _foo4, _foo5; (_foo$bar = (_foo3 = foo).bar) == null ? void 0 : _foo$bar.call(_foo3, foo.bar, false); -(_foo5 = _foo4 = foo) == null ? void 0 : (_foo$bar2 = _foo5.bar) == null ? void 0 : _foo$bar2.call(_foo4, foo.bar, true); \ No newline at end of file +(_foo4 = foo) == null ? void 0 : (_foo4$bar = (_foo5 = _foo4).bar) == null ? void 0 : _foo4$bar.call(_foo5, foo.bar, true); \ No newline at end of file diff --git a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/member-access/actual.js b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/member-access/actual.js index 83447e76d5..d6a9752544 100644 --- a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/member-access/actual.js +++ b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/member-access/actual.js @@ -1,3 +1,11 @@ foo?.bar a?.b.c?.d.e + +orders?.[0].price + +orders?.[0]?.price + +orders[client?.key].price + +orders[client.key]?.price diff --git a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/member-access/expected.js b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/member-access/expected.js index a8a381af7a..ffc9f8bc4e 100644 --- a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/member-access/expected.js +++ b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/member-access/expected.js @@ -1,5 +1,13 @@ -var _foo, _a$b$c, _a; +var _foo, _a, _a$b$c, _orders, _orders2, _orders2$, _client, _orders$client$key; (_foo = foo) == null ? void 0 : _foo.bar; -(_a = a) == null ? void 0 : (_a$b$c = _a.b.c) == null ? void 0 : _a$b$c.d.e; \ No newline at end of file +(_a = a) == null ? void 0 : (_a$b$c = _a.b.c) == null ? void 0 : _a$b$c.d.e; + +(_orders = orders) == null ? void 0 : _orders[0].price; + +(_orders2 = orders) == null ? void 0 : (_orders2$ = _orders2[0]) == null ? void 0 : _orders2$.price; + +orders[(_client = client) == null ? void 0 : _client.key].price; + +(_orders$client$key = orders[client.key]) == null ? void 0 : _orders$client$key.price; \ No newline at end of file diff --git a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/memoize-loose/actual.js b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/memoize-loose/actual.js new file mode 100644 index 0000000000..ffe543adde --- /dev/null +++ b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/memoize-loose/actual.js @@ -0,0 +1,21 @@ +function test(foo) { + foo?.bar; + + foo?.bar?.baz; + + foo?.(foo); + + foo?.bar() + + foo.bar?.(foo.bar, false) + + foo?.bar?.(foo.bar, true) + + foo.bar?.baz(foo.bar, false) + + foo?.bar?.baz(foo.bar, true) + + foo.bar?.baz?.(foo.bar, false) + + foo?.bar?.baz?.(foo.bar, true) +} diff --git a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/memoize-loose/expected.js b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/memoize-loose/expected.js new file mode 100644 index 0000000000..66f4809d09 --- /dev/null +++ b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/memoize-loose/expected.js @@ -0,0 +1,23 @@ +function test(foo) { + var _foo$bar, _foo$bar2, _foo$bar3, _foo$bar4, _foo$bar5; + + foo == null ? void 0 : foo.bar; + + foo == null ? void 0 : (_foo$bar = foo.bar) == null ? void 0 : _foo$bar.baz; + + foo == null ? void 0 : foo(foo); + + foo == null ? void 0 : foo.bar(); + + foo.bar == null ? void 0 : foo.bar(foo.bar, false); + + foo == null ? void 0 : foo.bar == null ? void 0 : foo.bar(foo.bar, true); + + (_foo$bar2 = foo.bar) == null ? void 0 : _foo$bar2.baz(foo.bar, false); + + foo == null ? void 0 : (_foo$bar3 = foo.bar) == null ? void 0 : _foo$bar3.baz(foo.bar, true); + + (_foo$bar4 = foo.bar) == null ? void 0 : _foo$bar4.baz == null ? void 0 : _foo$bar4.baz(foo.bar, false); + + foo == null ? void 0 : (_foo$bar5 = foo.bar) == null ? void 0 : _foo$bar5.baz == null ? void 0 : _foo$bar5.baz(foo.bar, true); +} \ No newline at end of file diff --git a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/memoize-loose/options.json b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/memoize-loose/options.json new file mode 100644 index 0000000000..acc83616f9 --- /dev/null +++ b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/memoize-loose/options.json @@ -0,0 +1,3 @@ +{ + "plugins": [["transform-optional-chaining", {"loose": true}]] +} diff --git a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/memoize/actual.js b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/memoize/actual.js new file mode 100644 index 0000000000..ffe543adde --- /dev/null +++ b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/memoize/actual.js @@ -0,0 +1,21 @@ +function test(foo) { + foo?.bar; + + foo?.bar?.baz; + + foo?.(foo); + + foo?.bar() + + foo.bar?.(foo.bar, false) + + foo?.bar?.(foo.bar, true) + + foo.bar?.baz(foo.bar, false) + + foo?.bar?.baz(foo.bar, true) + + foo.bar?.baz?.(foo.bar, false) + + foo?.bar?.baz?.(foo.bar, true) +} diff --git a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/memoize/expected.js b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/memoize/expected.js new file mode 100644 index 0000000000..d02f52f934 --- /dev/null +++ b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/memoize/expected.js @@ -0,0 +1,23 @@ +function test(foo) { + var _foo$bar, _foo$bar2, _foo, _foo$bar3, _foo2, _foo$bar4, _foo$bar5, _foo$bar6, _foo$bar6$baz, _foo$bar7, _foo$bar8, _foo$bar8$baz, _foo$bar9; + + foo == null ? void 0 : foo.bar; + + foo == null ? void 0 : (_foo$bar = foo.bar) == null ? void 0 : _foo$bar.baz; + + foo == null ? void 0 : foo(foo); + + foo == null ? void 0 : foo.bar(); + + (_foo$bar2 = (_foo = foo).bar) == null ? void 0 : _foo$bar2.call(_foo, foo.bar, false); + + foo == null ? void 0 : (_foo$bar3 = (_foo2 = foo).bar) == null ? void 0 : _foo$bar3.call(_foo2, foo.bar, true); + + (_foo$bar4 = foo.bar) == null ? void 0 : _foo$bar4.baz(foo.bar, false); + + foo == null ? void 0 : (_foo$bar5 = foo.bar) == null ? void 0 : _foo$bar5.baz(foo.bar, true); + + (_foo$bar6 = foo.bar) == null ? void 0 : (_foo$bar6$baz = (_foo$bar7 = _foo$bar6).baz) == null ? void 0 : _foo$bar6$baz.call(_foo$bar7, foo.bar, false); + + foo == null ? void 0 : (_foo$bar8 = foo.bar) == null ? void 0 : (_foo$bar8$baz = (_foo$bar9 = _foo$bar8).baz) == null ? void 0 : _foo$bar8$baz.call(_foo$bar9, foo.bar, true); +} \ No newline at end of file diff --git a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/new/expected.js b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/new/expected.js index cf631ecfd0..241badcfdd 100644 --- a/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/new/expected.js +++ b/packages/babel-plugin-transform-optional-chaining/test/fixtures/general/new/expected.js @@ -1,10 +1,10 @@ -var _a, _a$b$c, _a$b, _a2, _a3, _a$b$c2, _a$b2, _a4, _b, _a$b3, _a5; +var _a, _a2, _a2$b, _a2$b$c, _a3, _a4, _a4$b, _a4$b$c, _b, _a5, _a5$b; (_a = a) == null ? void 0 : new _a.b(); -(_a2 = a) == null ? void 0 : (_a$b = _a2.b) == null ? void 0 : (_a$b$c = _a$b.c) == null ? void 0 : new _a$b$c.d(); +(_a2 = a) == null ? void 0 : (_a2$b = _a2.b) == null ? void 0 : (_a2$b$c = _a2$b.c) == null ? void 0 : new _a2$b$c.d(); (_a3 = a) == null ? void 0 : new _a3.b(); -(_a4 = a) == null ? void 0 : (_a$b2 = _a4.b) == null ? void 0 : (_a$b$c2 = _a$b2.c) == null ? void 0 : new _a$b$c2.d(); +(_a4 = a) == null ? void 0 : (_a4$b = _a4.b) == null ? void 0 : (_a4$b$c = _a4$b.c) == null ? void 0 : new _a4$b$c.d(); (_b = b) == null ? void 0 : new _b(b); -(_a5 = a) == null ? void 0 : (_a$b3 = _a5.b) == null ? void 0 : new _a$b3(a.b, true); \ No newline at end of file +(_a5 = a) == null ? void 0 : (_a5$b = _a5.b) == null ? void 0 : new _a5$b(a.b, true); \ No newline at end of file