From bb2104dd7cc2be20ffbeeb2b2a2ae802f19747b6 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Mon, 26 Sep 2022 22:54:17 +0800 Subject: [PATCH] Support optional chaining in `.at()` transform (#13528) --- scripts/build/shims/at.js | 9 ++++-- scripts/build/transform/index.js | 32 +++++++++++++++----- tests/unit/syntax-transform.js | 51 ++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 tests/unit/syntax-transform.js diff --git a/scripts/build/shims/at.js b/scripts/build/shims/at.js index f3f7ba2ff..263043086 100644 --- a/scripts/build/shims/at.js +++ b/scripts/build/shims/at.js @@ -5,9 +5,14 @@ Note: 2. `postcss` contains `Container` class with `.at` method */ -const at = (object, index, ...args) => - object.at +const at = (isOptionalObject, object, index, ...args) => { + if (isOptionalObject && (object === undefined || object === null)) { + return; + } + + return object.at ? object.at(index, ...args) : object[index < 0 ? object.length + index : index]; +}; export default at; diff --git a/scripts/build/transform/index.js b/scripts/build/transform/index.js index d5a3b4a80..704e1509a 100644 --- a/scripts/build/transform/index.js +++ b/scripts/build/transform/index.js @@ -8,9 +8,14 @@ import { SOURCE_DIR } from "../../utils/index.mjs"; const generate = babelGenerator.default; const atHelperPath = fileURLToPath(new URL("../shims/at.js", import.meta.url)); -/* Doesn't work for dependencies, optional chaining, and spread arguments */ +/* Doesn't work for dependencies, optional call, computed property, and spread arguments */ -// `Object.hasOwn(foo, "bar")` -> `Object.prototype.hasOwnProperty.call(foo, "bar")` +/** + * `Object.hasOwn(foo, "bar")` -> `Object.prototype.hasOwnProperty.call(foo, "bar")` + * + * @param {import("@babel/types").Node} node + * @returns {boolean} + */ function transformObjectHasOwnCall(node) { if ( !( @@ -47,16 +52,23 @@ function transformObjectHasOwnCall(node) { return true; } -// `foo.at(index)` -> `__at(foo, index)` +/** + * `foo.at(index)` -> `__at(false, foo, index)` + * `foo?.at(index)` -> `__at(true, foo, index)` + * + * @param {import("@babel/types").Node} node + * @returns {boolean} + */ function transformRelativeIndexingCall(node) { if ( !( - node.type === "CallExpression" && + (node.type === "CallExpression" || + node.type === "OptionalCallExpression") && !node.optional && node.arguments.length === 1 && node.arguments.every(({ type }) => type !== "SpreadElement") && - node.callee.type === "MemberExpression" && - !node.callee.optional && + (node.callee.type === "MemberExpression" || + node.callee.type === "OptionalMemberExpression") && !node.callee.computed && node.callee.object.type !== "ThisExpression" && node.callee.property.type === "Identifier" && @@ -66,7 +78,13 @@ function transformRelativeIndexingCall(node) { return false; } - node.arguments.unshift(node.callee.object); + node.arguments.unshift( + { + type: "BooleanLiteral", + value: node.callee.type === "OptionalMemberExpression", + }, + node.callee.object + ); node.callee = { type: "Identifier", name: "__at" }; return true; diff --git a/tests/unit/syntax-transform.js b/tests/unit/syntax-transform.js new file mode 100644 index 000000000..aadfea43b --- /dev/null +++ b/tests/unit/syntax-transform.js @@ -0,0 +1,51 @@ +/* eslint-disable quotes -- `toMatchInlineSnapshot` uses template literal */ +import url from "node:url"; +import path from "node:path"; +import transformCode from "../../scripts/build/transform/index.js"; + +const file = url.fileURLToPath( + new URL("../../src/__dummy.js", import.meta.url) +); +const shimsDirectory = url.fileURLToPath( + new URL("../../scripts/build/shims", import.meta.url) +); +const transform = (code) => + transformCode(code, file).replaceAll( + JSON.stringify(shimsDirectory + path.sep).slice(1, -1), + "/" + ); + +test("Object.hasOwn", () => { + expect(transform("Object.hasOwn(foo, bar)")).toMatchInlineSnapshot( + `"Object.prototype.hasOwnProperty.call(foo, bar);"` + ); +}); + +test(".at", () => { + expect(transform("foo.at(-1)")).toMatchInlineSnapshot(` + "import __at from "/at.js"; + + __at(false, foo, -1);" + `); + + expect(transform("foo?.at(-1)")).toMatchInlineSnapshot(` + "import __at from "/at.js"; + + __at(true, foo, -1);" + `); + + expect(transform("foo?.bar.baz.at(-1)")).toMatchInlineSnapshot(` + "import __at from "/at.js"; + + __at(true, foo?.bar.baz, -1);" + `); + + expect(transform("foo.at(-1)?.bar")).toMatchInlineSnapshot(` + "import __at from "/at.js"; + + __at(false, foo, -1)?.bar;" + `); + + // Don't support optional call + expect(transform("foo.at?.(-1)")).toMatchInlineSnapshot(`"foo.at?.(-1)"`); +});