From f6bb3fe5b5ddd7e3e73720c1f81910d0272e0246 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Thu, 11 Feb 2021 16:22:51 +0000 Subject: [PATCH 01/30] Start turning folding an either into an option into an option.fromEither --- src/rules/prefer-constructor.ts | 145 +++++++++++++++++++++++++ tests/rules/prefer-constructor.test.ts | 137 +++++++++++++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 src/rules/prefer-constructor.ts create mode 100644 tests/rules/prefer-constructor.test.ts diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts new file mode 100644 index 0000000..340e959 --- /dev/null +++ b/src/rules/prefer-constructor.ts @@ -0,0 +1,145 @@ +import { + AST_NODE_TYPES, + TSESTree, +} from "@typescript-eslint/experimental-utils"; +import { constant, constVoid, flow, pipe } from 'fp-ts/function'; +import { boolean, option, readonlyNonEmptyArray } from "fp-ts"; +import { + calleeIdentifier, + contextUtils, + createRule +} from '../utils'; + +export default createRule({ + name: "prefer-constructor", + meta: { + type: "suggestion", + fixable: "code", + schema: [], + docs: { + category: "Best Practices", + description: "afsafaf", + recommended: "warn", + }, + messages: { + eitherFoldIsOptionFromEither: "cacsaffg", + replaceEitherFoldWithOptionFromEither: "dsagdgdsg", + }, + }, + defaultOptions: [], + create(context) { + const { isIdentifierImportedFrom } = contextUtils(context); + return { + CallExpression(node) { + const isEitherFold = (node: TSESTree.CallExpression) => { + return pipe( + node, + calleeIdentifier, + option.exists((callee) => callee.name === "fold") + ); + } + + const isOptionNoneArrowFunctionExpression = (node: TSESTree.ArrowFunctionExpression) => { + return pipe( + node.body, + option.of, + option.filter((arg): arg is TSESTree.MemberExpression => arg.type === AST_NODE_TYPES.MemberExpression), + option.exists(isOptionNoneMemberExpression) + ) + } + + const isOptionNoneMemberExpression = (node: TSESTree.MemberExpression) => { + return pipe( + node, + option.of, + option.filter(() => node.object?.type === AST_NODE_TYPES.Identifier && node.object.name === 'option'), + option.filter(() => node.property?.type === AST_NODE_TYPES.Identifier && node.property.name === 'none'), + option.isSome + ) + } + + const isOptionNoneCallExpression = (node: TSESTree.CallExpression) => { + return pipe( + node, + calleeIdentifier, + option.filter( + (callee) => + callee.name === "constant" && + isIdentifierImportedFrom(callee, /fp-ts\//, context) + ), + option.map(constant(node.arguments[0])), + option.chain(option.fromNullable), + option.filter((arg): arg is TSESTree.MemberExpression => arg.type === AST_NODE_TYPES.MemberExpression), + option.exists(isOptionNoneMemberExpression) + ) + } + + const isOptionNone = (node: TSESTree.Expression) => { + switch (node.type) { + case AST_NODE_TYPES.ArrowFunctionExpression: + return isOptionNoneArrowFunctionExpression(node); + case AST_NODE_TYPES.CallExpression: + return isOptionNoneCallExpression(node); + case AST_NODE_TYPES.MemberExpression: + return isOptionNoneMemberExpression(node); + default: + return false; + } + }; + + const isOptionSomeValue = (node: TSESTree.Expression) => { + return pipe( + node, + option.of, + option.map((n) => n.type === AST_NODE_TYPES.ArrowFunctionExpression && n.body.type === AST_NODE_TYPES.CallExpression ? n.body.callee : n), + option.filter((n): n is TSESTree.MemberExpression => n.type === AST_NODE_TYPES.MemberExpression), + option.exists( + (body) => + body.object?.type === AST_NODE_TYPES.Identifier && body.object.name === 'option' + && + body.property?.type === AST_NODE_TYPES.Identifier && body.property.name === 'some' + && + (body.parent?.type !== AST_NODE_TYPES.CallExpression || body.parent.parent?.type !== AST_NODE_TYPES.ArrowFunctionExpression + || + (body.parent.arguments.length === 1 && body.parent.parent.params.length === 1 && body.parent.arguments[0]?.type === AST_NODE_TYPES.Identifier && body.parent.parent.params[0]?.type === AST_NODE_TYPES.Identifier && body.parent.arguments[0].name === body.parent.parent.params[0].name)) + ), + ); + } + pipe( + node, + isEitherFold, + boolean.fold(constVoid, () => + pipe( + readonlyNonEmptyArray.fromArray(node.arguments), + option.filter((args) => args.length === 2), + option.filter(flow(readonlyNonEmptyArray.head, isOptionNone)), + option.filter(flow(readonlyNonEmptyArray.last, isOptionSomeValue)), + option.map(() => { + context.report({ + loc: { + start: node.loc.start, + end: node.loc.end, + }, + messageId: "eitherFoldIsOptionFromEither", + suggest: [ + { + messageId: "replaceEitherFoldWithOptionFromEither", + fix(fixer) { + return [ + fixer.replaceTextRange( + node.range, + `option.fromEither` + ), + ]; + }, + }, + ], + }); + }) + ) + ) + ); + }, + }; + }, +}); diff --git a/tests/rules/prefer-constructor.test.ts b/tests/rules/prefer-constructor.test.ts new file mode 100644 index 0000000..8785821 --- /dev/null +++ b/tests/rules/prefer-constructor.test.ts @@ -0,0 +1,137 @@ +import { stripIndent } from 'common-tags'; +import rule from "../../src/rules/prefer-constructor"; +import { ESLintUtils } from "@typescript-eslint/experimental-utils"; + +const ruleTester = new ESLintUtils.RuleTester({ + parser: "@typescript-eslint/parser", +}); + +ruleTester.run("prefer-constructor", rule, { + valid: [ + { + code: stripIndent` + import { either, option } from "fp-ts" + import { pipe } from "fp-ts/function" + + pipe( + either.of(1), + either.fold(() => option.none, (value) => option.some(otherValue)) + ) + `, + }, + { + code: stripIndent` + import { either, option } from "fp-ts" + import { pipe } from "fp-ts/function" + + pipe( + either.of(1), + either.fold(() => option.some(otherValue), (value) => option.some(value)) + ) + `, + }, + { + code: stripIndent` + import { either, option } from "fp-ts" + import { pipe } from "fp-ts/function" + + pipe( + either.of(1), + either.fold(() => option.some(otherValue), () => option.none) + ) + `, + }, + ], + invalid: [ + { + code: stripIndent` + import { either, option } from "fp-ts" + import { pipe } from "fp-ts/function" + + pipe( + either.of(1), + either.fold(() => option.none, (value) => option.some(value)) + ) + `, + errors: [ + { + messageId: "eitherFoldIsOptionFromEither", + suggestions: [ + { + messageId: "replaceEitherFoldWithOptionFromEither", + output: stripIndent` + import { either, option } from "fp-ts" + import { pipe } from "fp-ts/function" + + pipe( + either.of(1), + option.fromEither + ) + `, + }, + ], + }, + ], + }, + { + code: stripIndent` + import { either, option } from "fp-ts" + import { pipe } from "fp-ts/function" + + pipe( + either.of(1), + either.fold(() => option.none, option.some) + ) + `, + errors: [ + { + messageId: "eitherFoldIsOptionFromEither", + suggestions: [ + { + messageId: "replaceEitherFoldWithOptionFromEither", + output: stripIndent` + import { either, option } from "fp-ts" + import { pipe } from "fp-ts/function" + + pipe( + either.of(1), + option.fromEither + ) + `, + }, + ], + }, + ], + }, + { + code: stripIndent` + import { either, option } from "fp-ts" + import { constant, pipe } from "fp-ts/function" + + pipe( + either.of(1), + either.fold(constant(option.none), option.some) + ) + `, + errors: [ + { + messageId: "eitherFoldIsOptionFromEither", + suggestions: [ + { + messageId: "replaceEitherFoldWithOptionFromEither", + output: stripIndent` + import { either, option } from "fp-ts" + import { constant, pipe } from "fp-ts/function" + + pipe( + either.of(1), + option.fromEither + ) + `, + }, + ], + }, + ], + }, + ], +}); From 415aa5bb06fea242f9073ef069d044de38c6c782 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 12 Feb 2021 09:07:39 +0000 Subject: [PATCH 02/30] Only work on option.fold --- src/rules/prefer-constructor.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index 340e959..047eebd 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -35,7 +35,12 @@ export default createRule({ return pipe( node, calleeIdentifier, - option.exists((callee) => callee.name === "fold") + option.filter((callee) => callee.name === "fold"), + option.chain(flow((callee) => callee.parent, option.fromNullable)), + option.filter((parent): parent is TSESTree.MemberExpression => parent.type === AST_NODE_TYPES.MemberExpression), + option.map((parent) => parent.object), + option.filter((object): object is TSESTree.Identifier => object.type === AST_NODE_TYPES.Identifier), + option.exists((object) => object.name === 'either') ); } From a864859612a75253e7baee7c08a06aa29e547331 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 12 Feb 2021 11:04:19 +0000 Subject: [PATCH 03/30] Match project style more --- src/rules/prefer-constructor.ts | 138 +++++++++++++++----------------- 1 file changed, 65 insertions(+), 73 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index 047eebd..2519556 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -1,14 +1,7 @@ -import { - AST_NODE_TYPES, - TSESTree, -} from "@typescript-eslint/experimental-utils"; -import { constant, constVoid, flow, pipe } from 'fp-ts/function'; -import { boolean, option, readonlyNonEmptyArray } from "fp-ts"; -import { - calleeIdentifier, - contextUtils, - createRule -} from '../utils'; +import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/experimental-utils" +import { boolean, option, readonlyNonEmptyArray } from "fp-ts" +import { constVoid, flow, pipe } from "fp-ts/function" +import { calleeIdentifier, contextUtils, createRule } from "../utils" export default createRule({ name: "prefer-constructor", @@ -19,30 +12,30 @@ export default createRule({ docs: { category: "Best Practices", description: "afsafaf", - recommended: "warn", + recommended: "warn" }, messages: { eitherFoldIsOptionFromEither: "cacsaffg", - replaceEitherFoldWithOptionFromEither: "dsagdgdsg", - }, + replaceEitherFoldWithOptionFromEither: "dsagdgdsg" + } }, defaultOptions: [], create(context) { - const { isIdentifierImportedFrom } = contextUtils(context); + const { isIdentifierImportedFrom } = contextUtils(context) return { CallExpression(node) { const isEitherFold = (node: TSESTree.CallExpression) => { - return pipe( - node, - calleeIdentifier, - option.filter((callee) => callee.name === "fold"), - option.chain(flow((callee) => callee.parent, option.fromNullable)), - option.filter((parent): parent is TSESTree.MemberExpression => parent.type === AST_NODE_TYPES.MemberExpression), - option.map((parent) => parent.object), - option.filter((object): object is TSESTree.Identifier => object.type === AST_NODE_TYPES.Identifier), - option.exists((object) => object.name === 'either') - ); - } + return pipe( + node, + calleeIdentifier, + option.filter((callee) => callee.name === "fold"), + option.chain(flow((callee) => callee.parent, option.fromNullable)), + option.filter((parent): parent is TSESTree.MemberExpression => parent.type === AST_NODE_TYPES.MemberExpression), + option.map((parent) => parent.object), + option.filter((object): object is TSESTree.Identifier => object.type === AST_NODE_TYPES.Identifier), + option.exists((object) => object.name === "either") + ) + } const isOptionNoneArrowFunctionExpression = (node: TSESTree.ArrowFunctionExpression) => { return pipe( @@ -57,42 +50,41 @@ export default createRule({ return pipe( node, option.of, - option.filter(() => node.object?.type === AST_NODE_TYPES.Identifier && node.object.name === 'option'), - option.filter(() => node.property?.type === AST_NODE_TYPES.Identifier && node.property.name === 'none'), + option.filter(() => node.object?.type === AST_NODE_TYPES.Identifier && node.object.name === "option"), + option.filter(() => node.property?.type === AST_NODE_TYPES.Identifier && node.property.name === "none"), option.isSome ) } - const isOptionNoneCallExpression = (node: TSESTree.CallExpression) => { - return pipe( - node, - calleeIdentifier, - option.filter( - (callee) => - callee.name === "constant" && - isIdentifierImportedFrom(callee, /fp-ts\//, context) - ), - option.map(constant(node.arguments[0])), - option.chain(option.fromNullable), - option.filter((arg): arg is TSESTree.MemberExpression => arg.type === AST_NODE_TYPES.MemberExpression), - option.exists(isOptionNoneMemberExpression) - ) - } + const isOptionNoneCallExpression = (node: TSESTree.CallExpression) => { + return pipe( + node, + calleeIdentifier, + option.filter( + (callee) => + callee.name === "constant" && + isIdentifierImportedFrom(callee, /fp-ts\//, context) + ), + option.chain(() => pipe(node.arguments[0], option.fromNullable)), + option.filter((arg): arg is TSESTree.MemberExpression => arg.type === AST_NODE_TYPES.MemberExpression), + option.exists(isOptionNoneMemberExpression) + ) + } - const isOptionNone = (node: TSESTree.Expression) => { - switch (node.type) { - case AST_NODE_TYPES.ArrowFunctionExpression: - return isOptionNoneArrowFunctionExpression(node); - case AST_NODE_TYPES.CallExpression: - return isOptionNoneCallExpression(node); - case AST_NODE_TYPES.MemberExpression: - return isOptionNoneMemberExpression(node); - default: - return false; - } - }; + const isOptionNone = (node: TSESTree.Expression) => { + switch (node.type) { + case AST_NODE_TYPES.ArrowFunctionExpression: + return isOptionNoneArrowFunctionExpression(node) + case AST_NODE_TYPES.CallExpression: + return isOptionNoneCallExpression(node) + case AST_NODE_TYPES.MemberExpression: + return isOptionNoneMemberExpression(node) + default: + return false + } + } - const isOptionSomeValue = (node: TSESTree.Expression) => { + const isOptionSomeValue = (node: TSESTree.Expression) => { return pipe( node, option.of, @@ -100,15 +92,15 @@ export default createRule({ option.filter((n): n is TSESTree.MemberExpression => n.type === AST_NODE_TYPES.MemberExpression), option.exists( (body) => - body.object?.type === AST_NODE_TYPES.Identifier && body.object.name === 'option' + body.object?.type === AST_NODE_TYPES.Identifier && body.object.name === "option" && - body.property?.type === AST_NODE_TYPES.Identifier && body.property.name === 'some' + body.property?.type === AST_NODE_TYPES.Identifier && body.property.name === "some" && (body.parent?.type !== AST_NODE_TYPES.CallExpression || body.parent.parent?.type !== AST_NODE_TYPES.ArrowFunctionExpression - || + || (body.parent.arguments.length === 1 && body.parent.parent.params.length === 1 && body.parent.arguments[0]?.type === AST_NODE_TYPES.Identifier && body.parent.parent.params[0]?.type === AST_NODE_TYPES.Identifier && body.parent.arguments[0].name === body.parent.parent.params[0].name)) - ), - ); + ) + ) } pipe( node, @@ -123,7 +115,7 @@ export default createRule({ context.report({ loc: { start: node.loc.start, - end: node.loc.end, + end: node.loc.end }, messageId: "eitherFoldIsOptionFromEither", suggest: [ @@ -134,17 +126,17 @@ export default createRule({ fixer.replaceTextRange( node.range, `option.fromEither` - ), - ]; - }, - }, - ], - }); + ) + ] + } + } + ] + }) }) ) ) - ); - }, - }; - }, -}); + ) + } + } + } +}) From dcecac36785b74871eedcef07e8611d1a1f8b7cf Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 12 Feb 2021 11:45:44 +0000 Subject: [PATCH 04/30] Extract some functions --- src/rules/prefer-constructor.ts | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index 2519556..6905931 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -3,6 +3,17 @@ import { boolean, option, readonlyNonEmptyArray } from "fp-ts" import { constVoid, flow, pipe } from "fp-ts/function" import { calleeIdentifier, contextUtils, createRule } from "../utils" +const hasName = (name: string) => (identifier: TSESTree.Identifier) => identifier.name === name + +const isMemberExpression = (node: TSESTree.Node): node is TSESTree.MemberExpression => node.type === AST_NODE_TYPES.MemberExpression + +const isIdentifier = (node: TSESTree.Node): node is TSESTree.Identifier => node.type === AST_NODE_TYPES.Identifier + +const getParent = (identifier: TSESTree.BaseNode) => pipe( + identifier.parent, + option.fromNullable, +); + export default createRule({ name: "prefer-constructor", meta: { @@ -28,12 +39,12 @@ export default createRule({ return pipe( node, calleeIdentifier, - option.filter((callee) => callee.name === "fold"), - option.chain(flow((callee) => callee.parent, option.fromNullable)), - option.filter((parent): parent is TSESTree.MemberExpression => parent.type === AST_NODE_TYPES.MemberExpression), + option.filter(hasName("fold")), + option.chain(getParent), + option.filter(isMemberExpression), option.map((parent) => parent.object), - option.filter((object): object is TSESTree.Identifier => object.type === AST_NODE_TYPES.Identifier), - option.exists((object) => object.name === "either") + option.filter(isIdentifier), + option.exists(hasName("either")) ) } @@ -41,7 +52,7 @@ export default createRule({ return pipe( node.body, option.of, - option.filter((arg): arg is TSESTree.MemberExpression => arg.type === AST_NODE_TYPES.MemberExpression), + option.filter(isMemberExpression), option.exists(isOptionNoneMemberExpression) ) } @@ -66,7 +77,7 @@ export default createRule({ isIdentifierImportedFrom(callee, /fp-ts\//, context) ), option.chain(() => pipe(node.arguments[0], option.fromNullable)), - option.filter((arg): arg is TSESTree.MemberExpression => arg.type === AST_NODE_TYPES.MemberExpression), + option.filter(isMemberExpression), option.exists(isOptionNoneMemberExpression) ) } @@ -89,7 +100,7 @@ export default createRule({ node, option.of, option.map((n) => n.type === AST_NODE_TYPES.ArrowFunctionExpression && n.body.type === AST_NODE_TYPES.CallExpression ? n.body.callee : n), - option.filter((n): n is TSESTree.MemberExpression => n.type === AST_NODE_TYPES.MemberExpression), + option.filter(isMemberExpression), option.exists( (body) => body.object?.type === AST_NODE_TYPES.Identifier && body.object.name === "option" From d7c872964bcfb2cb11890e19c7aa62452a240100 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 12 Feb 2021 12:00:12 +0000 Subject: [PATCH 05/30] Make use of functions --- src/rules/prefer-constructor.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index 6905931..f4549ca 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -11,8 +11,8 @@ const isIdentifier = (node: TSESTree.Node): node is TSESTree.Identifier => node. const getParent = (identifier: TSESTree.BaseNode) => pipe( identifier.parent, - option.fromNullable, -); + option.fromNullable +) export default createRule({ name: "prefer-constructor", @@ -61,8 +61,20 @@ export default createRule({ return pipe( node, option.of, - option.filter(() => node.object?.type === AST_NODE_TYPES.Identifier && node.object.name === "option"), - option.filter(() => node.property?.type === AST_NODE_TYPES.Identifier && node.property.name === "none"), + option.filter(flow( + (n) => n.object, + option.of, + option.filter(isIdentifier), + option.filter(hasName("option")), + option.isSome + )), + option.filter(flow( + (n) => n.property, + option.of, + option.filter(isIdentifier), + option.filter(hasName("none")), + option.isSome + )), option.isSome ) } @@ -71,11 +83,8 @@ export default createRule({ return pipe( node, calleeIdentifier, - option.filter( - (callee) => - callee.name === "constant" && - isIdentifierImportedFrom(callee, /fp-ts\//, context) - ), + option.filter(hasName("constant")), + option.filter((callee) => isIdentifierImportedFrom(callee, /fp-ts\//, context)), option.chain(() => pipe(node.arguments[0], option.fromNullable)), option.filter(isMemberExpression), option.exists(isOptionNoneMemberExpression) From 89ea792a6441adc49962d5af053c0329a4ced173 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 12 Feb 2021 12:13:08 +0000 Subject: [PATCH 06/30] Use a pipe --- src/rules/prefer-constructor.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index f4549ca..5237bdf 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -1,10 +1,14 @@ import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/experimental-utils" import { boolean, option, readonlyNonEmptyArray } from "fp-ts" -import { constVoid, flow, pipe } from "fp-ts/function" +import { constant, constVoid, flow, pipe } from "fp-ts/function" import { calleeIdentifier, contextUtils, createRule } from "../utils" const hasName = (name: string) => (identifier: TSESTree.Identifier) => identifier.name === name +const isArrowFunctionExpression = (node: TSESTree.Node): node is TSESTree.ArrowFunctionExpression => node.type === AST_NODE_TYPES.ArrowFunctionExpression + +const isCallExpression = (node: TSESTree.Node): node is TSESTree.CallExpression => node.type === AST_NODE_TYPES.CallExpression + const isMemberExpression = (node: TSESTree.Node): node is TSESTree.MemberExpression => node.type === AST_NODE_TYPES.MemberExpression const isIdentifier = (node: TSESTree.Node): node is TSESTree.Identifier => node.type === AST_NODE_TYPES.Identifier @@ -108,7 +112,15 @@ export default createRule({ return pipe( node, option.of, - option.map((n) => n.type === AST_NODE_TYPES.ArrowFunctionExpression && n.body.type === AST_NODE_TYPES.CallExpression ? n.body.callee : n), + option.map((n) => pipe( + n, + option.of, + option.filter(isArrowFunctionExpression), + option.map((f) => f.body), + option.filter(isCallExpression), + option.map((e) => e.callee), + option.getOrElse(constant(n)) + )), option.filter(isMemberExpression), option.exists( (body) => From 7ec2f9f8c2a8056e22d1e233e4d0db041f10a1c6 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 12 Feb 2021 12:21:25 +0000 Subject: [PATCH 07/30] More pipes --- src/rules/prefer-constructor.ts | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index 5237bdf..e0f6017 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -122,15 +122,30 @@ export default createRule({ option.getOrElse(constant(n)) )), option.filter(isMemberExpression), - option.exists( - (body) => - body.object?.type === AST_NODE_TYPES.Identifier && body.object.name === "option" - && - body.property?.type === AST_NODE_TYPES.Identifier && body.property.name === "some" - && - (body.parent?.type !== AST_NODE_TYPES.CallExpression || body.parent.parent?.type !== AST_NODE_TYPES.ArrowFunctionExpression - || - (body.parent.arguments.length === 1 && body.parent.parent.params.length === 1 && body.parent.arguments[0]?.type === AST_NODE_TYPES.Identifier && body.parent.parent.params[0]?.type === AST_NODE_TYPES.Identifier && body.parent.arguments[0].name === body.parent.parent.params[0].name)) + option.filter((body) => pipe( + body.object, + option.of, + option.filter(isIdentifier), + option.exists(hasName("option")) + )), + option.filter((body) => pipe( + body.property, + option.of, + option.filter(isIdentifier), + option.exists(hasName("some")) + )), + option.map((body) => body.parent), + option.exists((parent) => ( + parent?.type !== AST_NODE_TYPES.CallExpression + || parent.parent?.type !== AST_NODE_TYPES.ArrowFunctionExpression + || ( + parent.arguments.length === 1 + && parent.parent.params.length === 1 + && parent.arguments[0]?.type === AST_NODE_TYPES.Identifier + && parent.parent.params[0]?.type === AST_NODE_TYPES.Identifier + && parent.arguments[0].name === parent.parent.params[0].name + ) + ) ) ) } From a3346ab906293ce3a6149c0082030cbe4cda3059 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 12 Feb 2021 13:11:57 +0000 Subject: [PATCH 08/30] Filter out undefined --- src/rules/prefer-constructor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index e0f6017..a698c70 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -134,9 +134,9 @@ export default createRule({ option.filter(isIdentifier), option.exists(hasName("some")) )), - option.map((body) => body.parent), + option.chain(flow((body) => body.parent, option.fromNullable)), option.exists((parent) => ( - parent?.type !== AST_NODE_TYPES.CallExpression + parent.type !== AST_NODE_TYPES.CallExpression || parent.parent?.type !== AST_NODE_TYPES.ArrowFunctionExpression || ( parent.arguments.length === 1 From c696acfec891272a5c5dbfa141375fef5ba20565 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 12 Feb 2021 13:51:32 +0000 Subject: [PATCH 09/30] Separate as a filter --- src/rules/prefer-constructor.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index a698c70..85e2751 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -18,6 +18,11 @@ const getParent = (identifier: TSESTree.BaseNode) => pipe( option.fromNullable ) +const getFirstArgument = (call: TSESTree.CallExpression) => pipe( + call.arguments[0], + option.fromNullable +) + export default createRule({ name: "prefer-constructor", meta: { @@ -86,10 +91,14 @@ export default createRule({ const isOptionNoneCallExpression = (node: TSESTree.CallExpression) => { return pipe( node, - calleeIdentifier, - option.filter(hasName("constant")), - option.filter((callee) => isIdentifierImportedFrom(callee, /fp-ts\//, context)), - option.chain(() => pipe(node.arguments[0], option.fromNullable)), + option.of, + option.filter(flow( + calleeIdentifier, + option.filter(hasName("constant")), + option.filter((callee) => isIdentifierImportedFrom(callee, /fp-ts\//, context)), + option.isSome + )), + option.chain(getFirstArgument), option.filter(isMemberExpression), option.exists(isOptionNoneMemberExpression) ) From f5c0bcf894a2ef29379b5c9d3fee3fc4eb1047da Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 12 Feb 2021 14:53:16 +0000 Subject: [PATCH 10/30] Refactoring --- src/rules/prefer-constructor.ts | 85 ++++++++++++++++----------------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index 85e2751..fa346a5 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -131,66 +131,65 @@ export default createRule({ option.getOrElse(constant(n)) )), option.filter(isMemberExpression), - option.filter((body) => pipe( - body.object, + option.filter(flow( + (body) => body.object, option.of, option.filter(isIdentifier), option.exists(hasName("option")) )), - option.filter((body) => pipe( - body.property, + option.filter(flow( + (body) => body.property, option.of, option.filter(isIdentifier), option.exists(hasName("some")) )), - option.chain(flow((body) => body.parent, option.fromNullable)), + option.chain(getParent), option.exists((parent) => ( - parent.type !== AST_NODE_TYPES.CallExpression - || parent.parent?.type !== AST_NODE_TYPES.ArrowFunctionExpression - || ( - parent.arguments.length === 1 - && parent.parent.params.length === 1 - && parent.arguments[0]?.type === AST_NODE_TYPES.Identifier - && parent.parent.params[0]?.type === AST_NODE_TYPES.Identifier - && parent.arguments[0].name === parent.parent.params[0].name - ) + !isCallExpression(parent) + || !(parent.parent) + || !isArrowFunctionExpression(parent.parent) + || ( + parent.arguments.length === 1 + && parent.parent.params.length === 1 + && parent.arguments[0]?.type === AST_NODE_TYPES.Identifier + && parent.parent.params[0]?.type === AST_NODE_TYPES.Identifier + && parent.arguments[0].name === parent.parent.params[0].name ) - ) + )) ) } pipe( node, isEitherFold, - boolean.fold(constVoid, () => - pipe( - readonlyNonEmptyArray.fromArray(node.arguments), - option.filter((args) => args.length === 2), - option.filter(flow(readonlyNonEmptyArray.head, isOptionNone)), - option.filter(flow(readonlyNonEmptyArray.last, isOptionSomeValue)), - option.map(() => { - context.report({ - loc: { - start: node.loc.start, - end: node.loc.end - }, - messageId: "eitherFoldIsOptionFromEither", - suggest: [ - { - messageId: "replaceEitherFoldWithOptionFromEither", - fix(fixer) { - return [ - fixer.replaceTextRange( - node.range, - `option.fromEither` - ) - ] - } + boolean.fold(constVoid, () => pipe( + node.arguments, + readonlyNonEmptyArray.fromArray, + option.filter((args) => args.length === 2), + option.filter(flow(readonlyNonEmptyArray.head, isOptionNone)), + option.filter(flow(readonlyNonEmptyArray.last, isOptionSomeValue)), + option.map(() => { + context.report({ + loc: { + start: node.loc.start, + end: node.loc.end + }, + messageId: "eitherFoldIsOptionFromEither", + suggest: [ + { + messageId: "replaceEitherFoldWithOptionFromEither", + fix(fixer) { + return [ + fixer.replaceTextRange( + node.range, + `option.fromEither` + ) + ] } - ] - }) + } + ] }) - ) - ) + }) + )) ) } } From 345146b0bd12fdb3782221e3885e75282b7dc1fb Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 12 Feb 2021 15:28:20 +0000 Subject: [PATCH 11/30] Use Do notation --- src/rules/prefer-constructor.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index fa346a5..bf0a3bb 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -7,6 +7,10 @@ const hasName = (name: string) => (identifier: TSESTree.Identifier) => identifie const isArrowFunctionExpression = (node: TSESTree.Node): node is TSESTree.ArrowFunctionExpression => node.type === AST_NODE_TYPES.ArrowFunctionExpression +const isFunctionExpression = (node: TSESTree.Node): node is TSESTree.FunctionExpression => node.type === AST_NODE_TYPES.FunctionExpression + +const isFunctionLike = (node: TSESTree.Node): node is TSESTree.FunctionLike => isArrowFunctionExpression(node) || isFunctionExpression(node) + const isCallExpression = (node: TSESTree.Node): node is TSESTree.CallExpression => node.type === AST_NODE_TYPES.CallExpression const isMemberExpression = (node: TSESTree.Node): node is TSESTree.MemberExpression => node.type === AST_NODE_TYPES.MemberExpression @@ -23,6 +27,11 @@ const getFirstArgument = (call: TSESTree.CallExpression) => pipe( option.fromNullable ) +const getFirstParam = (call: TSESTree.FunctionLike) => pipe( + call.params[0], + option.fromNullable +) + export default createRule({ name: "prefer-constructor", meta: { @@ -148,12 +157,11 @@ export default createRule({ !isCallExpression(parent) || !(parent.parent) || !isArrowFunctionExpression(parent.parent) - || ( - parent.arguments.length === 1 - && parent.parent.params.length === 1 - && parent.arguments[0]?.type === AST_NODE_TYPES.Identifier - && parent.parent.params[0]?.type === AST_NODE_TYPES.Identifier - && parent.arguments[0].name === parent.parent.params[0].name + || pipe( + option.Do, + option.bind('argument', () => pipe(parent, getFirstArgument, option.filter(isIdentifier))), + option.bind('param', () => pipe(parent, getParent, option.filter(isFunctionLike), option.chain(getFirstParam), option.filter(isIdentifier))), + option.exists(({ argument, param }) => hasName(argument.name)(param)) ) )) ) From 484b51e047412e2885dc9890bd320edecbb7811c Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 12 Feb 2021 15:42:31 +0000 Subject: [PATCH 12/30] Use a simpler pipe --- src/rules/prefer-constructor.ts | 84 +++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index bf0a3bb..1981d56 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -1,10 +1,12 @@ import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/experimental-utils" -import { boolean, option, readonlyNonEmptyArray } from "fp-ts" -import { constant, constVoid, flow, pipe } from "fp-ts/function" +import { option, readonlyNonEmptyArray } from "fp-ts" +import { constant, flow, pipe } from "fp-ts/function" import { calleeIdentifier, contextUtils, createRule } from "../utils" const hasName = (name: string) => (identifier: TSESTree.Identifier) => identifier.name === name +const hasLength = (length: number) => (array: ReadonlyArray) => array.length === length + const isArrowFunctionExpression = (node: TSESTree.Node): node is TSESTree.ArrowFunctionExpression => node.type === AST_NODE_TYPES.ArrowFunctionExpression const isFunctionExpression = (node: TSESTree.Node): node is TSESTree.FunctionExpression => node.type === AST_NODE_TYPES.FunctionExpression @@ -22,14 +24,26 @@ const getParent = (identifier: TSESTree.BaseNode) => pipe( option.fromNullable ) +const getArguments = (call: TSESTree.CallExpression) => pipe( + call.arguments, + readonlyNonEmptyArray.fromArray, +) + const getFirstArgument = (call: TSESTree.CallExpression) => pipe( - call.arguments[0], - option.fromNullable + call, + getArguments, + option.map(readonlyNonEmptyArray.head) +) + +const getParams = (call: TSESTree.FunctionLike) => pipe( + call.params, + readonlyNonEmptyArray.fromArray, ) const getFirstParam = (call: TSESTree.FunctionLike) => pipe( - call.params[0], - option.fromNullable + call, + getParams, + option.map(readonlyNonEmptyArray.head) ) export default createRule({ @@ -159,8 +173,8 @@ export default createRule({ || !isArrowFunctionExpression(parent.parent) || pipe( option.Do, - option.bind('argument', () => pipe(parent, getFirstArgument, option.filter(isIdentifier))), - option.bind('param', () => pipe(parent, getParent, option.filter(isFunctionLike), option.chain(getFirstParam), option.filter(isIdentifier))), + option.bind("argument", () => pipe(parent, getFirstArgument, option.filter(isIdentifier))), + option.bind("param", () => pipe(parent, getParent, option.filter(isFunctionLike), option.chain(getFirstParam), option.filter(isIdentifier))), option.exists(({ argument, param }) => hasName(argument.name)(param)) ) )) @@ -168,36 +182,34 @@ export default createRule({ } pipe( node, - isEitherFold, - boolean.fold(constVoid, () => pipe( - node.arguments, - readonlyNonEmptyArray.fromArray, - option.filter((args) => args.length === 2), - option.filter(flow(readonlyNonEmptyArray.head, isOptionNone)), - option.filter(flow(readonlyNonEmptyArray.last, isOptionSomeValue)), - option.map(() => { - context.report({ - loc: { - start: node.loc.start, - end: node.loc.end - }, - messageId: "eitherFoldIsOptionFromEither", - suggest: [ - { - messageId: "replaceEitherFoldWithOptionFromEither", - fix(fixer) { - return [ - fixer.replaceTextRange( - node.range, - `option.fromEither` - ) - ] - } + option.of, + option.filter(isEitherFold), + option.chain(getArguments), + option.filter(hasLength(2)), + option.filter(flow(readonlyNonEmptyArray.head, isOptionNone)), + option.filter(flow(readonlyNonEmptyArray.last, isOptionSomeValue)), + option.map(() => { + context.report({ + loc: { + start: node.loc.start, + end: node.loc.end + }, + messageId: "eitherFoldIsOptionFromEither", + suggest: [ + { + messageId: "replaceEitherFoldWithOptionFromEither", + fix(fixer) { + return [ + fixer.replaceTextRange( + node.range, + `option.fromEither` + ) + ] } - ] - }) + } + ] }) - )) + }) ) } } From c0175cccd14e059421f475ae62c816e1894ea550 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 12 Feb 2021 15:53:25 +0000 Subject: [PATCH 13/30] Extract some functions --- src/rules/prefer-constructor.ts | 59 ++++++++++++++------------------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index 1981d56..fb3f644 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -26,7 +26,7 @@ const getParent = (identifier: TSESTree.BaseNode) => pipe( const getArguments = (call: TSESTree.CallExpression) => pipe( call.arguments, - readonlyNonEmptyArray.fromArray, + readonlyNonEmptyArray.fromArray ) const getFirstArgument = (call: TSESTree.CallExpression) => pipe( @@ -37,7 +37,7 @@ const getFirstArgument = (call: TSESTree.CallExpression) => pipe( const getParams = (call: TSESTree.FunctionLike) => pipe( call.params, - readonlyNonEmptyArray.fromArray, + readonlyNonEmptyArray.fromArray ) const getFirstParam = (call: TSESTree.FunctionLike) => pipe( @@ -46,6 +46,23 @@ const getFirstParam = (call: TSESTree.FunctionLike) => pipe( option.map(readonlyNonEmptyArray.head) ) +const isIdentifierWithName = (name: string) => (node: TSESTree.Node) => pipe( + node, + option.of, + option.filter(isIdentifier), + option.exists(hasName(name)) +) + +const hasObjectIdentifierWithName = (name: string) => (node: TSESTree.MemberExpression) => pipe( + node.object, + isIdentifierWithName(name) +) + +const hasPropertyIdentifierWithName = (name: string) => (node: TSESTree.MemberExpression) => pipe( + node.property, + isIdentifierWithName(name) +) + export default createRule({ name: "prefer-constructor", meta: { @@ -74,9 +91,7 @@ export default createRule({ option.filter(hasName("fold")), option.chain(getParent), option.filter(isMemberExpression), - option.map((parent) => parent.object), - option.filter(isIdentifier), - option.exists(hasName("either")) + option.exists(hasObjectIdentifierWithName("either")) ) } @@ -93,21 +108,8 @@ export default createRule({ return pipe( node, option.of, - option.filter(flow( - (n) => n.object, - option.of, - option.filter(isIdentifier), - option.filter(hasName("option")), - option.isSome - )), - option.filter(flow( - (n) => n.property, - option.of, - option.filter(isIdentifier), - option.filter(hasName("none")), - option.isSome - )), - option.isSome + option.filter(hasObjectIdentifierWithName("option")), + option.exists(hasPropertyIdentifierWithName("none")) ) } @@ -118,8 +120,7 @@ export default createRule({ option.filter(flow( calleeIdentifier, option.filter(hasName("constant")), - option.filter((callee) => isIdentifierImportedFrom(callee, /fp-ts\//, context)), - option.isSome + option.exists((callee) => isIdentifierImportedFrom(callee, /fp-ts\//, context)) )), option.chain(getFirstArgument), option.filter(isMemberExpression), @@ -154,18 +155,8 @@ export default createRule({ option.getOrElse(constant(n)) )), option.filter(isMemberExpression), - option.filter(flow( - (body) => body.object, - option.of, - option.filter(isIdentifier), - option.exists(hasName("option")) - )), - option.filter(flow( - (body) => body.property, - option.of, - option.filter(isIdentifier), - option.exists(hasName("some")) - )), + option.filter(hasObjectIdentifierWithName("option")), + option.filter(hasPropertyIdentifierWithName("some")), option.chain(getParent), option.exists((parent) => ( !isCallExpression(parent) From b38817739b48ac29a28aea06eb52e50c70edb223 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 12 Feb 2021 16:18:09 +0000 Subject: [PATCH 14/30] Extract a pipe --- src/rules/prefer-constructor.ts | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index fb3f644..b9e9335 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -141,19 +141,30 @@ export default createRule({ } } + const getBodyCallee = (node: TSESTree.Expression) => { + return pipe( + node, + option.of, + option.filter(isArrowFunctionExpression), + option.map((f) => f.body), + option.filter(isCallExpression), + option.map((e) => e.callee) + ) + } + + const getBodyCalleeOrNode = (node: TSESTree.Expression) => { + return pipe( + node, + getBodyCallee, + option.getOrElse(constant(node)) + ) + } + const isOptionSomeValue = (node: TSESTree.Expression) => { return pipe( node, + getBodyCalleeOrNode, option.of, - option.map((n) => pipe( - n, - option.of, - option.filter(isArrowFunctionExpression), - option.map((f) => f.body), - option.filter(isCallExpression), - option.map((e) => e.callee), - option.getOrElse(constant(n)) - )), option.filter(isMemberExpression), option.filter(hasObjectIdentifierWithName("option")), option.filter(hasPropertyIdentifierWithName("some")), From 5a48836c9ed6bf75366d46f8f8305994bc0ad78b Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 12 Feb 2021 16:25:54 +0000 Subject: [PATCH 15/30] Improve scoping and use short returns --- src/rules/prefer-constructor.ts | 188 +++++++++++++++----------------- 1 file changed, 89 insertions(+), 99 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index b9e9335..a4788be 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -3,6 +3,8 @@ import { option, readonlyNonEmptyArray } from "fp-ts" import { constant, flow, pipe } from "fp-ts/function" import { calleeIdentifier, contextUtils, createRule } from "../utils" +type isFromFpTs = (identifier: TSESTree.Identifier) => boolean; + const hasName = (name: string) => (identifier: TSESTree.Identifier) => identifier.name === name const hasLength = (length: number) => (array: ReadonlyArray) => array.length === length @@ -63,6 +65,91 @@ const hasPropertyIdentifierWithName = (name: string) => (node: TSESTree.MemberEx isIdentifierWithName(name) ) +const isEitherFold = (node: TSESTree.CallExpression) => pipe( + node, + calleeIdentifier, + option.filter(hasName("fold")), + option.chain(getParent), + option.filter(isMemberExpression), + option.exists(hasObjectIdentifierWithName("either")) +) + +const isOptionNoneArrowFunctionExpression = (node: TSESTree.ArrowFunctionExpression) => pipe( + node.body, + option.of, + option.filter(isMemberExpression), + option.exists(isOptionNoneMemberExpression) +) + +const isOptionNoneMemberExpression = (node: TSESTree.MemberExpression) => pipe( + node, + option.of, + option.filter(hasObjectIdentifierWithName("option")), + option.exists(hasPropertyIdentifierWithName("none")) +) + +const isOptionNoneCallExpression = (isFromFpTs: isFromFpTs) => (node: TSESTree.CallExpression) => pipe( + node, + option.of, + option.filter(flow( + calleeIdentifier, + option.filter(hasName("constant")), + option.exists(isFromFpTs) + )), + option.chain(getFirstArgument), + option.filter(isMemberExpression), + option.exists(isOptionNoneMemberExpression) +) + +const isOptionNone = (isFromFpTs: isFromFpTs) => (node: TSESTree.Expression) => { + switch (node.type) { + case AST_NODE_TYPES.ArrowFunctionExpression: + return isOptionNoneArrowFunctionExpression(node) + case AST_NODE_TYPES.CallExpression: + return isOptionNoneCallExpression(isFromFpTs)(node) + case AST_NODE_TYPES.MemberExpression: + return isOptionNoneMemberExpression(node) + default: + return false + } +} + +const getBodyCallee = (node: TSESTree.Expression) => pipe( + node, + option.of, + option.filter(isArrowFunctionExpression), + option.map((f) => f.body), + option.filter(isCallExpression), + option.map((e) => e.callee) +) + +const getBodyCalleeOrNode = (node: TSESTree.Expression) => pipe( + node, + getBodyCallee, + option.getOrElse(constant(node)) +) + +const isOptionSomeValue = (node: TSESTree.Expression) => pipe( + node, + getBodyCalleeOrNode, + option.of, + option.filter(isMemberExpression), + option.filter(hasObjectIdentifierWithName("option")), + option.filter(hasPropertyIdentifierWithName("some")), + option.chain(getParent), + option.exists((parent) => ( + !isCallExpression(parent) + || !(parent.parent) + || !isArrowFunctionExpression(parent.parent) + || pipe( + option.Do, + option.bind("argument", () => pipe(parent, getFirstArgument, option.filter(isIdentifier))), + option.bind("param", () => pipe(parent, getParent, option.filter(isFunctionLike), option.chain(getFirstParam), option.filter(isIdentifier))), + option.exists(({ argument, param }) => hasName(argument.name)(param)) + ) + )) +) + export default createRule({ name: "prefer-constructor", meta: { @@ -82,113 +169,16 @@ export default createRule({ defaultOptions: [], create(context) { const { isIdentifierImportedFrom } = contextUtils(context) + const isFromFpTs = (identifier: TSESTree.Identifier) => isIdentifierImportedFrom(identifier, /fp-ts\//, context) return { CallExpression(node) { - const isEitherFold = (node: TSESTree.CallExpression) => { - return pipe( - node, - calleeIdentifier, - option.filter(hasName("fold")), - option.chain(getParent), - option.filter(isMemberExpression), - option.exists(hasObjectIdentifierWithName("either")) - ) - } - - const isOptionNoneArrowFunctionExpression = (node: TSESTree.ArrowFunctionExpression) => { - return pipe( - node.body, - option.of, - option.filter(isMemberExpression), - option.exists(isOptionNoneMemberExpression) - ) - } - - const isOptionNoneMemberExpression = (node: TSESTree.MemberExpression) => { - return pipe( - node, - option.of, - option.filter(hasObjectIdentifierWithName("option")), - option.exists(hasPropertyIdentifierWithName("none")) - ) - } - - const isOptionNoneCallExpression = (node: TSESTree.CallExpression) => { - return pipe( - node, - option.of, - option.filter(flow( - calleeIdentifier, - option.filter(hasName("constant")), - option.exists((callee) => isIdentifierImportedFrom(callee, /fp-ts\//, context)) - )), - option.chain(getFirstArgument), - option.filter(isMemberExpression), - option.exists(isOptionNoneMemberExpression) - ) - } - - const isOptionNone = (node: TSESTree.Expression) => { - switch (node.type) { - case AST_NODE_TYPES.ArrowFunctionExpression: - return isOptionNoneArrowFunctionExpression(node) - case AST_NODE_TYPES.CallExpression: - return isOptionNoneCallExpression(node) - case AST_NODE_TYPES.MemberExpression: - return isOptionNoneMemberExpression(node) - default: - return false - } - } - - const getBodyCallee = (node: TSESTree.Expression) => { - return pipe( - node, - option.of, - option.filter(isArrowFunctionExpression), - option.map((f) => f.body), - option.filter(isCallExpression), - option.map((e) => e.callee) - ) - } - - const getBodyCalleeOrNode = (node: TSESTree.Expression) => { - return pipe( - node, - getBodyCallee, - option.getOrElse(constant(node)) - ) - } - - const isOptionSomeValue = (node: TSESTree.Expression) => { - return pipe( - node, - getBodyCalleeOrNode, - option.of, - option.filter(isMemberExpression), - option.filter(hasObjectIdentifierWithName("option")), - option.filter(hasPropertyIdentifierWithName("some")), - option.chain(getParent), - option.exists((parent) => ( - !isCallExpression(parent) - || !(parent.parent) - || !isArrowFunctionExpression(parent.parent) - || pipe( - option.Do, - option.bind("argument", () => pipe(parent, getFirstArgument, option.filter(isIdentifier))), - option.bind("param", () => pipe(parent, getParent, option.filter(isFunctionLike), option.chain(getFirstParam), option.filter(isIdentifier))), - option.exists(({ argument, param }) => hasName(argument.name)(param)) - ) - )) - ) - } pipe( node, option.of, option.filter(isEitherFold), option.chain(getArguments), option.filter(hasLength(2)), - option.filter(flow(readonlyNonEmptyArray.head, isOptionNone)), + option.filter(flow(readonlyNonEmptyArray.head, isOptionNone(isFromFpTs))), option.filter(flow(readonlyNonEmptyArray.last, isOptionSomeValue)), option.map(() => { context.report({ From ff96c470d44e24a5178b7a15dbc8d5a92a101c92 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Sat, 13 Feb 2021 19:40:50 +0000 Subject: [PATCH 16/30] Use types to check Either --- src/rules/prefer-constructor.ts | 15 ++++---- src/utils.ts | 47 ++++++++++++++++++++++- tests/rules/prefer-constructor.test.ts | 53 ++++++++++++++++++++++++-- 3 files changed, 101 insertions(+), 14 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index a4788be..536d4e8 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -1,7 +1,7 @@ import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/experimental-utils" import { option, readonlyNonEmptyArray } from "fp-ts" import { constant, flow, pipe } from "fp-ts/function" -import { calleeIdentifier, contextUtils, createRule } from "../utils" +import { calleeIdentifier, ContextUtils, contextUtils, createRule, isFromModule } from "../utils" type isFromFpTs = (identifier: TSESTree.Identifier) => boolean; @@ -65,13 +65,12 @@ const hasPropertyIdentifierWithName = (name: string) => (node: TSESTree.MemberEx isIdentifierWithName(name) ) -const isEitherFold = (node: TSESTree.CallExpression) => pipe( +const isEitherFold = ({ typeOfNode }: ContextUtils) => (node: TSESTree.CallExpression) => pipe( node, calleeIdentifier, option.filter(hasName("fold")), - option.chain(getParent), - option.filter(isMemberExpression), - option.exists(hasObjectIdentifierWithName("either")) + option.chain(typeOfNode), + option.exists(isFromModule("Either")) ) const isOptionNoneArrowFunctionExpression = (node: TSESTree.ArrowFunctionExpression) => pipe( @@ -168,14 +167,14 @@ export default createRule({ }, defaultOptions: [], create(context) { - const { isIdentifierImportedFrom } = contextUtils(context) - const isFromFpTs = (identifier: TSESTree.Identifier) => isIdentifierImportedFrom(identifier, /fp-ts\//, context) + const utils = contextUtils(context) + const isFromFpTs = (identifier: TSESTree.Identifier) => utils.isIdentifierImportedFrom(identifier, /fp-ts\//, context) return { CallExpression(node) { pipe( node, option.of, - option.filter(isEitherFold), + option.filter(isEitherFold(utils)), option.chain(getArguments), option.filter(hasLength(2)), option.filter(flow(readonlyNonEmptyArray.head, isOptionNone(isFromFpTs))), diff --git a/src/utils.ts b/src/utils.ts index e949ca0..a069cb0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,8 +8,8 @@ import { } from "@typescript-eslint/experimental-utils"; import * as recast from "recast"; import { visitorKeys as tsVisitorKeys } from "@typescript-eslint/typescript-estree"; -import { array, option, apply } from "fp-ts"; -import { pipe } from "fp-ts/function"; +import { array, option, apply, readonlyNonEmptyArray } from "fp-ts" +import { flow, pipe } from "fp-ts/function" import estraverse from "estraverse"; import { @@ -32,6 +32,14 @@ declare module "typescript" { function toFileNameLowerCase(x: string): string; } +const modules = ["Either", "Option"] as const + +type Module = typeof modules[number] + +const is = (original: T) => (value: unknown): value is T => original === value + +const isModule = (name: string): name is Module => modules.includes(name as Module) + const version = require("../package.json").version; export const createRule = ESLintUtils.RuleCreator( @@ -189,6 +197,41 @@ export function inferQuote(node: TSESTree.Literal): Quote { return node.raw[0] === "'" ? "'" : '"'; } +const getDeclarationFileName = (declaration: ts.Declaration) => declaration.getSourceFile().fileName + +const getDeclarations = flow( + (symbol: ts.Symbol) => symbol.getDeclarations(), + option.fromNullable, + option.chain(readonlyNonEmptyArray.fromArray) +) + +const getFileName = flow( + (type: ts.Type) => type.symbol, + option.fromNullable, + option.chain(getDeclarations), + option.map(readonlyNonEmptyArray.head), + option.map(getDeclarationFileName) +) + +const getFpTsModule = flow( + (fileName: string) => /\/fp-ts\/lib\/(.+?)\.d\.ts$/.exec(fileName), + option.fromNullable, + option.chain(array.lookup(1)), + option.filter(isModule) +) + +const getModule = flow( + getFileName, + option.chain(getFpTsModule) +) + +export const isFromModule = (module: Module) => flow( + getModule, + option.exists(is(module)) +) + +export type ContextUtils = ReturnType + export const contextUtils = < TMessageIds extends string, TOptions extends readonly unknown[] diff --git a/tests/rules/prefer-constructor.test.ts b/tests/rules/prefer-constructor.test.ts index 8785821..46fa643 100644 --- a/tests/rules/prefer-constructor.test.ts +++ b/tests/rules/prefer-constructor.test.ts @@ -1,10 +1,23 @@ -import { stripIndent } from 'common-tags'; -import rule from "../../src/rules/prefer-constructor"; -import { ESLintUtils } from "@typescript-eslint/experimental-utils"; +import { ESLintUtils } from "@typescript-eslint/experimental-utils" +import { stripIndent } from "common-tags" +import path from "path" +import rule from "../../src/rules/prefer-constructor" + +const fixtureProjectPath = path.join( + __dirname, + "..", + "fixtures", + "fp-ts-project" +) const ruleTester = new ESLintUtils.RuleTester({ parser: "@typescript-eslint/parser", -}); + parserOptions: { + sourceType: "module", + tsconfigRootDir: fixtureProjectPath, + project: "./tsconfig.json" + } +}) ruleTester.run("prefer-constructor", rule, { valid: [ @@ -73,6 +86,38 @@ ruleTester.run("prefer-constructor", rule, { }, ], }, + { + code: stripIndent` + import * as E from "fp-ts/Either" + import option from "fp-ts" + import { pipe } from "fp-ts/function" + + pipe( + E.of(1), + E.fold(() => option.none, (value) => option.some(value)) + ) + `, + errors: [ + { + messageId: "eitherFoldIsOptionFromEither", + suggestions: [ + { + messageId: "replaceEitherFoldWithOptionFromEither", + output: stripIndent` + import * as E from "fp-ts/Either" + import option from "fp-ts" + import { pipe } from "fp-ts/function" + + pipe( + E.of(1), + option.fromEither + ) + ` + } + ] + } + ] + }, { code: stripIndent` import { either, option } from "fp-ts" From 3d5a844e3134c713c67918bfc5855d91f9c710a1 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Mon, 15 Feb 2021 13:27:51 +0000 Subject: [PATCH 17/30] Safely determine Option and function modules --- src/rules/prefer-constructor.ts | 49 ++++++++++++++------------ src/utils.ts | 4 +-- tests/rules/prefer-constructor.test.ts | 6 ++-- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index 536d4e8..96f0a8c 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -1,9 +1,7 @@ import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/experimental-utils" import { option, readonlyNonEmptyArray } from "fp-ts" import { constant, flow, pipe } from "fp-ts/function" -import { calleeIdentifier, ContextUtils, contextUtils, createRule, isFromModule } from "../utils" - -type isFromFpTs = (identifier: TSESTree.Identifier) => boolean; +import { calleeIdentifier, ContextUtils, contextUtils, createRule, isFromModule, Module } from "../utils" const hasName = (name: string) => (identifier: TSESTree.Identifier) => identifier.name === name @@ -55,9 +53,10 @@ const isIdentifierWithName = (name: string) => (node: TSESTree.Node) => pipe( option.exists(hasName(name)) ) -const hasObjectIdentifierWithName = (name: string) => (node: TSESTree.MemberExpression) => pipe( +const hasModuleObjectIdentifier = ({ typeOfNode }: ContextUtils) => (name: Module) => (node: TSESTree.MemberExpression) => pipe( node.object, - isIdentifierWithName(name) + typeOfNode, + option.exists(isFromModule(name)) ) const hasPropertyIdentifierWithName = (name: string) => (node: TSESTree.MemberExpression) => pipe( @@ -68,46 +67,47 @@ const hasPropertyIdentifierWithName = (name: string) => (node: TSESTree.MemberEx const isEitherFold = ({ typeOfNode }: ContextUtils) => (node: TSESTree.CallExpression) => pipe( node, calleeIdentifier, - option.filter(hasName("fold")), + option.filter((node) => hasName("fold")(node)), option.chain(typeOfNode), - option.exists(isFromModule("Either")) + option.exists((node) => isFromModule("Either")(node)) ) -const isOptionNoneArrowFunctionExpression = (node: TSESTree.ArrowFunctionExpression) => pipe( +const isOptionNoneArrowFunctionExpression = (utils: ContextUtils) => (node: TSESTree.ArrowFunctionExpression) => pipe( node.body, option.of, option.filter(isMemberExpression), - option.exists(isOptionNoneMemberExpression) + option.exists(isOptionNoneMemberExpression(utils)) ) -const isOptionNoneMemberExpression = (node: TSESTree.MemberExpression) => pipe( +const isOptionNoneMemberExpression = (utils: ContextUtils) => (node: TSESTree.MemberExpression) => pipe( node, option.of, - option.filter(hasObjectIdentifierWithName("option")), + option.filter(hasModuleObjectIdentifier(utils)("Option")), option.exists(hasPropertyIdentifierWithName("none")) ) -const isOptionNoneCallExpression = (isFromFpTs: isFromFpTs) => (node: TSESTree.CallExpression) => pipe( +const isOptionNoneCallExpression = (utils: ContextUtils) => (node: TSESTree.CallExpression) => pipe( node, option.of, option.filter(flow( calleeIdentifier, option.filter(hasName("constant")), - option.exists(isFromFpTs) + option.chain(utils.typeOfNode), + option.exists(isFromModule("function")) )), option.chain(getFirstArgument), option.filter(isMemberExpression), - option.exists(isOptionNoneMemberExpression) + option.exists(isOptionNoneMemberExpression(utils)) ) -const isOptionNone = (isFromFpTs: isFromFpTs) => (node: TSESTree.Expression) => { +const isOptionNone = (utils: ContextUtils) => (node: TSESTree.Expression) => { switch (node.type) { case AST_NODE_TYPES.ArrowFunctionExpression: - return isOptionNoneArrowFunctionExpression(node) + return isOptionNoneArrowFunctionExpression(utils)(node) case AST_NODE_TYPES.CallExpression: - return isOptionNoneCallExpression(isFromFpTs)(node) + return isOptionNoneCallExpression(utils)(node) case AST_NODE_TYPES.MemberExpression: - return isOptionNoneMemberExpression(node) + return isOptionNoneMemberExpression(utils)(node) default: return false } @@ -128,13 +128,17 @@ const getBodyCalleeOrNode = (node: TSESTree.Expression) => pipe( option.getOrElse(constant(node)) ) -const isOptionSomeValue = (node: TSESTree.Expression) => pipe( +const isOptionSomeValue = ({ typeOfNode }: ContextUtils) => (node: TSESTree.Expression) => pipe( node, getBodyCalleeOrNode, option.of, option.filter(isMemberExpression), - option.filter(hasObjectIdentifierWithName("option")), option.filter(hasPropertyIdentifierWithName("some")), + option.filter(flow( + (node) => node.object, + typeOfNode, + option.exists(isFromModule("Option")) + )), option.chain(getParent), option.exists((parent) => ( !isCallExpression(parent) @@ -168,7 +172,6 @@ export default createRule({ defaultOptions: [], create(context) { const utils = contextUtils(context) - const isFromFpTs = (identifier: TSESTree.Identifier) => utils.isIdentifierImportedFrom(identifier, /fp-ts\//, context) return { CallExpression(node) { pipe( @@ -177,8 +180,8 @@ export default createRule({ option.filter(isEitherFold(utils)), option.chain(getArguments), option.filter(hasLength(2)), - option.filter(flow(readonlyNonEmptyArray.head, isOptionNone(isFromFpTs))), - option.filter(flow(readonlyNonEmptyArray.last, isOptionSomeValue)), + option.filter(flow(readonlyNonEmptyArray.head, isOptionNone(utils))), + option.filter(flow(readonlyNonEmptyArray.last, isOptionSomeValue(utils))), option.map(() => { context.report({ loc: { diff --git a/src/utils.ts b/src/utils.ts index a069cb0..f991f58 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -32,9 +32,9 @@ declare module "typescript" { function toFileNameLowerCase(x: string): string; } -const modules = ["Either", "Option"] as const +const modules = ["Either", "Option", "function"] as const -type Module = typeof modules[number] +export type Module = typeof modules[number] const is = (original: T) => (value: unknown): value is T => original === value diff --git a/tests/rules/prefer-constructor.test.ts b/tests/rules/prefer-constructor.test.ts index 46fa643..0f968d9 100644 --- a/tests/rules/prefer-constructor.test.ts +++ b/tests/rules/prefer-constructor.test.ts @@ -89,12 +89,12 @@ ruleTester.run("prefer-constructor", rule, { { code: stripIndent` import * as E from "fp-ts/Either" - import option from "fp-ts" + import * as O from "fp-ts/Option" import { pipe } from "fp-ts/function" pipe( E.of(1), - E.fold(() => option.none, (value) => option.some(value)) + E.fold(() => O.none, (value) => O.some(value)) ) `, errors: [ @@ -105,7 +105,7 @@ ruleTester.run("prefer-constructor", rule, { messageId: "replaceEitherFoldWithOptionFromEither", output: stripIndent` import * as E from "fp-ts/Either" - import option from "fp-ts" + import * as O from "fp-ts/Option" import { pipe } from "fp-ts/function" pipe( From 2847883b811a71539b452d00e822d76a3499b5dd Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Mon, 15 Feb 2021 15:33:41 +0000 Subject: [PATCH 18/30] Separate finding the member expression from whether it's option.none --- src/rules/prefer-constructor.ts | 35 +++++++++++++++++---------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index 96f0a8c..5fe09ae 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -72,21 +72,20 @@ const isEitherFold = ({ typeOfNode }: ContextUtils) => (node: TSESTree.CallExpre option.exists((node) => isFromModule("Either")(node)) ) -const isOptionNoneArrowFunctionExpression = (utils: ContextUtils) => (node: TSESTree.ArrowFunctionExpression) => pipe( - node.body, - option.of, - option.filter(isMemberExpression), - option.exists(isOptionNoneMemberExpression(utils)) -) - -const isOptionNoneMemberExpression = (utils: ContextUtils) => (node: TSESTree.MemberExpression) => pipe( +const isOptionNone = (utils: ContextUtils) => (node: TSESTree.MemberExpression) => pipe( node, option.of, option.filter(hasModuleObjectIdentifier(utils)("Option")), option.exists(hasPropertyIdentifierWithName("none")) ) -const isOptionNoneCallExpression = (utils: ContextUtils) => (node: TSESTree.CallExpression) => pipe( +const findMemberExpressionFromArrowFunctionExpression = (node: TSESTree.ArrowFunctionExpression) => pipe( + node.body, + option.of, + option.filter(isMemberExpression), +); + +const findMemberExpressionFromCallExpression = (utils: ContextUtils) => (node: TSESTree.CallExpression) => pipe( node, option.of, option.filter(flow( @@ -97,22 +96,24 @@ const isOptionNoneCallExpression = (utils: ContextUtils) => (node: TSESTree.Call )), option.chain(getFirstArgument), option.filter(isMemberExpression), - option.exists(isOptionNoneMemberExpression(utils)) ) -const isOptionNone = (utils: ContextUtils) => (node: TSESTree.Expression) => { +const findMemberExpression = (utils: ContextUtils) => (node: TSESTree.Expression) => { switch (node.type) { case AST_NODE_TYPES.ArrowFunctionExpression: - return isOptionNoneArrowFunctionExpression(utils)(node) + return findMemberExpressionFromArrowFunctionExpression(node) case AST_NODE_TYPES.CallExpression: - return isOptionNoneCallExpression(utils)(node) - case AST_NODE_TYPES.MemberExpression: - return isOptionNoneMemberExpression(utils)(node) + return findMemberExpressionFromCallExpression(utils)(node) default: - return false + return option.none } } +const isCallToOptionNone = (utils: ContextUtils) => flow( + findMemberExpression(utils), + option.exists(isOptionNone(utils)) +) + const getBodyCallee = (node: TSESTree.Expression) => pipe( node, option.of, @@ -180,7 +181,7 @@ export default createRule({ option.filter(isEitherFold(utils)), option.chain(getArguments), option.filter(hasLength(2)), - option.filter(flow(readonlyNonEmptyArray.head, isOptionNone(utils))), + option.filter(flow(readonlyNonEmptyArray.head, isCallToOptionNone(utils))), option.filter(flow(readonlyNonEmptyArray.last, isOptionSomeValue(utils))), option.map(() => { context.report({ From d30b8a49673537089c88374c9be9f7906011e516 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Mon, 15 Feb 2021 15:36:34 +0000 Subject: [PATCH 19/30] Set the right namespace --- src/rules/prefer-constructor.ts | 18 +++++++++++++----- src/utils.ts | 1 + tests/rules/prefer-constructor.test.ts | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index 5fe09ae..004b53d 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -82,8 +82,8 @@ const isOptionNone = (utils: ContextUtils) => (node: TSESTree.MemberExpression) const findMemberExpressionFromArrowFunctionExpression = (node: TSESTree.ArrowFunctionExpression) => pipe( node.body, option.of, - option.filter(isMemberExpression), -); + option.filter(isMemberExpression) +) const findMemberExpressionFromCallExpression = (utils: ContextUtils) => (node: TSESTree.CallExpression) => pipe( node, @@ -95,7 +95,7 @@ const findMemberExpressionFromCallExpression = (utils: ContextUtils) => (node: T option.exists(isFromModule("function")) )), option.chain(getFirstArgument), - option.filter(isMemberExpression), + option.filter(isMemberExpression) ) const findMemberExpression = (utils: ContextUtils) => (node: TSESTree.Expression) => { @@ -154,6 +154,13 @@ const isOptionSomeValue = ({ typeOfNode }: ContextUtils) => (node: TSESTree.Expr )) ) +const findNamespace = (utils: ContextUtils) => flow( + findMemberExpression(utils), + option.map((node) => node.object), + option.filter(isIdentifier), + option.map((identifier) => identifier.name) +) + export default createRule({ name: "prefer-constructor", meta: { @@ -183,7 +190,8 @@ export default createRule({ option.filter(hasLength(2)), option.filter(flow(readonlyNonEmptyArray.head, isCallToOptionNone(utils))), option.filter(flow(readonlyNonEmptyArray.last, isOptionSomeValue(utils))), - option.map(() => { + option.bind("namespace", flow(readonlyNonEmptyArray.head, findNamespace(utils))), + option.map(({ namespace }) => { context.report({ loc: { start: node.loc.start, @@ -197,7 +205,7 @@ export default createRule({ return [ fixer.replaceTextRange( node.range, - `option.fromEither` + `${namespace}.fromEither` ) ] } diff --git a/src/utils.ts b/src/utils.ts index f991f58..f5fdc14 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -503,6 +503,7 @@ export const contextUtils = < } return { + findModuleImport, addNamedImportIfNeeded, removeImportDeclaration, isFlowExpression, diff --git a/tests/rules/prefer-constructor.test.ts b/tests/rules/prefer-constructor.test.ts index 0f968d9..57d2fef 100644 --- a/tests/rules/prefer-constructor.test.ts +++ b/tests/rules/prefer-constructor.test.ts @@ -110,7 +110,7 @@ ruleTester.run("prefer-constructor", rule, { pipe( E.of(1), - option.fromEither + O.fromEither ) ` } From 93332ee902bdbcd5cacf3580a6503d96d27d45cc Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Tue, 16 Feb 2021 10:53:24 +0000 Subject: [PATCH 20/30] Extract isCall and isValue --- src/rules/prefer-constructor.ts | 23 +++++++++-------------- src/utils.ts | 2 +- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index 004b53d..20f61a2 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -53,30 +53,25 @@ const isIdentifierWithName = (name: string) => (node: TSESTree.Node) => pipe( option.exists(hasName(name)) ) -const hasModuleObjectIdentifier = ({ typeOfNode }: ContextUtils) => (name: Module) => (node: TSESTree.MemberExpression) => pipe( - node.object, - typeOfNode, - option.exists(isFromModule(name)) -) - const hasPropertyIdentifierWithName = (name: string) => (node: TSESTree.MemberExpression) => pipe( node.property, isIdentifierWithName(name) ) -const isEitherFold = ({ typeOfNode }: ContextUtils) => (node: TSESTree.CallExpression) => pipe( +const isCall = ({ typeOfNode }: ContextUtils, module: Module, name: string) => (node: TSESTree.CallExpression) => pipe( node, calleeIdentifier, - option.filter((node) => hasName("fold")(node)), + option.filter(hasName(name)), option.chain(typeOfNode), - option.exists((node) => isFromModule("Either")(node)) + option.exists(isFromModule(module)) ) -const isOptionNone = (utils: ContextUtils) => (node: TSESTree.MemberExpression) => pipe( +const isValue = ({ typeOfNode }: ContextUtils, module: Module, name: string) => (node: TSESTree.MemberExpression) => pipe( node, option.of, - option.filter(hasModuleObjectIdentifier(utils)("Option")), - option.exists(hasPropertyIdentifierWithName("none")) + option.filter(hasPropertyIdentifierWithName(name)), + option.chain(typeOfNode), + option.exists(isFromModule(module)) ) const findMemberExpressionFromArrowFunctionExpression = (node: TSESTree.ArrowFunctionExpression) => pipe( @@ -111,7 +106,7 @@ const findMemberExpression = (utils: ContextUtils) => (node: TSESTree.Expression const isCallToOptionNone = (utils: ContextUtils) => flow( findMemberExpression(utils), - option.exists(isOptionNone(utils)) + option.exists(isValue(utils, "Option", "none")) ) const getBodyCallee = (node: TSESTree.Expression) => pipe( @@ -185,7 +180,7 @@ export default createRule({ pipe( node, option.of, - option.filter(isEitherFold(utils)), + option.filter(isCall(utils, "Either", "fold")), option.chain(getArguments), option.filter(hasLength(2)), option.filter(flow(readonlyNonEmptyArray.head, isCallToOptionNone(utils))), diff --git a/src/utils.ts b/src/utils.ts index f5fdc14..6d9125c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -206,7 +206,7 @@ const getDeclarations = flow( ) const getFileName = flow( - (type: ts.Type) => type.symbol, + (type: ts.Type) => type.aliasSymbol ?? type.symbol, option.fromNullable, option.chain(getDeclarations), option.map(readonlyNonEmptyArray.head), From fa53426116fbbb7c4e406e43ffeeba42f844e56f Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Tue, 16 Feb 2021 11:18:52 +0000 Subject: [PATCH 21/30] Extract isLazyValue and isConstantCall --- src/rules/prefer-constructor.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index 20f61a2..91b1eda 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -66,6 +66,11 @@ const isCall = ({ typeOfNode }: ContextUtils, module: Module, name: string) => ( option.exists(isFromModule(module)) ) +const isLazyValue = (utils: ContextUtils, module: Module, name: string) => flow( + findMemberExpression(utils), + option.exists(isValue(utils, module, name)) +) + const isValue = ({ typeOfNode }: ContextUtils, module: Module, name: string) => (node: TSESTree.MemberExpression) => pipe( node, option.of, @@ -80,15 +85,18 @@ const findMemberExpressionFromArrowFunctionExpression = (node: TSESTree.ArrowFun option.filter(isMemberExpression) ) +const isConstantCall = ({ typeOfNode }: ContextUtils) => (node: TSESTree.CallExpression) => pipe( + node, + calleeIdentifier, + option.filter(hasName("constant")), + option.chain(typeOfNode), + option.exists(isFromModule("function")) +) + const findMemberExpressionFromCallExpression = (utils: ContextUtils) => (node: TSESTree.CallExpression) => pipe( node, option.of, - option.filter(flow( - calleeIdentifier, - option.filter(hasName("constant")), - option.chain(utils.typeOfNode), - option.exists(isFromModule("function")) - )), + option.filter(isConstantCall(utils)), option.chain(getFirstArgument), option.filter(isMemberExpression) ) @@ -104,11 +112,6 @@ const findMemberExpression = (utils: ContextUtils) => (node: TSESTree.Expression } } -const isCallToOptionNone = (utils: ContextUtils) => flow( - findMemberExpression(utils), - option.exists(isValue(utils, "Option", "none")) -) - const getBodyCallee = (node: TSESTree.Expression) => pipe( node, option.of, @@ -183,7 +186,7 @@ export default createRule({ option.filter(isCall(utils, "Either", "fold")), option.chain(getArguments), option.filter(hasLength(2)), - option.filter(flow(readonlyNonEmptyArray.head, isCallToOptionNone(utils))), + option.filter(flow(readonlyNonEmptyArray.head, isLazyValue(utils, "Option", "none"))), option.filter(flow(readonlyNonEmptyArray.last, isOptionSomeValue(utils))), option.bind("namespace", flow(readonlyNonEmptyArray.head, findNamespace(utils))), option.map(({ namespace }) => { From 8683809b335ff9ff466273e9a13383d71775420d Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Tue, 16 Feb 2021 11:41:20 +0000 Subject: [PATCH 22/30] Use isCall everywhere --- src/rules/prefer-constructor.ts | 13 ++++--------- src/utils.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index 91b1eda..cb7ec87 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -1,7 +1,7 @@ import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/experimental-utils" import { option, readonlyNonEmptyArray } from "fp-ts" import { constant, flow, pipe } from "fp-ts/function" -import { calleeIdentifier, ContextUtils, contextUtils, createRule, isFromModule, Module } from "../utils" +import { Callee, calleeIdentifier, ContextUtils, contextUtils, createRule, isFromModule, Module } from "../utils" const hasName = (name: string) => (identifier: TSESTree.Identifier) => identifier.name === name @@ -58,7 +58,7 @@ const hasPropertyIdentifierWithName = (name: string) => (node: TSESTree.MemberEx isIdentifierWithName(name) ) -const isCall = ({ typeOfNode }: ContextUtils, module: Module, name: string) => (node: TSESTree.CallExpression) => pipe( +const isCall = ({ typeOfNode }: ContextUtils, module: Module, name: string) => (node: T) => pipe( node, calleeIdentifier, option.filter(hasName(name)), @@ -127,17 +127,12 @@ const getBodyCalleeOrNode = (node: TSESTree.Expression) => pipe( option.getOrElse(constant(node)) ) -const isOptionSomeValue = ({ typeOfNode }: ContextUtils) => (node: TSESTree.Expression) => pipe( +const isOptionSomeValue = (utils: ContextUtils) => (node: TSESTree.Expression) => pipe( node, getBodyCalleeOrNode, option.of, option.filter(isMemberExpression), - option.filter(hasPropertyIdentifierWithName("some")), - option.filter(flow( - (node) => node.object, - typeOfNode, - option.exists(isFromModule("Option")) - )), + option.filter(isCall(utils, "Option", "some")), option.chain(getParent), option.exists((parent) => ( !isCallExpression(parent) diff --git a/src/utils.ts b/src/utils.ts index 6d9125c..6ebf1d0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -47,12 +47,12 @@ export const createRule = ESLintUtils.RuleCreator( `https://github.com/buildo/eslint-plugin-fp-ts/blob/v${version}/docs/rules/${name}.md` ); -export function calleeIdentifier( - node: - | TSESTree.CallExpression - | TSESTree.MemberExpression - | TSESTree.Identifier -): option.Option { +export type Callee = + | TSESTree.CallExpression + | TSESTree.MemberExpression + | TSESTree.Identifier + +export function calleeIdentifier(node: Callee): option.Option { switch (node.type) { case AST_NODE_TYPES.MemberExpression: if (node.property.type === AST_NODE_TYPES.Identifier) { From 121bd6f4c5d1d7adee1183144a49f512b043a6d0 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Tue, 16 Feb 2021 12:09:23 +0000 Subject: [PATCH 23/30] Rewrite isOptionSomeValue --- src/rules/prefer-constructor.ts | 74 +++++++++++++-------------------- 1 file changed, 28 insertions(+), 46 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index cb7ec87..e1f2b4a 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -1,29 +1,18 @@ import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/experimental-utils" import { option, readonlyNonEmptyArray } from "fp-ts" -import { constant, flow, pipe } from "fp-ts/function" +import { flow, pipe } from "fp-ts/function" import { Callee, calleeIdentifier, ContextUtils, contextUtils, createRule, isFromModule, Module } from "../utils" const hasName = (name: string) => (identifier: TSESTree.Identifier) => identifier.name === name const hasLength = (length: number) => (array: ReadonlyArray) => array.length === length -const isArrowFunctionExpression = (node: TSESTree.Node): node is TSESTree.ArrowFunctionExpression => node.type === AST_NODE_TYPES.ArrowFunctionExpression - -const isFunctionExpression = (node: TSESTree.Node): node is TSESTree.FunctionExpression => node.type === AST_NODE_TYPES.FunctionExpression - -const isFunctionLike = (node: TSESTree.Node): node is TSESTree.FunctionLike => isArrowFunctionExpression(node) || isFunctionExpression(node) - const isCallExpression = (node: TSESTree.Node): node is TSESTree.CallExpression => node.type === AST_NODE_TYPES.CallExpression const isMemberExpression = (node: TSESTree.Node): node is TSESTree.MemberExpression => node.type === AST_NODE_TYPES.MemberExpression const isIdentifier = (node: TSESTree.Node): node is TSESTree.Identifier => node.type === AST_NODE_TYPES.Identifier -const getParent = (identifier: TSESTree.BaseNode) => pipe( - identifier.parent, - option.fromNullable -) - const getArguments = (call: TSESTree.CallExpression) => pipe( call.arguments, readonlyNonEmptyArray.fromArray @@ -112,40 +101,33 @@ const findMemberExpression = (utils: ContextUtils) => (node: TSESTree.Expression } } -const getBodyCallee = (node: TSESTree.Expression) => pipe( - node, - option.of, - option.filter(isArrowFunctionExpression), - option.map((f) => f.body), - option.filter(isCallExpression), - option.map((e) => e.callee) -) - -const getBodyCalleeOrNode = (node: TSESTree.Expression) => pipe( - node, - getBodyCallee, - option.getOrElse(constant(node)) -) - -const isOptionSomeValue = (utils: ContextUtils) => (node: TSESTree.Expression) => pipe( - node, - getBodyCalleeOrNode, - option.of, - option.filter(isMemberExpression), - option.filter(isCall(utils, "Option", "some")), - option.chain(getParent), - option.exists((parent) => ( - !isCallExpression(parent) - || !(parent.parent) - || !isArrowFunctionExpression(parent.parent) - || pipe( - option.Do, - option.bind("argument", () => pipe(parent, getFirstArgument, option.filter(isIdentifier))), - option.bind("param", () => pipe(parent, getParent, option.filter(isFunctionLike), option.chain(getFirstParam), option.filter(isIdentifier))), - option.exists(({ argument, param }) => hasName(argument.name)(param)) - ) - )) -) +const getWrappedCall = (node: TSESTree.ArrowFunctionExpression) => pipe( + option.Do, + option.bind("wrappedCall", () => pipe( + node.body, + option.of, + option.filter(isCallExpression) + )), + option.bind("param", () => pipe(node, getFirstParam, option.filter(isIdentifier))), + option.bind("argument", ({ wrappedCall }) => pipe( + wrappedCall, + getFirstArgument, + option.filter(isIdentifier) + )), + option.filter(({ argument, param }) => hasName(argument.name)(param)), + option.map(({ wrappedCall }) => wrappedCall) +) + +const isOptionSomeValue = (utils: ContextUtils) => (node: TSESTree.Expression) => { + switch (node.type) { + case AST_NODE_TYPES.ArrowFunctionExpression: + return pipe(node, getWrappedCall, option.exists(isCall(utils, "Option", "some"))) + case AST_NODE_TYPES.MemberExpression: + return pipe(node, isCall(utils, "Option", "some")) + default: + return false + } +} const findNamespace = (utils: ContextUtils) => flow( findMemberExpression(utils), From e81e8e34ba180003bdcef0422a170f518cf34f5b Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Tue, 16 Feb 2021 16:08:38 +0000 Subject: [PATCH 24/30] Really use isCall everywhere --- src/rules/prefer-constructor.ts | 63 ++++++++++++++++----------------- src/utils.ts | 2 ++ 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index e1f2b4a..a5afa2f 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -1,12 +1,14 @@ import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/experimental-utils" import { option, readonlyNonEmptyArray } from "fp-ts" -import { flow, pipe } from "fp-ts/function" -import { Callee, calleeIdentifier, ContextUtils, contextUtils, createRule, isFromModule, Module } from "../utils" +import { constant, flow, pipe } from "fp-ts/function" +import { calleeIdentifier, ContextUtils, contextUtils, createRule, isCallee, isFromModule, Module } from "../utils" const hasName = (name: string) => (identifier: TSESTree.Identifier) => identifier.name === name const hasLength = (length: number) => (array: ReadonlyArray) => array.length === length +const isArrowFunctionExpression = (node: TSESTree.Node): node is TSESTree.ArrowFunctionExpression => node.type === AST_NODE_TYPES.ArrowFunctionExpression + const isCallExpression = (node: TSESTree.Node): node is TSESTree.CallExpression => node.type === AST_NODE_TYPES.CallExpression const isMemberExpression = (node: TSESTree.Node): node is TSESTree.MemberExpression => node.type === AST_NODE_TYPES.MemberExpression @@ -47,8 +49,33 @@ const hasPropertyIdentifierWithName = (name: string) => (node: TSESTree.MemberEx isIdentifierWithName(name) ) -const isCall = ({ typeOfNode }: ContextUtils, module: Module, name: string) => (node: T) => pipe( +const getWrappedCall = (node: TSESTree.ArrowFunctionExpression) => pipe( + option.Do, + option.bind("wrappedCall", () => pipe( + node.body, + option.of, + option.filter(isCallExpression) + )), + option.bind("param", () => pipe(node, getFirstParam, option.filter(isIdentifier))), + option.bind("argument", ({ wrappedCall }) => pipe( + wrappedCall, + getFirstArgument, + option.filter(isIdentifier) + )), + option.filter(({ argument, param }) => hasName(argument.name)(param)), + option.map(({ wrappedCall }) => wrappedCall) +) + +const isCall = (utils: ContextUtils, module: Module, name: string) => (node: T) => pipe( node, + option.fromPredicate(isArrowFunctionExpression), + option.chain(getWrappedCall), + option.altW(constant(option.some(node))), + option.filter(isCallee), + option.exists(isCallTo(utils, module, name)) +) + +const isCallTo = ({ typeOfNode }: ContextUtils, module: Module, name: string) => flow( calleeIdentifier, option.filter(hasName(name)), option.chain(typeOfNode), @@ -101,34 +128,6 @@ const findMemberExpression = (utils: ContextUtils) => (node: TSESTree.Expression } } -const getWrappedCall = (node: TSESTree.ArrowFunctionExpression) => pipe( - option.Do, - option.bind("wrappedCall", () => pipe( - node.body, - option.of, - option.filter(isCallExpression) - )), - option.bind("param", () => pipe(node, getFirstParam, option.filter(isIdentifier))), - option.bind("argument", ({ wrappedCall }) => pipe( - wrappedCall, - getFirstArgument, - option.filter(isIdentifier) - )), - option.filter(({ argument, param }) => hasName(argument.name)(param)), - option.map(({ wrappedCall }) => wrappedCall) -) - -const isOptionSomeValue = (utils: ContextUtils) => (node: TSESTree.Expression) => { - switch (node.type) { - case AST_NODE_TYPES.ArrowFunctionExpression: - return pipe(node, getWrappedCall, option.exists(isCall(utils, "Option", "some"))) - case AST_NODE_TYPES.MemberExpression: - return pipe(node, isCall(utils, "Option", "some")) - default: - return false - } -} - const findNamespace = (utils: ContextUtils) => flow( findMemberExpression(utils), option.map((node) => node.object), @@ -164,7 +163,7 @@ export default createRule({ option.chain(getArguments), option.filter(hasLength(2)), option.filter(flow(readonlyNonEmptyArray.head, isLazyValue(utils, "Option", "none"))), - option.filter(flow(readonlyNonEmptyArray.last, isOptionSomeValue(utils))), + option.filter(flow(readonlyNonEmptyArray.last, isCall(utils, "Option", "some"))), option.bind("namespace", flow(readonlyNonEmptyArray.head, findNamespace(utils))), option.map(({ namespace }) => { context.report({ diff --git a/src/utils.ts b/src/utils.ts index 6ebf1d0..ebd7f6a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -52,6 +52,8 @@ export type Callee = | TSESTree.MemberExpression | TSESTree.Identifier +export const isCallee = (node: TSESTree.Node): node is Callee => isWithinTypes(node, [AST_NODE_TYPES.CallExpression, AST_NODE_TYPES.MemberExpression, AST_NODE_TYPES.Identifier]) + export function calleeIdentifier(node: Callee): option.Option { switch (node.type) { case AST_NODE_TYPES.MemberExpression: From c22df2898883d04981c7df5e700fa2835a8351de Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Tue, 16 Feb 2021 20:06:53 +0000 Subject: [PATCH 25/30] Simplify ensuring arguments --- src/rules/prefer-constructor.ts | 46 ++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index a5afa2f..7277f9a 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -1,5 +1,5 @@ import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/experimental-utils" -import { option, readonlyNonEmptyArray } from "fp-ts" +import { option, readonlyArray, readonlyNonEmptyArray } from "fp-ts" import { constant, flow, pipe } from "fp-ts/function" import { calleeIdentifier, ContextUtils, contextUtils, createRule, isCallee, isFromModule, Module } from "../utils" @@ -50,20 +50,15 @@ const hasPropertyIdentifierWithName = (name: string) => (node: TSESTree.MemberEx ) const getWrappedCall = (node: TSESTree.ArrowFunctionExpression) => pipe( - option.Do, - option.bind("wrappedCall", () => pipe( - node.body, + node.body, + option.of, + option.filter(isCallExpression), + option.filter(flow( option.of, - option.filter(isCallExpression) - )), - option.bind("param", () => pipe(node, getFirstParam, option.filter(isIdentifier))), - option.bind("argument", ({ wrappedCall }) => pipe( - wrappedCall, - getFirstArgument, - option.filter(isIdentifier) - )), - option.filter(({ argument, param }) => hasName(argument.name)(param)), - option.map(({ wrappedCall }) => wrappedCall) + option.bindTo("call"), + option.bind("param", () => pipe(node, getFirstParam, option.filter(isIdentifier))), + option.exists(({ call, param }) => pipe(call, hasArguments([isIdentifierWithName(param.name)]))) + )) ) const isCall = (utils: ContextUtils, module: Module, name: string) => (node: T) => pipe( @@ -135,6 +130,21 @@ const findNamespace = (utils: ContextUtils) => flow( option.map((identifier) => identifier.name) ) +const hasArguments = (args: ReadonlyArray<(node: TSESTree.Expression) => boolean>) => flow( + ensureArguments(args), + option.isSome +) + +const ensureArguments = (args: ReadonlyArray<(node: TSESTree.Expression) => boolean>) => flow( + getArguments, + option.filter(pipe(args.length, hasLength)), + option.chain( + readonlyNonEmptyArray.traverseWithIndex(option.option)( + (i, value) => pipe(args, readonlyArray.lookup(i), option.filter(test => test(value)), option.map(constant(value))) + ) + ) +) + export default createRule({ name: "prefer-constructor", meta: { @@ -160,10 +170,10 @@ export default createRule({ node, option.of, option.filter(isCall(utils, "Either", "fold")), - option.chain(getArguments), - option.filter(hasLength(2)), - option.filter(flow(readonlyNonEmptyArray.head, isLazyValue(utils, "Option", "none"))), - option.filter(flow(readonlyNonEmptyArray.last, isCall(utils, "Option", "some"))), + option.chain(ensureArguments([ + isLazyValue(utils, "Option", "none"), + isCall(utils, "Option", "some") + ])), option.bind("namespace", flow(readonlyNonEmptyArray.head, findNamespace(utils))), option.map(({ namespace }) => { context.report({ From fc4e4ef079d82f2ded3b38d43137114124f97d9e Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Wed, 17 Feb 2021 09:43:17 +0000 Subject: [PATCH 26/30] Move utilities out of the module --- src/rules/prefer-constructor.ts | 149 +---------------------------- src/utils.ts | 162 +++++++++++++++++++++++++++++--- 2 files changed, 152 insertions(+), 159 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index 7277f9a..c097a58 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -1,149 +1,6 @@ -import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/experimental-utils" -import { option, readonlyArray, readonlyNonEmptyArray } from "fp-ts" -import { constant, flow, pipe } from "fp-ts/function" -import { calleeIdentifier, ContextUtils, contextUtils, createRule, isCallee, isFromModule, Module } from "../utils" - -const hasName = (name: string) => (identifier: TSESTree.Identifier) => identifier.name === name - -const hasLength = (length: number) => (array: ReadonlyArray) => array.length === length - -const isArrowFunctionExpression = (node: TSESTree.Node): node is TSESTree.ArrowFunctionExpression => node.type === AST_NODE_TYPES.ArrowFunctionExpression - -const isCallExpression = (node: TSESTree.Node): node is TSESTree.CallExpression => node.type === AST_NODE_TYPES.CallExpression - -const isMemberExpression = (node: TSESTree.Node): node is TSESTree.MemberExpression => node.type === AST_NODE_TYPES.MemberExpression - -const isIdentifier = (node: TSESTree.Node): node is TSESTree.Identifier => node.type === AST_NODE_TYPES.Identifier - -const getArguments = (call: TSESTree.CallExpression) => pipe( - call.arguments, - readonlyNonEmptyArray.fromArray -) - -const getFirstArgument = (call: TSESTree.CallExpression) => pipe( - call, - getArguments, - option.map(readonlyNonEmptyArray.head) -) - -const getParams = (call: TSESTree.FunctionLike) => pipe( - call.params, - readonlyNonEmptyArray.fromArray -) - -const getFirstParam = (call: TSESTree.FunctionLike) => pipe( - call, - getParams, - option.map(readonlyNonEmptyArray.head) -) - -const isIdentifierWithName = (name: string) => (node: TSESTree.Node) => pipe( - node, - option.of, - option.filter(isIdentifier), - option.exists(hasName(name)) -) - -const hasPropertyIdentifierWithName = (name: string) => (node: TSESTree.MemberExpression) => pipe( - node.property, - isIdentifierWithName(name) -) - -const getWrappedCall = (node: TSESTree.ArrowFunctionExpression) => pipe( - node.body, - option.of, - option.filter(isCallExpression), - option.filter(flow( - option.of, - option.bindTo("call"), - option.bind("param", () => pipe(node, getFirstParam, option.filter(isIdentifier))), - option.exists(({ call, param }) => pipe(call, hasArguments([isIdentifierWithName(param.name)]))) - )) -) - -const isCall = (utils: ContextUtils, module: Module, name: string) => (node: T) => pipe( - node, - option.fromPredicate(isArrowFunctionExpression), - option.chain(getWrappedCall), - option.altW(constant(option.some(node))), - option.filter(isCallee), - option.exists(isCallTo(utils, module, name)) -) - -const isCallTo = ({ typeOfNode }: ContextUtils, module: Module, name: string) => flow( - calleeIdentifier, - option.filter(hasName(name)), - option.chain(typeOfNode), - option.exists(isFromModule(module)) -) - -const isLazyValue = (utils: ContextUtils, module: Module, name: string) => flow( - findMemberExpression(utils), - option.exists(isValue(utils, module, name)) -) - -const isValue = ({ typeOfNode }: ContextUtils, module: Module, name: string) => (node: TSESTree.MemberExpression) => pipe( - node, - option.of, - option.filter(hasPropertyIdentifierWithName(name)), - option.chain(typeOfNode), - option.exists(isFromModule(module)) -) - -const findMemberExpressionFromArrowFunctionExpression = (node: TSESTree.ArrowFunctionExpression) => pipe( - node.body, - option.of, - option.filter(isMemberExpression) -) - -const isConstantCall = ({ typeOfNode }: ContextUtils) => (node: TSESTree.CallExpression) => pipe( - node, - calleeIdentifier, - option.filter(hasName("constant")), - option.chain(typeOfNode), - option.exists(isFromModule("function")) -) - -const findMemberExpressionFromCallExpression = (utils: ContextUtils) => (node: TSESTree.CallExpression) => pipe( - node, - option.of, - option.filter(isConstantCall(utils)), - option.chain(getFirstArgument), - option.filter(isMemberExpression) -) - -const findMemberExpression = (utils: ContextUtils) => (node: TSESTree.Expression) => { - switch (node.type) { - case AST_NODE_TYPES.ArrowFunctionExpression: - return findMemberExpressionFromArrowFunctionExpression(node) - case AST_NODE_TYPES.CallExpression: - return findMemberExpressionFromCallExpression(utils)(node) - default: - return option.none - } -} - -const findNamespace = (utils: ContextUtils) => flow( - findMemberExpression(utils), - option.map((node) => node.object), - option.filter(isIdentifier), - option.map((identifier) => identifier.name) -) - -const hasArguments = (args: ReadonlyArray<(node: TSESTree.Expression) => boolean>) => flow( - ensureArguments(args), - option.isSome -) - -const ensureArguments = (args: ReadonlyArray<(node: TSESTree.Expression) => boolean>) => flow( - getArguments, - option.filter(pipe(args.length, hasLength)), - option.chain( - readonlyNonEmptyArray.traverseWithIndex(option.option)( - (i, value) => pipe(args, readonlyArray.lookup(i), option.filter(test => test(value)), option.map(constant(value))) - ) - ) -) +import { option, readonlyNonEmptyArray } from "fp-ts" +import { flow, pipe } from "fp-ts/function" +import { contextUtils, createRule, ensureArguments, findNamespace, isCall, isLazyValue } from "../utils" export default createRule({ name: "prefer-constructor", diff --git a/src/utils.ts b/src/utils.ts index ebd7f6a..f399b5b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,8 +8,8 @@ import { } from "@typescript-eslint/experimental-utils"; import * as recast from "recast"; import { visitorKeys as tsVisitorKeys } from "@typescript-eslint/typescript-estree"; -import { array, option, apply, readonlyNonEmptyArray } from "fp-ts" -import { flow, pipe } from "fp-ts/function" +import { array, option, apply, readonlyArray, readonlyNonEmptyArray } from "fp-ts"; +import { constant, flow, pipe } from "fp-ts/function"; import estraverse from "estraverse"; import { @@ -52,8 +52,6 @@ export type Callee = | TSESTree.MemberExpression | TSESTree.Identifier -export const isCallee = (node: TSESTree.Node): node is Callee => isWithinTypes(node, [AST_NODE_TYPES.CallExpression, AST_NODE_TYPES.MemberExpression, AST_NODE_TYPES.Identifier]) - export function calleeIdentifier(node: Callee): option.Option { switch (node.type) { case AST_NODE_TYPES.MemberExpression: @@ -79,12 +77,17 @@ export function calleeIdentifier(node: Callee): option.Option( - n: TSESTree.Node | undefined, - types: N["type"][] -): n is N { - return !!n && types.includes(n.type); -} +const isWithinTypes = (types: T["type"][]) => (node: TSESTree.Node): node is T => types.includes(node.type) + +const isArrowFunctionExpression = isWithinTypes([AST_NODE_TYPES.ArrowFunctionExpression]) + +const isCallExpression = isWithinTypes([AST_NODE_TYPES.CallExpression]) + +const isCallee = isWithinTypes([AST_NODE_TYPES.CallExpression, AST_NODE_TYPES.MemberExpression, AST_NODE_TYPES.Identifier]) + +const isIdentifier = isWithinTypes([AST_NODE_TYPES.Identifier]) + +const isMemberExpression = isWithinTypes([AST_NODE_TYPES.MemberExpression]) type CombinatorNode = | TSESTree.CallExpression @@ -114,11 +117,11 @@ export function getAdjacentCombinators< const firstCombinatorIndex = pipeOrFlowExpression.arguments.findIndex( (a, index) => { if ( - isWithinTypes(a, combinator1.types) && + isWithinTypes(combinator1.types)(a) && index < pipeOrFlowExpression.arguments.length - 1 ) { const b = pipeOrFlowExpression.arguments[index + 1]; - if (isWithinTypes(b, combinator2.types)) { + if (b && isWithinTypes(combinator2.types)(b)) { return pipe( apply.sequenceS(option.option)({ idA: calleeIdentifier(a), @@ -232,6 +235,140 @@ export const isFromModule = (module: Module) => flow( option.exists(is(module)) ) +const hasName = (name: string) => (identifier: TSESTree.Identifier) => identifier.name === name + +const hasLength = (length: number) => (array: ReadonlyArray) => array.length === length + +const getArguments = (call: TSESTree.CallExpression) => pipe( + call.arguments, + readonlyNonEmptyArray.fromArray +) + +const getFirstArgument = (call: TSESTree.CallExpression) => pipe( + call, + getArguments, + option.map(readonlyNonEmptyArray.head) +) + +const getParams = (call: TSESTree.FunctionLike) => pipe( + call.params, + readonlyNonEmptyArray.fromArray +) + +const getFirstParam = (call: TSESTree.FunctionLike) => pipe( + call, + getParams, + option.map(readonlyNonEmptyArray.head) +) + +const isIdentifierWithName = (name: string) => (node: TSESTree.Node) => pipe( + node, + option.of, + option.filter(isIdentifier), + option.exists(hasName(name)) +) + +const hasPropertyIdentifierWithName = (name: string) => (node: TSESTree.MemberExpression) => pipe( + node.property, + isIdentifierWithName(name) +) + +const getWrappedCall = (node: TSESTree.ArrowFunctionExpression) => pipe( + node.body, + option.of, + option.filter(isCallExpression), + option.filter(flow( + option.of, + option.bindTo("call"), + option.bind("param", () => pipe(node, getFirstParam, option.filter(isIdentifier))), + option.exists(({ call, param }) => pipe(call, hasArguments([isIdentifierWithName(param.name)]))) + )) +) + +export const isCall = (utils: ContextUtils, module: Module, name: string) => (node: T) => pipe( + node, + option.fromPredicate(isArrowFunctionExpression), + option.chain(getWrappedCall), + option.altW(constant(option.some(node))), + option.filter(isCallee), + option.exists(isCallTo(utils, module, name)) +) + +const isCallTo = ({ typeOfNode }: ContextUtils, module: Module, name: string) => flow( + calleeIdentifier, + option.filter(hasName(name)), + option.chain(typeOfNode), + option.exists(isFromModule(module)) +) + +export const isLazyValue = (utils: ContextUtils, module: Module, name: string) => flow( + findMemberExpression(utils), + option.exists(isValue(utils, module, name)) +) + +const isValue = ({ typeOfNode }: ContextUtils, module: Module, name: string) => (node: TSESTree.MemberExpression) => pipe( + node, + option.of, + option.filter(hasPropertyIdentifierWithName(name)), + option.chain(typeOfNode), + option.exists(isFromModule(module)) +) + +const findMemberExpressionFromArrowFunctionExpression = (node: TSESTree.ArrowFunctionExpression) => pipe( + node.body, + option.of, + option.filter(isMemberExpression) +) + +const isConstantCall = ({ typeOfNode }: ContextUtils) => (node: TSESTree.CallExpression) => pipe( + node, + calleeIdentifier, + option.filter(hasName("constant")), + option.chain(typeOfNode), + option.exists(isFromModule("function")) +) + +const findMemberExpressionFromCallExpression = (utils: ContextUtils) => (node: TSESTree.CallExpression) => pipe( + node, + option.of, + option.filter(isConstantCall(utils)), + option.chain(getFirstArgument), + option.filter(isMemberExpression) +) + +const findMemberExpression = (utils: ContextUtils) => (node: TSESTree.Expression) => { + switch (node.type) { + case AST_NODE_TYPES.ArrowFunctionExpression: + return findMemberExpressionFromArrowFunctionExpression(node) + case AST_NODE_TYPES.CallExpression: + return findMemberExpressionFromCallExpression(utils)(node) + default: + return option.none + } +} + +const hasArguments = (args: ReadonlyArray<(node: TSESTree.Expression) => boolean>) => flow( + ensureArguments(args), + option.isSome +) + +export const ensureArguments = (args: ReadonlyArray<(node: TSESTree.Expression) => boolean>) => flow( + getArguments, + option.filter(pipe(args.length, hasLength)), + option.chain( + readonlyNonEmptyArray.traverseWithIndex(option.option)( + (i, value) => pipe(args, readonlyArray.lookup(i), option.filter(test => test(value)), option.map(constant(value))) + ) + ) +) + +export const findNamespace = (utils: ContextUtils) => flow( + findMemberExpression(utils), + option.map((node) => node.object), + option.filter(isIdentifier), + option.map((identifier) => identifier.name) +) + export type ContextUtils = ReturnType export const contextUtils = < @@ -505,7 +642,6 @@ export const contextUtils = < } return { - findModuleImport, addNamedImportIfNeeded, removeImportDeclaration, isFlowExpression, From 9cf00de4d51c26d23d4d9eeeeff05c9dc77559ca Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Wed, 17 Feb 2021 09:59:18 +0000 Subject: [PATCH 27/30] Add to documentation and configuration --- README.md | 1 + docs/rules/prefer-constructor.md | 35 ++++++++++++++++++++++++++++++++ src/index.ts | 1 + src/rules/prefer-constructor.ts | 6 +++--- 4 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 docs/rules/prefer-constructor.md diff --git a/README.md b/README.md index 637b771..43d4047 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ If your project is a multi-package monorepo, you can follow the instructions | [fp-ts/prefer-chain](docs/rules/prefer-chain.md) | Replace `map` + `flatten` with `chain` | 💡 | | | [fp-ts/prefer-bimap](docs/rules/prefer-bimap.md) | Replace `map` + `mapLeft` with `bimap` | 💡 | | | [fp-ts/no-discarded-pure-expression](docs/rules/no-discarded-pure-expression.md) | Disallow expressions returning pure data types (like `Task` or `IO`) in statement position | 💡 | 🦄 | +| [fp-ts/prefer-constructor](docs/rules/prefer-constructor.md) | Replace destructors with constructors | 💡 | 🦄 | ### Fixable legend: diff --git a/docs/rules/prefer-constructor.md b/docs/rules/prefer-constructor.md new file mode 100644 index 0000000..5b86f8f --- /dev/null +++ b/docs/rules/prefer-constructor.md @@ -0,0 +1,35 @@ +# Replace destructors with constructors (fp-ts/prefer-constructor) + +Suggest replacing the combination of a destructor and constructors with a constructor when changing types. + +This rule covers: + +- `Option.fromEither` + +**💡 Fixable**: This rule provides in-editor suggested fixes. + +## Rule Details + +Examples of **incorrect** code for this rule: + +```ts +import { either, option } from "fp-ts" +import { pipe } from "fp-ts/function" + +pipe( + either.of(1), + either.fold(() => option.none, option.some) +) +``` + +Examples of **correct** code for this rule: + +```ts +import { either, option } from "fp-ts" +import { pipe } from "fp-ts/function" + +pipe( + either.of(1), + option.fromEither +) +``` diff --git a/src/index.ts b/src/index.ts index c1642a4..b1346bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ const suggestions = { "no-redundant-flow": require("./rules/no-redundant-flow").default, "prefer-chain": require("./rules/prefer-chain").default, "prefer-bimap": require("./rules/prefer-bimap").default, + "prefer-constructor": require("./rules/prefer-constructor").default }; export const rules = { diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index c097a58..ffdbef4 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -10,12 +10,12 @@ export default createRule({ schema: [], docs: { category: "Best Practices", - description: "afsafaf", + description: "Replace destructor + constructors with a constructor", recommended: "warn" }, messages: { - eitherFoldIsOptionFromEither: "cacsaffg", - replaceEitherFoldWithOptionFromEither: "dsagdgdsg" + eitherFoldIsOptionFromEither: "Either.fold can be replaced with Option.fromEither", + replaceEitherFoldWithOptionFromEither: "replace Either.fold with Option.fromEither" } }, defaultOptions: [], From ebf3ad7c944b0e0f5c932347df17e6e3a13ac019 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Wed, 17 Feb 2021 10:12:42 +0000 Subject: [PATCH 28/30] Move context-aware utilities into contextUtils --- src/rules/prefer-constructor.ts | 12 +-- src/utils.ts | 131 ++++++++++++++++---------------- 2 files changed, 72 insertions(+), 71 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index ffdbef4..8960ffb 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -1,6 +1,6 @@ import { option, readonlyNonEmptyArray } from "fp-ts" import { flow, pipe } from "fp-ts/function" -import { contextUtils, createRule, ensureArguments, findNamespace, isCall, isLazyValue } from "../utils" +import { contextUtils, createRule, ensureArguments } from "../utils" export default createRule({ name: "prefer-constructor", @@ -20,18 +20,18 @@ export default createRule({ }, defaultOptions: [], create(context) { - const utils = contextUtils(context) + const { findNamespace, isCall, isLazyValue } = contextUtils(context) return { CallExpression(node) { pipe( node, option.of, - option.filter(isCall(utils, "Either", "fold")), + option.filter(isCall( "Either", "fold")), option.chain(ensureArguments([ - isLazyValue(utils, "Option", "none"), - isCall(utils, "Option", "some") + isLazyValue( "Option", "none"), + isCall("Option", "some") ])), - option.bind("namespace", flow(readonlyNonEmptyArray.head, findNamespace(utils))), + option.bind("namespace", flow(readonlyNonEmptyArray.head, findNamespace)), option.map(({ namespace }) => { context.report({ loc: { diff --git a/src/utils.ts b/src/utils.ts index f399b5b..abb2693 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -285,68 +285,12 @@ const getWrappedCall = (node: TSESTree.ArrowFunctionExpression) => pipe( )) ) -export const isCall = (utils: ContextUtils, module: Module, name: string) => (node: T) => pipe( - node, - option.fromPredicate(isArrowFunctionExpression), - option.chain(getWrappedCall), - option.altW(constant(option.some(node))), - option.filter(isCallee), - option.exists(isCallTo(utils, module, name)) -) - -const isCallTo = ({ typeOfNode }: ContextUtils, module: Module, name: string) => flow( - calleeIdentifier, - option.filter(hasName(name)), - option.chain(typeOfNode), - option.exists(isFromModule(module)) -) - -export const isLazyValue = (utils: ContextUtils, module: Module, name: string) => flow( - findMemberExpression(utils), - option.exists(isValue(utils, module, name)) -) - -const isValue = ({ typeOfNode }: ContextUtils, module: Module, name: string) => (node: TSESTree.MemberExpression) => pipe( - node, - option.of, - option.filter(hasPropertyIdentifierWithName(name)), - option.chain(typeOfNode), - option.exists(isFromModule(module)) -) - const findMemberExpressionFromArrowFunctionExpression = (node: TSESTree.ArrowFunctionExpression) => pipe( node.body, option.of, option.filter(isMemberExpression) ) -const isConstantCall = ({ typeOfNode }: ContextUtils) => (node: TSESTree.CallExpression) => pipe( - node, - calleeIdentifier, - option.filter(hasName("constant")), - option.chain(typeOfNode), - option.exists(isFromModule("function")) -) - -const findMemberExpressionFromCallExpression = (utils: ContextUtils) => (node: TSESTree.CallExpression) => pipe( - node, - option.of, - option.filter(isConstantCall(utils)), - option.chain(getFirstArgument), - option.filter(isMemberExpression) -) - -const findMemberExpression = (utils: ContextUtils) => (node: TSESTree.Expression) => { - switch (node.type) { - case AST_NODE_TYPES.ArrowFunctionExpression: - return findMemberExpressionFromArrowFunctionExpression(node) - case AST_NODE_TYPES.CallExpression: - return findMemberExpressionFromCallExpression(utils)(node) - default: - return option.none - } -} - const hasArguments = (args: ReadonlyArray<(node: TSESTree.Expression) => boolean>) => flow( ensureArguments(args), option.isSome @@ -362,15 +306,6 @@ export const ensureArguments = (args: ReadonlyArray<(node: TSESTree.Expression) ) ) -export const findNamespace = (utils: ContextUtils) => flow( - findMemberExpression(utils), - option.map((node) => node.object), - option.filter(isIdentifier), - option.map((identifier) => identifier.name) -) - -export type ContextUtils = ReturnType - export const contextUtils = < TMessageIds extends string, TOptions extends readonly unknown[] @@ -641,6 +576,69 @@ export const contextUtils = < ); } + const isCall = (module: Module, name: string) => (node: T) => pipe( + node, + option.fromPredicate(isArrowFunctionExpression), + option.chain(getWrappedCall), + option.altW(constant(option.some(node))), + option.filter(isCallee), + option.exists(isCallTo(module, name)) + ) + + const isCallTo = (module: Module, name: string) => flow( + calleeIdentifier, + option.filter(hasName(name)), + option.chain(typeOfNode), + option.exists(isFromModule(module)) + ) + + const isLazyValue = (module: Module, name: string) => flow( + findMemberExpression, + option.exists(isValue(module, name)) + ) + + const isValue = (module: Module, name: string) => (node: TSESTree.MemberExpression) => pipe( + node, + option.of, + option.filter(hasPropertyIdentifierWithName(name)), + option.chain(typeOfNode), + option.exists(isFromModule(module)) + ) + + const isConstantCall = (node: TSESTree.CallExpression) => pipe( + node, + calleeIdentifier, + option.filter(hasName("constant")), + option.chain(typeOfNode), + option.exists(isFromModule("function")) + ) + + const findMemberExpressionFromCallExpression = (node: TSESTree.CallExpression) => pipe( + node, + option.of, + option.filter(isConstantCall), + option.chain(getFirstArgument), + option.filter(isMemberExpression) + ) + + const findMemberExpression = (node: TSESTree.Expression) => { + switch (node.type) { + case AST_NODE_TYPES.ArrowFunctionExpression: + return findMemberExpressionFromArrowFunctionExpression(node) + case AST_NODE_TYPES.CallExpression: + return findMemberExpressionFromCallExpression(node) + default: + return option.none + } + } + + const findNamespace = flow( + findMemberExpression, + option.map((node) => node.object), + option.filter(isIdentifier), + option.map((identifier) => identifier.name) + ) + return { addNamedImportIfNeeded, removeImportDeclaration, @@ -651,5 +649,8 @@ export const contextUtils = < typeOfNode, isFromFpTs, parserServices, + isCall, + isLazyValue, + findNamespace, }; }; From a4a384eab0a1d720720c55c7b448cfbca28d2d06 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Wed, 17 Feb 2021 10:42:26 +0000 Subject: [PATCH 29/30] Simplify utility changes --- src/utils.ts | 71 ++++++++++++++++------------------------------------ 1 file changed, 22 insertions(+), 49 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index abb2693..904c48b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -34,9 +34,7 @@ declare module "typescript" { const modules = ["Either", "Option", "function"] as const -export type Module = typeof modules[number] - -const is = (original: T) => (value: unknown): value is T => original === value +type Module = typeof modules[number] const isModule = (name: string): name is Module => modules.includes(name as Module) @@ -47,7 +45,7 @@ export const createRule = ESLintUtils.RuleCreator( `https://github.com/buildo/eslint-plugin-fp-ts/blob/v${version}/docs/rules/${name}.md` ); -export type Callee = +type Callee = | TSESTree.CallExpression | TSESTree.MemberExpression | TSESTree.Identifier @@ -230,22 +228,17 @@ const getModule = flow( option.chain(getFpTsModule) ) -export const isFromModule = (module: Module) => flow( +const isFromModule = (expected: Module) => flow( getModule, - option.exists(is(module)) + option.exists(module => module === expected) ) -const hasName = (name: string) => (identifier: TSESTree.Identifier) => identifier.name === name - -const hasLength = (length: number) => (array: ReadonlyArray) => array.length === length - const getArguments = (call: TSESTree.CallExpression) => pipe( call.arguments, readonlyNonEmptyArray.fromArray ) -const getFirstArgument = (call: TSESTree.CallExpression) => pipe( - call, +const getFirstArgument = flow( getArguments, option.map(readonlyNonEmptyArray.head) ) @@ -255,50 +248,35 @@ const getParams = (call: TSESTree.FunctionLike) => pipe( readonlyNonEmptyArray.fromArray ) -const getFirstParam = (call: TSESTree.FunctionLike) => pipe( - call, +const getFirstParam = flow( getParams, option.map(readonlyNonEmptyArray.head) ) -const isIdentifierWithName = (name: string) => (node: TSESTree.Node) => pipe( - node, - option.of, - option.filter(isIdentifier), - option.exists(hasName(name)) -) - -const hasPropertyIdentifierWithName = (name: string) => (node: TSESTree.MemberExpression) => pipe( - node.property, - isIdentifierWithName(name) +const isIdentifierWithName = (expected: string) => flow( + option.fromPredicate(isIdentifier), + option.exists(({ name }) => name === expected) ) const getWrappedCall = (node: TSESTree.ArrowFunctionExpression) => pipe( node.body, - option.of, - option.filter(isCallExpression), + option.fromPredicate(isCallExpression), option.filter(flow( option.of, option.bindTo("call"), option.bind("param", () => pipe(node, getFirstParam, option.filter(isIdentifier))), - option.exists(({ call, param }) => pipe(call, hasArguments([isIdentifierWithName(param.name)]))) + option.exists(({ call, param }) => pipe(call, ensureArguments([isIdentifierWithName(param.name)]), option.isSome)) )) ) const findMemberExpressionFromArrowFunctionExpression = (node: TSESTree.ArrowFunctionExpression) => pipe( node.body, - option.of, - option.filter(isMemberExpression) -) - -const hasArguments = (args: ReadonlyArray<(node: TSESTree.Expression) => boolean>) => flow( - ensureArguments(args), - option.isSome + option.fromPredicate(isMemberExpression) ) export const ensureArguments = (args: ReadonlyArray<(node: TSESTree.Expression) => boolean>) => flow( getArguments, - option.filter(pipe(args.length, hasLength)), + option.filter((array) => array.length === args.length), option.chain( readonlyNonEmptyArray.traverseWithIndex(option.option)( (i, value) => pipe(args, readonlyArray.lookup(i), option.filter(test => test(value)), option.map(constant(value))) @@ -585,9 +563,9 @@ export const contextUtils = < option.exists(isCallTo(module, name)) ) - const isCallTo = (module: Module, name: string) => flow( + const isCallTo = (module: Module, expected: string) => flow( calleeIdentifier, - option.filter(hasName(name)), + option.filter(({name}) => name === expected), option.chain(typeOfNode), option.exists(isFromModule(module)) ) @@ -597,26 +575,21 @@ export const contextUtils = < option.exists(isValue(module, name)) ) - const isValue = (module: Module, name: string) => (node: TSESTree.MemberExpression) => pipe( - node, - option.of, - option.filter(hasPropertyIdentifierWithName(name)), + const isValue = (module: Module, name: string) => flow( + option.fromPredicate((node: TSESTree.MemberExpression) => pipe(node.property, isIdentifierWithName(name))), option.chain(typeOfNode), option.exists(isFromModule(module)) ) - const isConstantCall = (node: TSESTree.CallExpression) => pipe( - node, - calleeIdentifier, - option.filter(hasName("constant")), + const isConstantCall = flow( + (node: TSESTree.CallExpression) => pipe(node, calleeIdentifier), + option.filter(({ name }) => name === "constant"), option.chain(typeOfNode), option.exists(isFromModule("function")) ) - const findMemberExpressionFromCallExpression = (node: TSESTree.CallExpression) => pipe( - node, - option.of, - option.filter(isConstantCall), + const findMemberExpressionFromCallExpression = flow( + option.fromPredicate(isConstantCall), option.chain(getFirstArgument), option.filter(isMemberExpression) ) From cb92266dea83c8c9b674b5c690d3892f38c3f05d Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Wed, 17 Feb 2021 16:22:44 +0000 Subject: [PATCH 30/30] CS tweaks --- src/rules/prefer-constructor.ts | 4 +-- tests/rules/prefer-constructor.test.ts | 44 +++++++++++++------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/rules/prefer-constructor.ts b/src/rules/prefer-constructor.ts index 8960ffb..df6c21f 100644 --- a/src/rules/prefer-constructor.ts +++ b/src/rules/prefer-constructor.ts @@ -26,9 +26,9 @@ export default createRule({ pipe( node, option.of, - option.filter(isCall( "Either", "fold")), + option.filter(isCall("Either", "fold")), option.chain(ensureArguments([ - isLazyValue( "Option", "none"), + isLazyValue("Option", "none"), isCall("Option", "some") ])), option.bind("namespace", flow(readonlyNonEmptyArray.head, findNamespace)), diff --git a/tests/rules/prefer-constructor.test.ts b/tests/rules/prefer-constructor.test.ts index 57d2fef..0f3663b 100644 --- a/tests/rules/prefer-constructor.test.ts +++ b/tests/rules/prefer-constructor.test.ts @@ -30,7 +30,7 @@ ruleTester.run("prefer-constructor", rule, { either.of(1), either.fold(() => option.none, (value) => option.some(otherValue)) ) - `, + ` }, { code: stripIndent` @@ -41,7 +41,7 @@ ruleTester.run("prefer-constructor", rule, { either.of(1), either.fold(() => option.some(otherValue), (value) => option.some(value)) ) - `, + ` }, { code: stripIndent` @@ -52,8 +52,8 @@ ruleTester.run("prefer-constructor", rule, { either.of(1), either.fold(() => option.some(otherValue), () => option.none) ) - `, - }, + ` + } ], invalid: [ { @@ -80,11 +80,11 @@ ruleTester.run("prefer-constructor", rule, { either.of(1), option.fromEither ) - `, - }, - ], - }, - ], + ` + } + ] + } + ] }, { code: stripIndent` @@ -142,11 +142,11 @@ ruleTester.run("prefer-constructor", rule, { either.of(1), option.fromEither ) - `, - }, - ], - }, - ], + ` + } + ] + } + ] }, { code: stripIndent` @@ -172,11 +172,11 @@ ruleTester.run("prefer-constructor", rule, { either.of(1), option.fromEither ) - `, - }, - ], - }, - ], - }, - ], -}); + ` + } + ] + } + ] + } + ] +})