From 1d650bf949eebb8f97318ecebbaa0308f3d4c6ba Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Mon, 28 Sep 2020 22:19:54 -0600 Subject: [PATCH 1/4] Add support for getOr --- __testfixtures__/getOr.input.js | 19 +++++++++++++++++ __testfixtures__/getOr.output.js | 16 +++++++++++++++ __tests__/transform-test.js | 4 ++++ transform.js | 35 +++++++++++++++++++++----------- 4 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 __testfixtures__/getOr.input.js create mode 100644 __testfixtures__/getOr.output.js diff --git a/__testfixtures__/getOr.input.js b/__testfixtures__/getOr.input.js new file mode 100644 index 0000000..6395c7d --- /dev/null +++ b/__testfixtures__/getOr.input.js @@ -0,0 +1,19 @@ +// @flow +import _ from "lodash/fp"; +import { getOr } from "lodash/fp"; +import gettOr from "lodash/fp/getOr"; +const foo1 = _.getOr(1, "a.b.c", bar); +const foo2 = getOr(2, "a.b.c", bar); +const foo3 = gettOr(3, "a.b.c", bar); +const foo4 = gettOr(4, "a[2].c", bar); +const foo5 = gettOr(5, ["a", foo5, "c"], bar); +const foo6 = gettOr(6, ["a", 321, "c"], bar); +const foo7 = gettOr(7, ["a", this.smthng, "c"], bar); +const foo8 = gettOr(8, ["a", foo5, "c"], bar); +const foo9 = _.getOr([], `a.${foo5}`, bar); +const foo10 = _.getOr({}, `a.${foo5}.smthng`, bar); +const foo11 = _.getOr([], someKey, bar); +const foo12 = _.getOr({}, that.bar, that.foo); +const foo13 = getOr([], 'bar[0]["60"]', foo); +const foo14 = getOr({}, "bar.data-thing", foo); +const foo15 = getOr("test", "data-bar[0].baz.data-thing", foo); diff --git a/__testfixtures__/getOr.output.js b/__testfixtures__/getOr.output.js new file mode 100644 index 0000000..ce0a874 --- /dev/null +++ b/__testfixtures__/getOr.output.js @@ -0,0 +1,16 @@ +// @flow +const foo1 = bar?.a?.b?.c ?? 1; +const foo2 = bar?.a?.b?.c ?? 2; +const foo3 = bar?.a?.b?.c ?? 3; +const foo4 = bar?.a?.[2]?.c ?? 4; +const foo5 = bar?.a?.[foo5]?.c ?? 5; +const foo6 = bar?.a?.[321]?.c ?? 6; +const foo7 = bar?.a?.[this.smthng]?.c ?? 7; +const foo8 = bar?.a?.[foo5]?.c ?? 8; +const foo9 = bar?.a?.[foo5] ?? []; +const foo10 = bar?.a?.[foo5]?.smthng ?? {}; +const foo11 = bar?.[someKey] ?? []; +const foo12 = that.foo?.[that.bar] ?? {}; +const foo13 = foo?.bar?.[0]?.[60] ?? []; +const foo14 = foo?.bar?.["data-thing"] ?? {}; +const foo15 = foo?.["data-bar"]?.[0]?.baz?.["data-thing"] ?? "test"; diff --git a/__tests__/transform-test.js b/__tests__/transform-test.js index 17421ed..230bdc4 100644 --- a/__tests__/transform-test.js +++ b/__tests__/transform-test.js @@ -44,4 +44,8 @@ describe("lodash get to optional chaining", () => { describe("import from lodash/fp", () => { defineTest(__dirname, "transform", null, "lodashFP") }); + + describe("import from getOr", () => { + defineTest(__dirname, "transform", null, "getOr") + }); }); diff --git a/transform.js b/transform.js index cc95998..b7e9a72 100644 --- a/transform.js +++ b/transform.js @@ -104,18 +104,27 @@ const addWithNullishCoalescing = (node, j) => node.value.arguments[2] ); -const swapArguments = (node, options) => { - const [object, path] = node.value.arguments; - node.value.arguments = [path, object]; +const swapArguments = (node, hasDefault) => { + if (hasDefault) { + const [default_, path, object] = node.value.arguments; + node.value.arguments = [object, path, default_]; + } else { + const [path, object] = node.value.arguments; + node.value.arguments = [object, path]; + } return node; }; -const replaceGetWithOptionalChain = (node, j, shouldSwapArgs) => - node.value.arguments[2] +const replaceGetWithOptionalChain = (node, j, shouldSwapArgs) => { + if (shouldSwapArgs) { + node = swapArguments(node, !!node.value.arguments[2]); + } + return node.value.arguments[2] ? addWithNullishCoalescing(node, j) - : generateOptionalChain(shouldSwapArgs ? swapArguments(node) : node, j); + : generateOptionalChain(node, j); +} -const mangleLodashGets = (ast, j, options, isTypescript, importLiteral = "lodash") => { +const mangleLodashGets = (ast, j, options, isTypescript, isGetOr, importLiteral = "lodash") => { const literal = isTypescript ? "StringLiteral" : "Literal"; const getFirstNode = () => ast.find(j.Program).get("body", 0).node; @@ -123,10 +132,11 @@ const mangleLodashGets = (ast, j, options, isTypescript, importLiteral = "lodash const firstNode = getFirstNode(); const { comments } = firstNode; const shouldSwapArgs = importLiteral === "lodash/fp"; + const funcName = isGetOr ? "getOr" : "get"; const getImportSpecifier = ast .find("ImportDeclaration", { source: { type: literal, value: importLiteral } }) - .find("ImportSpecifier", { imported: { name: "get" } }); + .find("ImportSpecifier", { imported: { name: funcName } }); if (getImportSpecifier.length) { const getName = getImportSpecifier.get().value.local.name; ast @@ -148,7 +158,7 @@ const mangleLodashGets = (ast, j, options, isTypescript, importLiteral = "lodash } const getScopedImport = ast.find("ImportDeclaration", { - source: { type: literal, value: `${importLiteral}/get` } + source: { type: literal, value: `${importLiteral}/${funcName}` } }); const getScopedSpecifier = getScopedImport @@ -181,7 +191,7 @@ const mangleLodashGets = (ast, j, options, isTypescript, importLiteral = "lodash .find("CallExpression", { callee: { object: { name: lodashDefaultImportName }, - property: { name: "get" } + property: { name: funcName } } }) .replaceWith(node => @@ -310,7 +320,8 @@ module.exports = function(fileInfo, api, options) { const j = api.jscodeshift; const ast = j(fileInfo.source); mangleNestedObjects(ast, j, options, isTypescript); - mangleLodashGets(ast, j, options, isTypescript); - mangleLodashGets(ast, j, options, isTypescript, "lodash/fp"); + mangleLodashGets(ast, j, options, isTypescript, false); + mangleLodashGets(ast, j, options, isTypescript, false, "lodash/fp"); + mangleLodashGets(ast, j, options, isTypescript, true, "lodash/fp"); return ast.toSource(); }; From 5da9507630884c05d0779403d70d7e638b58075c Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Mon, 28 Sep 2020 22:55:05 -0600 Subject: [PATCH 2/4] Fix handling of 0 in the path --- __testfixtures__/getOr.input.js | 4 ++++ __testfixtures__/getOr.output.js | 4 ++++ __testfixtures__/transform.input.js | 4 ++++ __testfixtures__/transform.output.js | 4 ++++ transform.js | 2 +- 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/__testfixtures__/getOr.input.js b/__testfixtures__/getOr.input.js index 6395c7d..6bcbc4a 100644 --- a/__testfixtures__/getOr.input.js +++ b/__testfixtures__/getOr.input.js @@ -17,3 +17,7 @@ const foo12 = _.getOr({}, that.bar, that.foo); const foo13 = getOr([], 'bar[0]["60"]', foo); const foo14 = getOr({}, "bar.data-thing", foo); const foo15 = getOr("test", "data-bar[0].baz.data-thing", foo); +const foo16 = getOr("works", 0, foo); +const foo17 = getOr("works", [0], foo); +const foo18 = getOr("works", 1, foo); +const foo19 = getOr("works", [1], foo); diff --git a/__testfixtures__/getOr.output.js b/__testfixtures__/getOr.output.js index ce0a874..9e1c876 100644 --- a/__testfixtures__/getOr.output.js +++ b/__testfixtures__/getOr.output.js @@ -14,3 +14,7 @@ const foo12 = that.foo?.[that.bar] ?? {}; const foo13 = foo?.bar?.[0]?.[60] ?? []; const foo14 = foo?.bar?.["data-thing"] ?? {}; const foo15 = foo?.["data-bar"]?.[0]?.baz?.["data-thing"] ?? "test"; +const foo16 = foo?.[0] ?? "works"; +const foo17 = foo?.[0] ?? "works"; +const foo18 = foo?.[1] ?? "works"; +const foo19 = foo?.[1] ?? "works"; diff --git a/__testfixtures__/transform.input.js b/__testfixtures__/transform.input.js index 7a17e03..078b4b5 100644 --- a/__testfixtures__/transform.input.js +++ b/__testfixtures__/transform.input.js @@ -21,3 +21,7 @@ const foo16 = get(foo, "bar.data-thing"); const foo17 = get(foo, "data-bar[0].baz.data-thing", value); const foo18 = get(foo, getPath(name)); const foo19 = get(foo, ["data-bar", 0, "baz", "data-thing"], value); +const foo20 = get(foo, 0, value); +const foo21 = get(foo, [0], value); +const foo22 = get(foo, 1, value); +const foo23 = get(foo, [1], value); diff --git a/__testfixtures__/transform.output.js b/__testfixtures__/transform.output.js index 9baf2aa..dc8f19e 100644 --- a/__testfixtures__/transform.output.js +++ b/__testfixtures__/transform.output.js @@ -18,3 +18,7 @@ const foo16 = foo?.bar?.["data-thing"]; const foo17 = foo?.["data-bar"]?.[0]?.baz?.["data-thing"] ?? value; const foo18 = foo?.[getPath(name)]; const foo19 = foo?.["data-bar"]?.[0]?.baz?.["data-thing"] ?? value; +const foo20 = foo?.[0] ?? value; +const foo21 = foo?.[0] ?? value; +const foo22 = foo?.[1] ?? value; +const foo23 = foo?.[1] ?? value; diff --git a/transform.js b/transform.js index b7e9a72..f3954b2 100644 --- a/transform.js +++ b/transform.js @@ -20,7 +20,7 @@ const replaceArrayWithOptionalChain = (node, j) => const replaceStringWithOptionalChain = (str, startNode, j) => lodashObjectPathParser(str) - .filter(Boolean) + .filter((v) => v !== '') .reduce((p, c) => { return j.optionalMemberExpression( p, From ab4ac9a253280dc3ea05117124861141bc8543f1 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Tue, 29 Sep 2020 08:58:13 -0600 Subject: [PATCH 3/4] Add support for curried versions of get and getOr --- __testfixtures__/getOrCurried.input.js | 23 ++++++++ __testfixtures__/getOrCurried.output.js | 20 +++++++ __testfixtures__/lodashFPCurried.input.js | 19 +++++++ __testfixtures__/lodashFPCurried.output.js | 16 ++++++ __tests__/transform-test.js | 8 +++ transform.js | 63 ++++++++++++++-------- 6 files changed, 128 insertions(+), 21 deletions(-) create mode 100644 __testfixtures__/getOrCurried.input.js create mode 100644 __testfixtures__/getOrCurried.output.js create mode 100644 __testfixtures__/lodashFPCurried.input.js create mode 100644 __testfixtures__/lodashFPCurried.output.js diff --git a/__testfixtures__/getOrCurried.input.js b/__testfixtures__/getOrCurried.input.js new file mode 100644 index 0000000..0f526ce --- /dev/null +++ b/__testfixtures__/getOrCurried.input.js @@ -0,0 +1,23 @@ +// @flow +import _ from "lodash/fp"; +import { getOr } from "lodash/fp"; +import gettOr from "lodash/fp/getOr"; +const foo1 = _.getOr(1, "a.b.c"); +const foo2 = getOr(2, "a.b.c"); +const foo3 = gettOr(3, "a.b.c"); +const foo4 = gettOr(4, "a[2].c"); +const foo5 = gettOr(5, ["a", foo5, "c"]); +const foo6 = gettOr(6, ["a", 321, "c"]); +const foo7 = gettOr(7, ["a", this.smthng, "c"]); +const foo8 = gettOr(8, ["a", foo5, "c"]); +const foo9 = _.getOr([], `a.${foo5}`); +const foo10 = _.getOr({}, `a.${foo5}.smthng`); +const foo11 = _.getOr([], someKey); +const foo12 = _.getOr({}, that.bar); +const foo13 = getOr([], 'bar[0]["60"]'); +const foo14 = getOr({}, "bar.data-thing"); +const foo15 = getOr("test", "data-bar[0].baz.data-thing"); +const foo16 = getOr("works", 0); +const foo17 = getOr("works", [0]); +const foo18 = getOr("works", 1); +const foo19 = getOr("works", [1]); diff --git a/__testfixtures__/getOrCurried.output.js b/__testfixtures__/getOrCurried.output.js new file mode 100644 index 0000000..d44bc1b --- /dev/null +++ b/__testfixtures__/getOrCurried.output.js @@ -0,0 +1,20 @@ +// @flow +const foo1 = o => o?.a?.b?.c ?? 1; +const foo2 = o => o?.a?.b?.c ?? 2; +const foo3 = o => o?.a?.b?.c ?? 3; +const foo4 = o => o?.a?.[2]?.c ?? 4; +const foo5 = o => o?.a?.[foo5]?.c ?? 5; +const foo6 = o => o?.a?.[321]?.c ?? 6; +const foo7 = o => o?.a?.[this.smthng]?.c ?? 7; +const foo8 = o => o?.a?.[foo5]?.c ?? 8; +const foo9 = o => o?.a?.[foo5] ?? []; +const foo10 = o => o?.a?.[foo5]?.smthng ?? {}; +const foo11 = o => o?.[someKey] ?? []; +const foo12 = o => o?.[that.bar] ?? {}; +const foo13 = o => o?.bar?.[0]?.[60] ?? []; +const foo14 = o => o?.bar?.["data-thing"] ?? {}; +const foo15 = o => o?.["data-bar"]?.[0]?.baz?.["data-thing"] ?? "test"; +const foo16 = o => o?.[0] ?? "works"; +const foo17 = o => o?.[0] ?? "works"; +const foo18 = o => o?.[1] ?? "works"; +const foo19 = o => o?.[1] ?? "works"; diff --git a/__testfixtures__/lodashFPCurried.input.js b/__testfixtures__/lodashFPCurried.input.js new file mode 100644 index 0000000..63b756b --- /dev/null +++ b/__testfixtures__/lodashFPCurried.input.js @@ -0,0 +1,19 @@ +// @flow +import _ from "lodash/fp"; +import { get } from "lodash/fp"; +import gett from "lodash/fp/get"; +const foo1 = _.get("a.b.c"); +const foo2 = get("a.b.c"); +const foo3 = gett("a.b.c"); +const foo4 = gett("a[2].c"); +const foo5 = gett(["a", foo5, "c"]); +const foo6 = gett(["a", 321, "c"]); +const foo7 = gett(["a", this.smthng, "c"]); +const foo8 = gett(["a", foo5, "c"]); +const foo9 = _.get(`a.${foo5}`); +const foo10 = _.get(`a.${foo5}.smthng`); +const foo11 = _.get(someKey); +const foo12 = _.get(that.bar); +const foo13 = get('bar[0]["60"]'); +const foo14 = get("bar.data-thing"); +const foo15 = get("data-bar[0].baz.data-thing"); diff --git a/__testfixtures__/lodashFPCurried.output.js b/__testfixtures__/lodashFPCurried.output.js new file mode 100644 index 0000000..9e0be8b --- /dev/null +++ b/__testfixtures__/lodashFPCurried.output.js @@ -0,0 +1,16 @@ +// @flow +const foo1 = o => o?.a?.b?.c; +const foo2 = o => o?.a?.b?.c; +const foo3 = o => o?.a?.b?.c; +const foo4 = o => o?.a?.[2]?.c; +const foo5 = o => o?.a?.[foo5]?.c; +const foo6 = o => o?.a?.[321]?.c; +const foo7 = o => o?.a?.[this.smthng]?.c; +const foo8 = o => o?.a?.[foo5]?.c; +const foo9 = o => o?.a?.[foo5]; +const foo10 = o => o?.a?.[foo5]?.smthng; +const foo11 = o => o?.[someKey]; +const foo12 = o => o?.[that.bar]; +const foo13 = o => o?.bar?.[0]?.[60]; +const foo14 = o => o?.bar?.["data-thing"]; +const foo15 = o => o?.["data-bar"]?.[0]?.baz?.["data-thing"]; diff --git a/__tests__/transform-test.js b/__tests__/transform-test.js index 230bdc4..a1e13d1 100644 --- a/__tests__/transform-test.js +++ b/__tests__/transform-test.js @@ -45,7 +45,15 @@ describe("lodash get to optional chaining", () => { defineTest(__dirname, "transform", null, "lodashFP") }); + describe("import from lodash/fp curried", () => { + defineTest(__dirname, "transform", null, "lodashFPCurried") + }); + describe("import from getOr", () => { defineTest(__dirname, "transform", null, "getOr") }); + + describe("import from getOr curried", () => { + defineTest(__dirname, "transform", null, "getOrCurried") + }); }); diff --git a/transform.js b/transform.js index f3954b2..16a2035 100644 --- a/transform.js +++ b/transform.js @@ -80,8 +80,9 @@ const generateOptionalChain = (node, j) => { } }; -const skip = (node, options) => { - switch (node.value.arguments[1].type) { +const skip = (node, options, isGetOr, isFp) => { + const index = isFp ? (isGetOr ? 1 : 0) : 1; + switch (node.value.arguments[index].type) { case "ArrayExpression": case "StringLiteral": case "Literal": @@ -93,7 +94,7 @@ const skip = (node, options) => { case "CallExpression": return !!options.skipVariables; default: - throw new Error(`argument type not supported "${node.value.arguments[1].type}"`); + throw new Error(`argument type not supported "${node.value.arguments[index].type}"`); } }; @@ -104,24 +105,44 @@ const addWithNullishCoalescing = (node, j) => node.value.arguments[2] ); -const swapArguments = (node, hasDefault) => { - if (hasDefault) { - const [default_, path, object] = node.value.arguments; - node.value.arguments = [object, path, default_]; +const swapArguments = (node, j, isGetOr) => { + if (isGetOr) { + if (node.value.arguments[2]) { + const [default_, path, object] = node.value.arguments; + node.value.arguments = [object, path, default_]; + } else { + const [default_, path] = node.value.arguments; + node.value.arguments = [j.identifier("o"), path, default_]; + node.value.curried = true; + } } else { - const [path, object] = node.value.arguments; - node.value.arguments = [object, path]; + if (node.value.arguments[1]) { + const [path, object] = node.value.arguments; + node.value.arguments = [object, path]; + } else { + const [path] = node.value.arguments; + node.value.arguments = [j.identifier("o"), path]; + node.value.curried = true; + } } return node; }; -const replaceGetWithOptionalChain = (node, j, shouldSwapArgs) => { - if (shouldSwapArgs) { - node = swapArguments(node, !!node.value.arguments[2]); +const doCurry = (node, j, body) => { + if (node.value && node.value.curried) { + return j.arrowFunctionExpression([{ type: "Identifier", name: "o" }], body); + } + + return body; +}; + +const replaceGetWithOptionalChain = (node, j, isGetOr, isFp) => { + if (isFp) { + node = swapArguments(node, j, isGetOr); } - return node.value.arguments[2] + return doCurry(node, j, node.value.arguments[2] ? addWithNullishCoalescing(node, j) - : generateOptionalChain(node, j); + : generateOptionalChain(node, j)); } const mangleLodashGets = (ast, j, options, isTypescript, isGetOr, importLiteral = "lodash") => { @@ -131,7 +152,7 @@ const mangleLodashGets = (ast, j, options, isTypescript, isGetOr, importLiteral // Save the comments attached to the first node const firstNode = getFirstNode(); const { comments } = firstNode; - const shouldSwapArgs = importLiteral === "lodash/fp"; + const isFp = importLiteral === "lodash/fp"; const funcName = isGetOr ? "getOr" : "get"; const getImportSpecifier = ast @@ -142,9 +163,9 @@ const mangleLodashGets = (ast, j, options, isTypescript, isGetOr, importLiteral ast .find("CallExpression", { callee: { name: getName } }) .replaceWith(node => - skip(node, options, isTypescript) + skip(node, options, isGetOr, isFp) ? node.get().value - : replaceGetWithOptionalChain(node, j, shouldSwapArgs) + : replaceGetWithOptionalChain(node, j, isGetOr, isFp) ); if ( ast.find("CallExpression", { callee: { name: getName } }).length === 0 @@ -170,9 +191,9 @@ const mangleLodashGets = (ast, j, options, isTypescript, isGetOr, importLiteral ast .find("CallExpression", { callee: { name: getScopedName } }) .replaceWith(node => - skip(node, options) + skip(node, options, isGetOr, isFp) ? node.get().value - : replaceGetWithOptionalChain(node, j, shouldSwapArgs) + : replaceGetWithOptionalChain(node, j, isGetOr, isFp) ); if ( ast.find("CallExpression", { callee: { name: getScopedName } }).length === @@ -195,9 +216,9 @@ const mangleLodashGets = (ast, j, options, isTypescript, isGetOr, importLiteral } }) .replaceWith(node => - skip(node, options) + skip(node, options, isGetOr, isFp) ? node.get().value - : replaceGetWithOptionalChain(node, j, shouldSwapArgs) + : replaceGetWithOptionalChain(node, j, isGetOr, isFp) ); const lodashIdentifiers = ast.find("Identifier", { name: lodashDefaultImportName From ee72903fe94d42678c6a77ff9eaca69c6ac8a943 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Tue, 29 Sep 2020 12:39:28 -0600 Subject: [PATCH 4/4] Allow to run on more code --- transform.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/transform.js b/transform.js index 16a2035..005cddc 100644 --- a/transform.js +++ b/transform.js @@ -74,6 +74,10 @@ const generateOptionalChain = (node, j) => { case "Identifier": case "MemberExpression": case "CallExpression": + case "ObjectExpression": + case "BinaryExpression": + case "LogicalExpression": + case "OptionalMemberExpression": return defaultOptionalChain(node, j); default: throw new Error(`argument type not supported "${node.value.arguments[1].type}"`); @@ -82,10 +86,16 @@ const generateOptionalChain = (node, j) => { const skip = (node, options, isGetOr, isFp) => { const index = isFp ? (isGetOr ? 1 : 0) : 1; + if (!node.value.arguments[index]) { + return true; + } switch (node.value.arguments[index].type) { case "ArrayExpression": case "StringLiteral": case "Literal": + case "BinaryExpression": + case "LogicalExpression": + case "OptionalMemberExpression": return false; case "TemplateLiteral": return !!options.skipTemplateStrings;