diff --git a/README.md b/README.md index fffea99..461eaa6 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,5 @@ and enable the rules you want, for example 'pipeable' module - [fp-ts/prefer-traverse](docs/rules/prefer-traverse.md): Replace map + sequence with traverse +- [fp-ts/no-redundant-flow](docs/rules/no-redundant-flow.md): Remove redundant + uses of flow diff --git a/docs/rules/no-redundant-flow.md b/docs/rules/no-redundant-flow.md new file mode 100644 index 0000000..41b8d4f --- /dev/null +++ b/docs/rules/no-redundant-flow.md @@ -0,0 +1,31 @@ +# Remove redundant uses of flow (fp-ts/no-redundant-flow) + +Suggest removing `flow` when it only has one argument. This can happen after a +refactoring that removed some combinators from a flow expression. + +**Fixable**: This rule is automatically fixable using the `--fix` flag on the +command line. + +## Rule Details + +Example of **incorrect** code for this rule: + +```ts +import { flow } from "fp-ts/pipeable"; +import { some, Option } from "fp-ts/Option"; + +const f: (n: number): Option = flow(some); +``` + +Example of **correct** code for this rule: + +```ts +import { flow } from "fp-ts/pipeable"; +import { some, filter, Option } from "fp-ts/Option"; + +const f: (n: number): Option = + flow( + some, + filter((n) => n > 2) + ); +``` diff --git a/src/index.ts b/src/index.ts index 0546809..d3b9e0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,4 +2,5 @@ export const rules = { "no-lib-imports": require("./rules/no-lib-imports"), "no-pipeable": require("./rules/no-pipeable"), "prefer-traverse": require("./rules/prefer-traverse"), + "no-redundant-flow": require("./rules/no-redundant-flow"), }; diff --git a/src/rules/no-redundant-flow.ts b/src/rules/no-redundant-flow.ts new file mode 100644 index 0000000..6f34a9e --- /dev/null +++ b/src/rules/no-redundant-flow.ts @@ -0,0 +1,44 @@ +import { TSESLint } from "@typescript-eslint/experimental-utils"; +import { isFlowExpression } from "../utils"; + +const messages = { + redundantFlow: "flow can be removed because it takes only one argument", + removeFlow: "remove flow", +} as const; +type MessageIds = keyof typeof messages; + +export const meta: TSESLint.RuleMetaData = { + type: "suggestion", + fixable: "code", + schema: [], + messages, +}; + +export function create( + context: TSESLint.RuleContext +): TSESLint.RuleListener { + return { + CallExpression(node) { + if (node.arguments.length === 1 && isFlowExpression(node, context)) { + context.report({ + node, + messageId: "redundantFlow", + suggest: [ + { + messageId: "removeFlow", + fix(fixer) { + return [ + fixer.removeRange([ + node.callee.range[0], + node.callee.range[1] + 1, + ]), + fixer.removeRange([node.range[1] - 1, node.range[1]]), + ]; + }, + }, + ], + }); + } + }, + }; +} diff --git a/src/utils.ts b/src/utils.ts index 961461a..57fe4a3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -40,6 +40,21 @@ export function calleeIdentifier( return undefined; } +export function isFlowExpression< + TMessageIds extends string, + TOptions extends readonly unknown[] +>( + node: TSESTree.CallExpression, + context: TSESLint.RuleContext +): boolean { + const callee = calleeIdentifier(node); + return !!( + callee && + callee.name === "flow" && + isIdentifierImportedFrom(callee, /fp-ts\//, context) + ); +} + export function isPipeOrFlowExpression< TMessageIds extends string, TOptions extends readonly unknown[] diff --git a/tests/rules/no-pipeable.test.ts b/tests/rules/no-pipeable.test.ts index e6bada5..021c4c9 100644 --- a/tests/rules/no-pipeable.test.ts +++ b/tests/rules/no-pipeable.test.ts @@ -8,7 +8,7 @@ const ruleTester = new ESLintUtils.RuleTester({ }, }); -ruleTester.run("no-lib-imports", rule, { +ruleTester.run("no-pipeable", rule, { valid: [ 'import { pipe } from "fp-ts/function"', 'import { pipe } from "fp-ts/lib/function"', diff --git a/tests/rules/no-redundant-flow.test.ts b/tests/rules/no-redundant-flow.test.ts new file mode 100644 index 0000000..d7a9aae --- /dev/null +++ b/tests/rules/no-redundant-flow.test.ts @@ -0,0 +1,69 @@ +import * as rule from "../../src/rules/no-redundant-flow"; +import { ESLintUtils } from "@typescript-eslint/experimental-utils"; + +const ruleTester = new ESLintUtils.RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + sourceType: "module", + }, +}); + +ruleTester.run("no-redundant-flow", rule, { + valid: [ + `import { flow } from "fp-ts/function" + flow(foo, bar) + `, + `import { flow } from "fp-ts/function" + flow( + foo, + bar + ) + `, + ], + invalid: [ + { + code: ` +import { flow } from "fp-ts/function" +const a = flow(foo) +`, + errors: [ + { + messageId: "redundantFlow", + suggestions: [ + { + messageId: "removeFlow", + output: ` +import { flow } from "fp-ts/function" +const a = foo +`, + }, + ], + }, + ], + }, + { + code: ` +import { flow } from "fp-ts/function" +const a = flow( + foo +) +`, + errors: [ + { + messageId: "redundantFlow", + suggestions: [ + { + messageId: "removeFlow", + output: ` +import { flow } from "fp-ts/function" +const a = ${""} + foo + +`, + }, + ], + }, + ], + }, + ], +});