diff --git a/scripts/build/shims/object-group-by.js b/scripts/build/shims/function-object-group-by.js similarity index 100% rename from scripts/build/shims/object-group-by.js rename to scripts/build/shims/function-object-group-by.js diff --git a/scripts/build/shims/function-object-has-own.js b/scripts/build/shims/function-object-has-own.js new file mode 100644 index 000000000..af8b91387 --- /dev/null +++ b/scripts/build/shims/function-object-has-own.js @@ -0,0 +1,5 @@ +const hasOwn = + Object.hasOwn ?? + Function.prototype.call.bind(Object.prototype.hasOwnProperty); + +export default hasOwn; diff --git a/scripts/build/transform/transforms/create-function-call-transform.js b/scripts/build/transform/transforms/create-function-call-transform.js new file mode 100644 index 000000000..74d40c250 --- /dev/null +++ b/scripts/build/transform/transforms/create-function-call-transform.js @@ -0,0 +1,63 @@ +import { fileURLToPath } from "node:url"; +import { outdent } from "outdent"; +import { createIdentifier, isIdentifier } from "./utilities.js"; + +/* Only works for function `Object.hasOwn()` and `Object.groupBy()` */ + +/** + * @param {import("@babel/types").Node} node + * @returns {boolean} + */ +function isFunctionCall(node, { function: name, argumentsLength }) { + const [objectName, methodName] = name.split("."); + + return ( + node.type === "CallExpression" && + !node.optional && + node.arguments.length === argumentsLength && + node.arguments.every(({ type }) => type !== "SpreadElement") && + node.callee.type === "MemberExpression" && + !node.callee.optional && + !node.callee.computed && + isIdentifier(node.callee.object, objectName) && + isIdentifier(node.callee.property, methodName) + ); +} + +/** + * `Object.hasOwn(foo, bar)` -> `__Object_hasOwn(foo, bar)` + * + * @param {import("@babel/types").CallExpression | import("@babel/types").OptionalCallExpression} node + * @returns {import("@babel/types").CallExpression} + */ +function transformFunctionCall(node, functionName) { + return { + ...node, + callee: createIdentifier(functionName), + }; +} + +function createFunctionCallTransform({ function: name, argumentsLength }) { + const functionName = `__${name.replaceAll(".", "_")}`; + const fileName = `function-${name + .replace(/^[A-Z]/, (character) => character.toLowerCase()) + .replaceAll(/\W/g, "-") + .replaceAll(/[A-Z]/g, (character) => `-${character.toLowerCase()}`)}.js`; + const functionImplementationUrl = new URL( + `../../shims/${fileName}`, + import.meta.url, + ); + const functionImplementationPath = fileURLToPath(functionImplementationUrl); + + return { + shouldSkip: (text, file) => + !text.includes(`${name}(`) || file === functionImplementationPath, + test: (node) => isFunctionCall(node, { function: name, argumentsLength }), + transform: (node) => transformFunctionCall(node, functionName), + inject: outdent` + import ${functionName} from ${JSON.stringify(functionImplementationPath)}; + `, + }; +} + +export default createFunctionCallTransform; diff --git a/scripts/build/transform/transforms/create-method-call-transform.js b/scripts/build/transform/transforms/create-method-call-transform.js index 7d26a900d..310249060 100644 --- a/scripts/build/transform/transforms/create-method-call-transform.js +++ b/scripts/build/transform/transforms/create-method-call-transform.js @@ -3,7 +3,7 @@ import { outdent } from "outdent"; import { OPTIONAL_OBJECT } from "../../shims/shared.js"; import { createIdentifier, isIdentifier } from "./utilities.js"; -/* Doesn't work for optional call, computed property, and spread arguments */ +/* Doesn't work for computed property, and spread arguments */ /** * @param {import("@babel/types").Node} node diff --git a/scripts/build/transform/transforms/transform-object-group-by.js b/scripts/build/transform/transforms/transform-object-group-by.js index 9032badac..f8d243f2e 100644 --- a/scripts/build/transform/transforms/transform-object-group-by.js +++ b/scripts/build/transform/transforms/transform-object-group-by.js @@ -1,43 +1,8 @@ -import { fileURLToPath } from "node:url"; -import { outdent } from "outdent"; -import { createIdentifier, isIdentifier } from "./utilities.js"; +import createFunctionCallTransform from "./create-function-call-transform.js"; -/* -`Object.groupBy(items, callbackFn)` -> `__objectGroupBy(items, callbackFn)` -*/ +const transformObjectGroupBy = createFunctionCallTransform({ + function: "Object.groupBy", + argumentsLength: 2, +}); -const functionName = "__objectGroupBy"; -const functionImplementationPath = fileURLToPath( - new URL("../../shims/object-group-by.js", import.meta.url), -); - -const transformObjectHasOwnCall = { - shouldSkip: (text) => !text.includes("Object.groupBy("), - /** - * @param {import("@babel/types").Node} node - * @returns {boolean} - */ - test: (node) => - node.type === "CallExpression" && - !node.optional && - node.arguments.length === 2 && - node.arguments.every(({ type }) => type !== "SpreadElement") && - node.callee.type === "MemberExpression" && - !node.callee.optional && - !node.callee.computed && - isIdentifier(node.callee.object, "Object") && - isIdentifier(node.callee.property, "groupBy"), - /** - * @param {import("@babel/types").CallExpression} node - * @returns {import("@babel/types").CallExpression} - */ - transform: (node) => ({ - ...node, - callee: createIdentifier(functionName), - }), - inject: outdent` - import ${functionName} from ${JSON.stringify(functionImplementationPath)}; - `, -}; - -export default transformObjectHasOwnCall; +export default transformObjectGroupBy; diff --git a/scripts/build/transform/transforms/transform-object-has-own.js b/scripts/build/transform/transforms/transform-object-has-own.js index ef3618158..95396012d 100644 --- a/scripts/build/transform/transforms/transform-object-has-own.js +++ b/scripts/build/transform/transforms/transform-object-has-own.js @@ -1,33 +1,8 @@ -import { createMemberExpression, isIdentifier } from "./utilities.js"; +import createFunctionCallTransform from "./create-function-call-transform.js"; -/* -`Object.hasOwn(foo, "bar")` -> `Object.prototype.hasOwnProperty.call(foo, "bar")` -*/ +const transformObjectHasOwn = createFunctionCallTransform({ + function: "Object.hasOwn", + argumentsLength: 2, +}); -const transformObjectHasOwnCall = { - shouldSkip: (text) => !text.includes("Object.hasOwn("), - /** - * @param {import("@babel/types").Node} node - * @returns {boolean} - */ - test: (node) => - node.type === "CallExpression" && - !node.optional && - node.arguments.length === 2 && - node.arguments.every(({ type }) => type !== "SpreadElement") && - node.callee.type === "MemberExpression" && - !node.callee.optional && - !node.callee.computed && - isIdentifier(node.callee.object, "Object") && - isIdentifier(node.callee.property, "hasOwn"), - /** - * @param {import("@babel/types").CallExpression} node - * @returns {import("@babel/types").CallExpression} - */ - transform: (node) => ({ - ...node, - callee: createMemberExpression("Object.prototype.hasOwnProperty.call"), - }), -}; - -export default transformObjectHasOwnCall; +export default transformObjectHasOwn; diff --git a/tests/unit/syntax-transform.js b/tests/unit/syntax-transform.js index e6915711e..12116ab18 100644 --- a/tests/unit/syntax-transform.js +++ b/tests/unit/syntax-transform.js @@ -28,21 +28,23 @@ const transform = (code) => ) + "\n"; -test("Object.hasOwn", () => { +test("Object.hasOwn()", () => { expect(transform("Object.hasOwn(foo, bar)")).toMatchInlineSnapshot(` " - Object.prototype.hasOwnProperty.call(foo, bar); + import __Object_hasOwn from "/function-object-has-own.js"; + + __Object_hasOwn(foo, bar); " `); }); -test("Object.groupBy", () => { +test("Object.groupBy()", () => { expect(transform("const {} = Object.groupBy(foo, bar)")) .toMatchInlineSnapshot(` " - import __objectGroupBy from "/object-group-by.js"; + import __Object_groupBy from "/function-object-group-by.js"; - const {} = __objectGroupBy(foo, bar); + const {} = __Object_groupBy(foo, bar); " `); }); @@ -272,6 +274,7 @@ test("All", () => { `), ).toMatchInlineSnapshot(` " + import __Object_hasOwn from "/function-object-has-own.js"; import __replaceAll from "/method-replace-all.js"; import __at from "/method-at.js"; import __findLast from "/method-find-last.js"; @@ -284,7 +287,7 @@ test("All", () => { align, line } = __doc_builders; - if (Object.prototype.hasOwnProperty.call(foo, bar)) { + if (__Object_hasOwn(foo, bar)) { const a = __replaceAll(/* OPTIONAL_OBJECT: true */1, __at(/* OPTIONAL_OBJECT: true */1, foo, -1), /bar/, ""), b = __findLast(/* OPTIONAL_OBJECT: true */1, bar, () => true), c = __findLastIndex(/* OPTIONAL_OBJECT: false */0, foo, callback);