Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for getOr #36

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions __testfixtures__/getOr.input.js
Original file line number Diff line number Diff line change
@@ -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", 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);
const foo16 = getOr("works", 0, foo);
const foo17 = getOr("works", [0], foo);
const foo18 = getOr("works", 1, foo);
const foo19 = getOr("works", [1], foo);
20 changes: 20 additions & 0 deletions __testfixtures__/getOr.output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @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";
const foo16 = foo?.[0] ?? "works";
const foo17 = foo?.[0] ?? "works";
const foo18 = foo?.[1] ?? "works";
const foo19 = foo?.[1] ?? "works";
23 changes: 23 additions & 0 deletions __testfixtures__/getOrCurried.input.js
Original file line number Diff line number Diff line change
@@ -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]);
20 changes: 20 additions & 0 deletions __testfixtures__/getOrCurried.output.js
Original file line number Diff line number Diff line change
@@ -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";
19 changes: 19 additions & 0 deletions __testfixtures__/lodashFPCurried.input.js
Original file line number Diff line number Diff line change
@@ -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");
16 changes: 16 additions & 0 deletions __testfixtures__/lodashFPCurried.output.js
Original file line number Diff line number Diff line change
@@ -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"];
4 changes: 4 additions & 0 deletions __testfixtures__/transform.input.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
4 changes: 4 additions & 0 deletions __testfixtures__/transform.output.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
12 changes: 12 additions & 0 deletions __tests__/transform-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,16 @@ describe("lodash get to optional chaining", () => {
describe("import from lodash/fp", () => {
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")
});
});
88 changes: 65 additions & 23 deletions transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -74,17 +74,28 @@ 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}"`);
}
};

const skip = (node, options) => {
switch (node.value.arguments[1].type) {
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;
Expand All @@ -93,7 +104,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}"`);
}
};

Expand All @@ -104,37 +115,67 @@ 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, 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 {
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) =>
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 doCurry(node, j, 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;
// 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
.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
.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
Expand All @@ -148,7 +189,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
Expand All @@ -160,9 +201,9 @@ const mangleLodashGets = (ast, j, options, isTypescript, importLiteral = "lodash
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 ===
Expand All @@ -181,13 +222,13 @@ const mangleLodashGets = (ast, j, options, isTypescript, importLiteral = "lodash
.find("CallExpression", {
callee: {
object: { name: lodashDefaultImportName },
property: { name: "get" }
property: { name: funcName }
}
})
.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
Expand Down Expand Up @@ -310,7 +351,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();
};